I recently ran into a problem where I needed to use validates_uniqueness_of with more than one scope. Basically, I have resources that users can either share or keep to themselves. Each resource has a description that should be unique. However, the scope of the uniqueness depends on the situation. In pseudo-code, I wanted something like:

if shared?
  validates_uniqueness_of :description, :scope => :shared
else
  validates_uniqueness_of :description, :scope => :user_id
end

(That is, if you share, the description should be unique across all the shared resources. Otherwise, it should just be unique within your stuff.)

That seems straightforward, but it’s complicated by the fact that Rails validations are at the class-level rather than the instance-level. To me, it makes more sense to execute validations at the instance-level.

Specifically because they’re not executed at the instance-level, validations like validates_uniqueness_of need “smelly” option keys like :if and :unless. If the validations were executed in the proper context (i.e., in the context of the instance that’s being validated), Rails wouldn’t need any of that hackery. The real if and unless could be used directly.

The Hashrocket folks said something similar in their Rails 3 Blog Club – Week 1 video. As far as I know, the situation is staying the same in Rails 3, even with all the validation refactoring. Even so, I think there’s a strong argument for moving validations to the instance level.

For example, if validations were executed in the context of the model instance, the above would have translated directly to something like this:

class MyModel < ActiveRecord::Base
  # [...]

  def validate
    if shared?
      validate_uniqueness_of :description, :scope => :shared
    else
      validate_uniqueness_of :description, :scope => :user_id
    end
  end
end

(Note for blog skimmers, the above code does NOT work.)

Instead, I ended up with the following:

class MyModel < ActiveRecord::Base
  validates_uniqueness_of :description, :scope => :shared, :if => :shared?
  validates_uniqueness_of :description, :scope => :user_id, :unless => :shared?
end

The :if / :unless keys still feel like a hack to me. (I’m scared they will break in non-obvious ways, to be honest – I think I’ve already found a couple of corner cases.)

Am I missing something when it comes to why validations were designed this way? If so, how would you approach this problem?