一个令人惊讶的复杂的ActiveRecord关系

时间:2013-04-25 16:17:21

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

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

事实证明这是一个令人惊讶的难以处理的问题,我尝试过的每一个解决方案都有一些令人不满意的因素。这似乎是一个非常普遍的问题(A有很多B,其中一个B是特殊的)所以我很想知道如何彻底解决它。

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

类似的东西:

primary_email_adress_id

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

除了EmailAddress上的user_id列之外,

解决方案2 - 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 列上的用户

类似的东西:

user.primary_email_address

这样做更好,因为具有正好1个主电子邮件地址的用户的验证更容易且与用户模型紧密耦合。然而,逆转之间现在存在恼人的问题。 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模型中稍微笨拙的has_many :email_addresses/belongs_to :user行的原因。 EmailAddresses和用户通过这两种不同的ActiveRecord关系(belongs_to :primary_email_address和{{1}})进行关联有点奇怪。

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

2 个答案:

答案 0 :(得分:0)

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

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

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

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

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

答案 1 :(得分:0)

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

由于需要对用户的整个EmailAddresses集合进行检查,因此应使用数据库内容来确定状态是否有效 - 使用after_save和{{1}这是一个简单的方法:

after_destroy