令人惊讶的复杂的 ActiveRecord 关系

问题描述 投票:0回答:3

我有一个

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 - 除了电子邮件地址
上的 
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)
=> ["[email protected]", "[email protected]"]
> u.primary_email_address.destroy
=> true
> u.email_addresses.map(&:email)
=> ["[email protected]", "[email protected]"]
> u.reload
> u.email_addresses.map(&:email)
=> ["[email protected]"]

这会在

after_destroy
钩子和其他情况下导致很多问题。这似乎是由用户模型中稍微尴尬的
belongs_to :primary_email_address
线引起的。通过这两种不同的 ActiveRecord 关系(
has_many :email_addresses/belongs_to :user
belongs_to :primary_email_address
)来关联 EmailAddresses 和 Users 有点奇怪。

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

ruby-on-rails ruby ruby-on-rails-3 activerecord
3个回答
0
投票

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

由于需要对用户的整个电子邮件地址集合进行检查,因此应使用数据库内容来确定状态是否有效 - 使用

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
投票

您可以使用此 gem 来满足此要求https://github.com/mechanicles/set_as_primary您也可以关注此博客文章https://blog.mechanicles.com/2019/12/26/primary-或-default-flag-to-the-rails-models.html 了解更多信息。如果你们发现任何问题请告诉我。

© www.soinside.com 2019 - 2024. All rights reserved.