I have the models Account and User. Both models have an email attribute.
An Account has_many :users and a User belongs_to :account
I would like to validate the uniqueness of the email accross both models when an Account is being created so the Account email is invalid if it's taken by a User (since the account email later becomes the admin user email).
I added a scope to the email constraint in the Account model but it is not working (the form is not being rejected).
Account model:
has_many :users
validates :email, uniqueness: { scope: :users, case_sensitive: false }
What is the correct way to implement this? Do I need to add an index to the DB?
An application level validation doesn't actually guarentee the uniqueness of your data - it merely catches a lot of the cases where users try to input duplicates and provides feedback.
So while you could naively implement a custom validation that queries both tables it won't even survive the race condition caused by a double clicking senior citizen or any insertion method that circumvents validations.
Since there is no way (AFAIK) to create indices across tables so you might want to just restructure your domain and add an "owner" (call it whatever you want) to the accounts table:
class AddOwnerToAccounts < ActiveRecord::Migration[6.1]
def change
add_reference :accounts, :owner, null: false, foreign_key: { to_table: 'users' }
end
end
class Account < ApplicationRecord
has_many :users
belongs_to :owner,
class_name: 'User',
inverse_of: :owned_accounts
delegate :email, to: :owner
end
class User < ApplicationRecord
belongs_to :account
has_many :owned_accounts,
class_name: 'Account',
foreign_key: :owner_id,
inverse_of: :owner
end
This is an alternative method based on a separate table and a polymorphic assocation:
class CreateEmailAddresses < ActiveRecord::Migration[6.1]
def change
create_table :email_addresses do |t|
t.string :email, unique: true
t.references :entitity, polymorphic: true
t.timestamps
end
add_index :email_addresses, [:entity_type, :entity_id], unique: true
end
end
class EmailAddress < ApplicationRecord
validates_uniqueness_of :email
validates_uniqueness_of :entity_id, scope: :entity_type
belongs_to :entity, polymorphic: true
end
class User < ApplicationRecord
has_one :email_address,
as: :entity,
dependent: :destroy
delegate :email, to: :email_address
accepts_nested_attributes_for :email_address
end
class Account < ApplicationRecord
has_one :email_address,
as: :entity,
dependent: :destroy
delegate :email, to: :email_address
accepts_nested_attributes_for :email_address
end
It avoids having to restructure your domain or create a user simply to create a email but will cause issues with authentication libraries such as Devise as well as lacking a real foreign key to guarentee referential integrity which can lead to orphaned records.
IMO not a great solution as it most likely will create as many problems as it solves.
This an only be achieved with a custom validation method.
validate :email_unique_for_account_and_user
private
def email_unique_for_account_and_user
if User.where(email: email).where.not(id: id).exists? ||
Account.where(email: email).where.not(id: id).exists?
errors.add(:email, :taken)
end
end
IMO it is not possible to define a unique index over multiple database tables.