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的查询进行插入。