确保has_many:通过关联在创建时是唯一的
问题描述:
如果您在创建记录时保存has_many:through关联,如何确保关联具有唯一对象。唯一性由一组自定义属性定义。确保has_many:通过关联在创建时是唯一的
考虑:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
什么是验证user_roles和角色的最佳方式是独特的基于关联任意条件?
答
只做一个多字段验证。喜欢的东西:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
喜欢的东西,这将确保用户只能有一个给定项目的作用 - 如果这就是你要找的内容。
正如Kache提到的那样,您可能还需要想要做一个db级索引。整个迁移可能看起来像:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
的name:
参数是可选的,但可以很方便的情况下,字段名的串联变得很长(并抛出一个错误)。
答
要做到这一点,最好的方法是使用关系数据库,特别是在user_roles
上创建一个唯一的多列索引。
add_index :user_roles, [:user_id, :role_id], unique: true
然后摆好的时候加入的角色失败处理:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
关系的DB设计,以保证引用完整性,所以用它正是这么做的。任何ruby/rails-only解决方案都会有竞争条件和/或效率低下。
如果要提供用户友好的消息,并检查“以防万一”,干脆去检查:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
你仍然必须处理潜在的异常,当您尝试坚持角色另外,但是。
谢谢。但是这对验证错误很有用,但是,问题实际上是如何在验证之前进行消毒,而不是简单地将错误消除。如果有人放置相同的角色两次,我*可能会抛出一个错误并让他们修复它,或者我可以提供良好的用户体验并为其重新进行删除。 –
如果你做了'valid?'之类的事情,你就不会抛出错误,并且可以根据错误的性质向用户提供具体的反馈。这是一个非常标准的SOP。你可以在尝试'valid''或'save'或'create'之前自己做一些手动检查,但是你只是在复制ActiveRecord的功能,我不知道你为什么要这么做。 – jvillian