Rails习惯用来避免has_many中的重复:through

Kin*_*ong 36 activerecord idioms ruby-on-rails associations

我的Rails应用程序中的用户和角色之间存在标准的多对多关系:

class User < ActiveRecord::Base
  has_many :user_roles
  has_many :roles, :through => :user_roles
end
Run Code Online (Sandbox Code Playgroud)

我想确保只为用户分配一次角色.任何插入重复项的尝试都应该忽略该请求,而不是抛出错误或导致验证失败.我真正想要表示的是"集合",其中插入已存在于集合中的元素无效.{1,2,3} U {1} = {1,2,3},而非{1,1,2,3}.

我意识到我可以这样做:

user.roles << role unless user.roles.include?(role)
Run Code Online (Sandbox Code Playgroud)

或者通过创建一个包装器方法(例如add_to_roles(role)),但我希望通过关联使其自动化的一些惯用方法,以便我可以写:

user.roles << role  # automatically checks roles.include?
Run Code Online (Sandbox Code Playgroud)

它只是为我做的工作.这样,我不必记得检查重复或使用自定义方法.我缺少框架中的某些东西吗?我首先想到:has_many的uniq选项可以做到,但它基本上只是"选择不同的".

有没有办法以声明方式做到这一点?如果没有,可能使用关联扩展名?

以下是默认行为失败的示例:

    >> u = User.create
      User Create (0.6ms)   INSERT INTO "users" ("name") VALUES(NULL)
    => #<User id: 3, name: nil>
    >> u.roles << Role.first
      Role Load (0.5ms)   SELECT * FROM "roles" LIMIT 1
      UserRole Create (0.5ms)   INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3)
      Role Load (0.4ms)   SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) 
    => [#<Role id: 1, name: "1">]
    >> u.roles << Role.first
      Role Load (0.4ms)   SELECT * FROM "roles" LIMIT 1
      UserRole Create (0.5ms)   INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3)
    => [#<Role id: 1, name: "1">, #<Role id: 1, name: "1">]

aus*_*ton 24

只要附加的角色是ActiveRecord对象,您正在做的事情:

user.roles << role
Run Code Online (Sandbox Code Playgroud)

应自动为: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
Run Code Online (Sandbox Code Playgroud)

如果超级不起作用,您可能需要设置alias_method_chain.

  • 对于后代,上述方法可以缩短并通用化为:def <<(*items)super(items - proxy_target)end (3认同)
  • &lt;&lt; 对于 `has_many` 而不是 `has_many :through` 进行重复数据删除对我来说很奇怪。但是,解决此问题的 Rails 问题 (https://github.com/rails/rails/issues/8573) 被拒绝,原因是“这是您的域逻辑,因此您有责任检查它”。 (2认同)

Jos*_*ter 11

使用Array的|=Join方法.

您可以使用Array的|=join方法向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">]
Run Code Online (Sandbox Code Playgroud)

也可用于添加可能已存在或可能不存在的多个元素:

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">]
Run Code Online (Sandbox Code Playgroud)

StackOverflow的答案中找到了这种技术.

  • 我认为正确且最干净的答案 (2认同)