41

我的 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)
    => #<用户id:3,名称:nil>
    >> u.roles << Role.first
      角色加载 (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))
    => [#<角色ID:1,名称:“1”>]
    >> u.roles << Role.first
      角色加载 (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">]
4

8 回答 8

28

只要附加的角色是一个 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。

于 2009-08-22T06:33:39.610 回答
23

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

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

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

在这个 StackOverflow 答案上找到了这种技术。

于 2018-03-11T16:36:01.983 回答
3

您可以在主模型中使用 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
于 2011-03-16T19:01:15.897 回答
2

我认为正确的验证规则在您的 users_roles 加入模型中:

validates_uniqueness_of :user_id, :scope => [:role_id]
于 2009-08-22T06:49:13.650 回答
0

也许可以创建验证规则

validates_uniqueness_of :user_roles

然后捕获验证异常并优雅地进行。但是,如果可能的话,这感觉真的很hacky并且非常不雅。

于 2009-08-22T05:41:55.847 回答
0

我想你想做这样的事情:

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
于 2017-04-05T23:23:19.410 回答
0

我今天遇到了这个问题并最终使用了#replace,它“将执行差异并仅删除/添加已更改的记录”。

因此,您需要通过现有角色的联合(因此它们不会被删除)和您的新角色:

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

重要的是要注意,这个答案和接受的答案都将关联的roles对象加载到内存中,以便执行 Array diff ( -) 和 union ( |)。如果您要处理大量关联记录,这可能会导致性能问题。

如果这是一个问题,您可能需要先查看通过查询检查是否存在的选项,或者使用INSERT ON DUPLICATE KEY UPDATE(mysql) 类型的查询进行插入。

于 2017-04-25T18:16:27.073 回答
0

即使多次调用,这也只会在数据库中创建一个关联Refer rails guide

user.roles=[Role.first] 
于 2019-09-25T11:38:23.167 回答