ruby-on-railsscopenested-attributesvalidates-uniqueness-of

Rails - Validate Nested Attributes Uniqueness with scope parent of parent


I have a problem with the scoped uniqueness validation in Rails for nested attributes with a parent of parent.

Background

I have a rails 4 application with 3 models :

#app/models/account.rb
class Account < ActiveRecord::Base
  has_many :contacts, dependent: :destroy
end

#app/models/contact.rb
class Contact < ActiveRecord::Base
  belongs_to :account
  has_many :email_addresses, dependent: :destroy, validate: :true, inverse_of: :contact
  accepts_nested_attributes_for :email_addresses,allow_destroy: true
  validates :email_addresses, presence: true
end

#app/models/email_address.rb
class EmailAddress  < ActiveRecord::Base
  belongs_to :contact, inverse_of: :email_addresses

  validates :label, presence: true
  validates :contact, presence: true
  validates :email, uniqueness: true, presence: true
  validates_email_format_of :email
end

Issue

I want make a scope, so as to make sure the attribute :email of the model EmailAddress is unique at the Account Level (Account is parent of Contact, which is itself parent of EmailAddress).

As suggested at http://guides.rubyonrails.org/active_record_validations.html, I tried :

 class EmailAddress  < ActiveRecord::Base
  belongs_to :contact, inverse_of: :email_addresses

  validates :label, presence: true
  validates :contact, presence: true
  validates :email, presence: true, uniqueness: { scope: :account, 
                    message: "This contact email is already taken" }
  validates_email_format_of :email
 end

This raises the error "column email_addresses.account does not exist" What should I do ?

Thanks for you help !


Solution

  • A better option in terms of performances is described below. It is tested and works just fine.

    Why?

    Mapping emails can consume a lot of ressources when a lot of emails are at stake, so its better to perform the scope directly with the database.

    How?

    Cashing the account_id in the EmailAddress model and performing a before validation method.

    1) Create a migration :

    change_table :email_addresses do |t|
      t.references :account, index: true
    end
    add_index :email_addresses, [:account_id, :email], unique: true
    

    2) Migrate

    3) Update the EmailAddress model

    #app/models/email_address.rb
    
    class EmailAddress < ActiveRecord::Base
      belongs_to :contact, inverse_of: :email_addresses
      belongs_to :account
    
      validates :label, presence: true
      validates :contact, presence: true
      validates_email_format_of :email
      validates_uniqueness_of :email, allow_blank: false, scope: :account
    
      before_validation do
        self.account = contact.account if contact
      end
    
    end