Rails idiom to avoid duplicates in has_many :through

狂风中的少年 提交于 2019-11-28 03:12:18

As long as the appended role is an ActiveRecord object, what you are doing:

user.roles << role

Should de-duplicate automatically for :has_many associations.

For has_many :through, try:

class User
  has_many :roles, :through => :user_roles do
    def <<(new_item)
      super( Array(new_item) - proxy_association.owner.roles )
    end
  end
end

if super doesn't work, you may need to set up an alias_method_chain.

Use Array's |= Join Method.

You can use Array's |= join method to add an element to the Array, unless it is already present. Just make sure you wrap the element in an 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">]

Can also be used for adding multiple elements that may or may not already be present:

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">]

Found this technique on this StackOverflow answer.

You can use a combination of validates_uniqueness_of and overriding << in the main model, though this will also catch any other validation errors in the join model.

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

i think the proper validation rule is in your users_roles join model:

validates_uniqueness_of :user_id, :scope => [:role_id]

Perhaps it is possible to create the validation rule

validates_uniqueness_of :user_roles

then catch the validation exception and carry on gracefully. However, this feels really hacky and is very inelegant, if even possible.

I think you want to do something like:

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

I ran into this today and ended up using #replace, which "will perform a diff and delete/add only records that have changed".

Therefore, you need to pass the union of the existing roles (so they don't get deleted) and your new role(s):

new_roles = [role]
user.roles.replace(user.roles | new_roles)

It's important to note that both this answer and the accepted one are loading the associated roles objects into memory in order to perform the Array diff (-) and union (|). This could lead to performance issues if you're dealing with a large number of associated records.

If that's a concern, you may want to look into options that check for existence via queries first, or use an INSERT ON DUPLICATE KEY UPDATE (mysql) type query for inserting.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!