Search code examples
ruby-on-railsactiverecordvalidates-uniqueness-of

Validate uniqueness of attribute scoped by another attribute presence


If I need to validate uniqueness of cat name scoped by owner so that no owner can have many cats with same name I can use that:

validates_uniqueness_of :name, scope: :owner_id

But how can I validate the uniqueness scoped by that the cat has owner or not?

So that all cats that have an owner, have unique names.

And all cats that don't have an owner, have unique names.

So it's something like scoped by scope: self.owner.present?.

Can I do so with validations helpers without using custom validation method?


Solution

  • You can assign a conditions clause to your uniqueness validation. See the documentation, which gives this example:

    class Article < ActiveRecord::Base
      validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
    end
    

    So if you wanted to ensure uniqueness for all cats with no owner, you would write

    validates_uniqueness_of :name, conditions: -> { where(owner_id: nil) }
    

    Or if you wanted to ensure that all cats with owners are uniquely named, regardless of who their owner is:

    validates_uniqueness_of :name, conditions: -> { where.not(owner_id: nil) }
    

    If you only want to do one or the other, that'd work. However, if you want to do both, you'd find that both conditions are checked - and effectively, you're back with a uniqueness constraint that checks against the whole table, but in two queries instead of one.

    So, if we do want both checks, we need to make sure that only one of them gets checked, which we can do with if/unless modifiers:

    validates_uniqueness_of :name, conditions: -> { where(owner_id: nil) },
        if: -> { owner_id.nil? }
    validates_uniqueness_of :name, conditions: -> { where.not(owner_id: nil) },
        unless: -> { owner_id.nil? }
    

    So now, if your cat has an owner ID, the validation that checks uniqueness within the scope of all records with an owner ID will run. But if it doesn't have an owner ID, the other validation will kick in instead.

    It means a few more hoops to jump through, but should meet your need.