【问题标题】:A surprisingly complex ActiveRecord relationship令人惊讶的复杂 ActiveRecord 关系
【发布时间】:2013-04-25 16:17:21
【问题描述】:

我有一个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 - 除了电子邮件地址上的 user_id 列之外,用户上的 primary_email_adress_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 模型的耦合更紧密。然而,现在围绕逆存在令人讨厌的问题。 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钩子和其他情况下会导致很多问题。这似乎是由 User 模型中略显尴尬的 belongs_to :primary_email_address 行引起的。通过这两种不同的 ActiveRecord 关系(has_many :email_addresses/belongs_to :userbelongs_to :primary_email_address)将 EmailAddresses 和 Users 关联起来有点奇怪。

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

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-3 activerecord


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

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

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

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

      如果您将主地址的概念保持松散,除非明确定义,否则您有一个合理的默认值,那么您要做的工作量就会大大减少。

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

      【讨论】:

        猜你喜欢
        • 2021-09-28
        • 2019-10-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-16
        • 1970-01-01
        • 2018-01-09
        相关资源
        最近更新 更多