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
: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
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
: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?