Amazingly complicated ActiveRecord ratio

I have a model User. The user has a lot EmailAddresses, and they choose one of them as their own primary_email_address, to which I send emails. A user must have at least one email address and must have a primary email address. The primary email address may be destroyed, but the new primary email address must be assigned to the user.

This turned out to be a surprisingly difficult situation, and every solution I tried has some unsatisfactory elements. This seems like a very common class of problems (A has a lot of B, and one of their Bs is special), so I would like to know how to solve it cleanly.

Solution 1 - A logical column in EmailAddress saying whether it is primary or not

Sort of:

class User < ActiveRecord::Base
  has_many :email_addresses, inverse_of: :user

  validates :has_exactly_one_primary_email_address

  def primary_email_address
    email_addresses.where(is_primary:true).first
  end

  def has_exactly_one_primary_email_address
    # ...
  end
end

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

  before_destroy :check_not_users_only_email_address
  after_destroy :reassign_user_primary_email_address_if_necessary

  # the logic for both these methods should live on the user but you get the idea
  def reassign_user_primary_email_address_if_necessary
    # ...
  end

  def check_not_users_only_email_address
    # ...
  end
end

This is conceptually inconvenient because it is so important that the user has exactly one primary email address, and checking it against multiple email addresses seems to be bad. And although I know that ActiveRecord transactions should mean that the user is not stuck without a primary email address, this seems like a recipe for disaster. The primary email address is basically what belongs to the user, and the inclusion of this logic in the EmailAddress model is unideal.

Solution 2 column - user_id in EmailAddress

Sort of:

class User < ActiveRecord::Base
  has_many :email_addresses, inverse_of: :user
  belongs_to :primary_email_address

  validates_presence_of :primary_email_address
end

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

  before_destroy :check_not_users_only_email_address
  after_destroy :reassign_user_primary_email_address_if_necessary

  # the logic for both these methods should live on the user but you get the idea
  def reassign_user_primary_email_address_if_necessary
    # ...
  end

  def check_not_users_only_email_address
    # ...
  end
end

, , 1 , . , . user.primary_email_address , user.email_addresses, , , .

> u = User.last
> u.email_addresses.map(&:email)
=> ["monkey@hotmail.com", "gorilla@gmail.com"]
> u.primary_email_address.destroy
=> true
> u.email_addresses.map(&:email)
=> ["monkey@hotmail.com", "gorilla@gmail.com"]
> u.reload
> u.email_addresses.map(&:email)
=> ["monkey@hotmail.com"]

after_destroy ​​ . , -, belongs_to :primary_email_address User. , ActiveRecord (has_many :email_addresses/belongs_to :user belongs_to :primary_email_address).

2 , ( ), . , . .

+5
2

, -. , , . , .

EmailAddresses , , - after_save after_destroy :

class EmailAdress < AR::Base
  belongs_to :user, :inverse_of :email_addresses

  scope :primary, where(:primary => true)

  after_save :ensure_single_primary_email
  after_destroy :ensure_primary_email_exists

  def ensure_single_primary_email
    user.verify_primary_email(self) if new_record? || primary_changed?
  end

  def ensure_primary_email_exists
    user.ensure_primary_email
  end
end

class User < AR::Base
  has_many :email_addresses, :inverse_of => :user, :dependent => :destroy
  attr_accessible :primary_email_address

  validates_presence_of :primary_email_address

  def primary_email_address
    if association(:email_addresses).loaded? 
      email_addresses.detect(&:primary?)
    else
      email_addresses.primary.first
    end
  end

  def primary_email_address=(email)
    email.primary = true
    email_addresses << email
  end

  def verify_primary_email(email)
    if email.primary? && email_addresses.primary.count > 1
      raise "Only one primary email can exist for a user"
    elsif !email.primary? && !email_addresses.primary.exists?
      raise "A user must have one primary email"
    end
  end

  def ensure_primary_email
    return if email_addresses.primary.exists?
    raise 'Missing primary email' if !email_addresses.exists?
    email_addresses.first.update_attribute(:primary, true)
  end
end
0

, , . , .

, "" . , , , . , , , , .

, , , , . , , , .

, , , , , , .

, , , ActiveRecord, , , . , , : user.email_addresses(true) .

0

All Articles