Rails习语避免has_many:through中的重复
在我的Rails应用程序中,我有一个标准的用户和angular色之间的多对多关系:
class User < ActiveRecord::Base has_many :user_roles has_many :roles, :through => :user_roles end
我想确保用户只能分配一次任何angular色。 任何尝试插入重复应该忽略请求,不会抛出错误或导致validation失败。 我真正想要表示的是一个“集合”,插入已经存在于集合中的元素不起作用。 {1,2,3} U {1} = {1,2,3},而不是{1,1,2,3}。
我意识到我可以这样做:
user.roles << role unless user.roles.include?(role)
或者通过创build一个包装方法(例如add_to_roles(role)
),但是我希望有一些惯用的方法使它通过关联自动化,这样我可以写:
user.roles << role # automatically checks roles.include?
它只是为我做的工作。 这样,我不必记得检查dups或使用自定义的方法。 在我失踪的框架中有什么东西吗? 我首先想到:has_many的uniq选项可以实现,但基本上只是“select不同”。
有没有办法做到这一点声明? 如果没有,也许通过使用关联扩展?
以下是默认行为失败的一个例子:
>> u = User.create 用户创build(0.6ms)INSERT INTO“users”(“name”)VALUES(NULL) =>#<用户标识:3,名称:nil> >> u.roles << Role.first angular色负载(0.5ms)SELECT * FROM“roles”LIMIT 1 UserRole Create(0.5ms)INSERT INTO“user_roles”(“role_id”,“user_id”)VALUES(1,3) angular色负载(0.4ms)SELECT“roles”* FROM“roles”INNER JOIN“user_roles”ON“roles”.id =“user_roles”.role_id WHERE((“user_roles”.user_id = 3) => [#<angular色ID:1,名称:“1”>] >> u.roles << Role.first angular色负载(0.4ms)SELECT * FROM“roles”LIMIT 1 UserRole Create(0.5ms)INSERT INTO“user_roles”(“role_id”,“user_id”)VALUES(1,3) => [#<angular色ID:1,名称:“1”>,#<angular色ID:1,名称:“1”>]
只要附加的angular色是一个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。
您可以在主模型中使用validates_uniqueness_of和覆盖<<的组合,但也可以在连接模型中捕获任何其他validation错误。
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
我认为适当的validation规则是在你的users_rolesjoin模型中:
validates_uniqueness_of :user_id, :scope => [:role_id]
也许可以创buildvalidation规则
validates_uniqueness_of :user_roles
然后捕获validationexception并进行优雅。 但是,如果可能的话,这种感觉真的很黑,而且很不雅观。
我想你想做的事情是这样的:
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
我今天遇到了这个,最后使用了#replace ,它将“执行差异并删除/添加只有已经改变的logging”。
因此,您需要传递现有angular色的联合(所以他们不会被删除)和您的新angular色:
new_roles = [role] user.roles.replace(user.roles | new_roles)
需要注意的是,这个答案和接受的答案都将相关的roles
对象加载到内存中,以便执行数组diff( -
)和union( |
)。 这可能会导致性能问题,如果你正在处理大量的相关logging。
如果这是一个问题,您可能需要查看通过查询首先检查是否存在的选项,或者使用INSERT ON DUPLICATE KEY UPDATE
(mysql)types的查询进行插入。