5

我有一个User模型。一个用户有很多EmailAddresses,他们选择其中一个作为他们的primary_email_address,这是我发送电子邮件的那个。用户必须始终拥有至少一个电子邮件地址,并且必须设置一个主电子邮件地址。主电子邮件地址可能会被销毁,但必须为用户分配一个新的主电子邮件地址。

事实证明,这是一个非常难以处理的情况,而且我尝试过的每个解决方案都有一些不令人满意的元素。这似乎是一类非常常见的问题(A 有很多 B,其中一个 B 很特殊)所以我很想知道如何干净利落地解决它。

解决方案 1 - EmailAddress 上的布尔列说明它是否是主要的

就像是:

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

这在概念上很尴尬,因为用户只有一个主电子邮件地址非常重要,并且必须跨多个电子邮件地址记录验证这一点似乎很糟糕。虽然我知道 ActiveRecord 事务应该意味着用户不会因为没有主电子邮件地址而陷入困境,但这似乎是灾难的根源。主电子邮件地址本质上是属于用户的东西,将这个逻辑放到 EmailAddress 模型中是不理想的。

解决方案 2 -除了EmailAddress 上的列primary_email_adress_id之外,用户上的列user_id

就像是:

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_addressUser 模型中略显尴尬的行引起的。has_many :email_addresses/belongs_to :userEmailAddresses 和 Users 被这两个不同的 ActiveRecord 关系(和)关联起来有点奇怪belongs_to :primary_email_address

两种在技术上都有效的解决方案(我们目前正在使用第二种解决方案),但都存在不直观且耗时的缺陷。我很想听听一些关于如何正确解决这个问题的好主意。谢谢。

4

2 回答 2

0

一般来说,最好让你们的关系只向一个方向流动。反向链接创建了难以解决的循环依赖关系。

在第一个示例中,您在其中一个电子邮件地址上有一个标志,表明它是“主要”地址。如果一个地址没有被明确标记为主要地址,那么您可以通过默认第一个地址来使其更加健壮,并且如果有多个主要地址,只需选择其中的第一个。没有什么事情是完美的,所以让你的应用程序能够处理一些不确定性而不是抛出异常并不一定是一件坏事。

请记住,在指定主要地址之前无法保存您的用户记录,并且您不太可能在最初保存电子邮件地址之前将其与用户地址相关联。这是很难解决的循环依赖关系之一,除非您小心地以正确的顺序保存内容。

如果您将主地址的概念保持松散,除非明确定义您有一个合理的默认值,否则您必须做的工作量要少得多。

您发现的一件事是关系通常由 ActiveRecord 缓存,因此拥有两个具有重叠数据的独立关系可能会很麻烦。没有真正的方法可以避免这种情况,但是如果您担心过时的缓存数据,您始终可以强制重新加载关系:user.email_addresses(true)将始终从数据库中获取记录。

于 2013-04-25T16:30:29.197 回答
0

我建议使用第一种方法,尽管略有不同。实际上没有任何方法可以避免在电子邮件地址上设置触发回调,因为这是进行修改的地方。但是,您不需要像以前那样复杂。

由于需要对用户的整个 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
于 2013-04-25T16:38:16.260 回答