Rails 成语避免在 has_many 中重复:通过
Posted
技术标签:
【中文标题】Rails 成语避免在 has_many 中重复:通过【英文标题】:Rails idiom to avoid duplicates in has_many :through 【发布时间】:2010-11-21 20:31:04 【问题描述】:我的 Rails 应用程序中的用户和角色之间存在标准的多对多关系:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, :through => :user_roles
end
我想确保一个用户只能被分配一次任何角色。任何插入重复项的尝试都应忽略该请求,而不是引发错误或导致验证失败。我真正想要表示的是一个“集合”,其中插入集合中已经存在的元素没有任何效果。 1,2,3 U 1 = 1,2,3,而不是 1,1,2,3。
我意识到我可以这样做:
user.roles << role unless user.roles.include?(role)
或者通过创建一个包装方法(例如add_to_roles(role)
),但我希望有一些惯用的方式通过关联使其自动化,以便我可以编写:
user.roles << role # automatically checks roles.include?
它只是为我工作。这样,我就不必记住检查重复或使用自定义方法。我缺少的框架中有什么东西吗?我首先认为 has_many 的 :uniq 选项可以做到这一点,但它基本上只是“选择不同的”。
有没有办法以声明的方式做到这一点?如果没有,也许通过使用关联扩展?
以下是默认行为如何失败的示例:
>> u = User.create 用户创建 (0.6ms) INSERT INTO "users" ("name") VALUES(NULL) => # >> u.roles 角色加载 (0.5ms) SELECT * FROM "roles" LIMIT 1 UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) 角色加载 (0.4ms) SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) => [#] >> u.roles 角色加载 (0.4ms) SELECT * FROM "roles" LIMIT 1 UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) => [#, # ]
【问题讨论】:
【参考方案1】:也许可以创建验证规则
validates_uniqueness_of :user_roles
然后捕获验证异常并优雅地进行。然而,如果可能的话,这感觉真的很hacky并且非常不雅。
【讨论】:
【参考方案2】:只要附加的角色是 ActiveRecord 对象,你在做什么:
user.roles << role
应该为:has_many
关联自动重复数据删除。
对于has_many :through
,请尝试:
class User
has_many :roles, :through => :user_roles do
def <<(new_item)
super( Array(new_item) - proxy_association.owner.roles )
end
end
end
如果 super 不起作用,你可能需要设置一个 alias_method_chain。
【讨论】:
这样不行。我将更新帖子以包含测试。 为了后人,上述方法可以简写为:def 对于 Rails 3.1,s/proxy_owner/proxy_association.owner/
related Q
当<<
只接受一个对象时,为什么要使用*items
参数? ruby-doc.org/core-1.9.3/Array.html#method-i-3C-3C
我很奇怪 has_many 而不是 has_many :through
。然而,解决此问题的 Rails 问题 (github.com/rails/rails/issues/8573) 被拒绝,原因是“这是您的域逻辑,因此您有责任检查它。”【参考方案3】:
我认为正确的验证规则在您的 users_roles 加入模型中:
validates_uniqueness_of :user_id, :scope => [:role_id]
【讨论】:
谢谢。这实际上并没有做我想要的(这是一种类似集合的行为),我已经澄清了原始帖子中的内容。对不起。 我认为这是解决您问题的最佳答案。如果您在创建界面时小心翼翼,那么用户无论如何都必须破解它以添加错误的角色,在这种情况下,验证异常是完全合适的响应。 嘿,你疯了吗?用户不添加自己的角色 :-) 典型的用例是用户成为角色的成员,这是其他事物的副作用。例如,购买特定产品。其他产品也可能提供相同的作用,因此存在重复的机会。我宁愿在一个地方进行重复检查,而不是在任何需要确保用户具有角色的随机地方。从这个意义上说,给用户一个他已经拥有的角色并不是一个错误条件。【参考方案4】:您可以在主模型中使用 validates_uniqueness_of 和覆盖
validates_uniqueness_of :user_id, :scope => [:role_id]
class User
has_many :roles, :through => :user_roles do
def <<(*items)
super(items) rescue ActiveRecord::RecordInvalid
end
end
end
【讨论】:
你不能把那个异常改成ActiveRecord::RecordNotUnique
吗?我喜欢这个答案。请注意race conditions。
不错的答案。我在没有validates_uniqueness_of
的情况下使用它,在数据库中声明了唯一索引并且工作得很好。【参考方案5】:
我认为你想做这样的事情:
user.roles.find_or_create_by(role_id: role.id) # saves association to database
user.roles.find_or_initialize_by(role_id: role.id) # builds association to be saved later
【讨论】:
【参考方案6】:我今天遇到了这个问题,最终使用了#replace,它“将执行差异并仅删除/添加已更改的记录”。
因此,您需要传递现有角色的联合(这样它们就不会被删除)和您的新角色:
new_roles = [role]
user.roles.replace(user.roles | new_roles)
重要的是要注意,这个答案和接受的答案都将关联的roles
对象加载到内存中,以执行 Array diff (-
) 和 union (|
)。如果您要处理大量相关记录,这可能会导致性能问题。
如果这是一个问题,您可能需要先查看通过查询检查是否存在的选项,或者使用INSERT ON DUPLICATE KEY UPDATE
(mysql) 类型的查询进行插入。
【讨论】:
【参考方案7】:使用数组的|=
Join 方法。
您可以使用 Array 的 |=
连接方法将元素添加到 Array,除非它已经存在。只需确保将元素包装在一个数组中即可。
role #=> #<Role id: 1, name: "1">
user.roles #=> []
user.roles |= [role] #=> [#<Role id: 1, name: "1">]
user.roles |= [role] #=> [#<Role id: 1, name: "1">]
也可用于添加可能已经存在或不存在的多个元素:
role1 #=> #<Role id: 1, name: "1">
role2 #=> #<Role id: 2, name: "2">
user.roles #=> [#<Role id: 1, name: "1">]
user.roles |= [role1, role2] #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]
user.roles |= [role1, role2] #=> [#<Role id: 1, name: "1">, #<Role id: 2, name: "2">]
在this *** answer 上发现了这种技术。
【讨论】:
我认为正确和最干净的答案【参考方案8】:即使多次调用Refer rails guide,这也只会在数据库中创建一个关联。
user.roles=[Role.first]
【讨论】:
以上是关于Rails 成语避免在 has_many 中重复:通过的主要内容,如果未能解决你的问题,请参考以下文章
如何避免 has_many :through 关系中的重复?
在 Rails 中,如何在一个查找器中查询两个 has_many 关联?
何时在 Rails 中使用“has_many :through”关系?