我的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 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">]
答案 0 :(得分:23)
只要附加的角色是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。
答案 1 :(得分:9)
|=
加入方法。您可以使用Array的|=
连接方法向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">]
在this StackOverflow answer上找到了这项技术。
答案 2 :(得分:3)
您可以结合使用validates_uniqueness_of和覆盖&lt;&lt;在主模型中,虽然这也会捕获连接模型中的任何其他验证错误。
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
答案 3 :(得分:2)
我认为适当的验证规则在您的users_roles连接模型中:
validates_uniqueness_of :user_id, :scope => [:role_id]
答案 4 :(得分:0)
也许可以创建验证规则
validates_uniqueness_of :user_roles
然后捕获验证异常并继续优雅地进行。然而,如果可能的话,这种感觉真的很糟糕并且非常不优雅。
答案 5 :(得分: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
答案 6 :(得分:0)
我今天遇到了这个问题并最终使用了#replace,其中&#34;将执行差异并删除/仅添加已更改的记录&#34;。
因此,您需要传递现有角色的联合(因此它们不会被删除)和您的新角色:
new_roles = [role]
user.roles.replace(user.roles | new_roles)
重要的是要注意,这个答案和接受的答案都将关联的roles
对象加载到内存中,以便执行Array diff(-
)和union({{ 1}})。如果您正在处理大量相关记录,这可能会导致性能问题。
如果这是一个问题,您可能需要先查看通过查询检查是否存在的选项,或使用|
(mysql)类型查询进行插入。
答案 7 :(得分:0)
即使多次调用Refer rails guide,这也只会在数据库中创建一个关联。
user.roles=[Role.first]