2017-07-14 85 views
0

如果您在创建记录时保存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和角色的最佳方式是独特的基于关联任意条件?

回答

1

只做一个多字段验证。喜欢的东西:

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:参数是可选的,但可以很方便的情况下,字段名的串联变得很长(并抛出一个错误)。

+0

谢谢。但是这对验证错误很有用,但是,问题实际上是如何在验证之前进行消毒,而不是简单地将错误消除。如果有人放置相同的角色两次,我*可能会抛出一个错误并让他们修复它,或者我可以提供良好的用户体验并为其重新进行删除。 –

+0

如果你做了'valid?'之类的事情,你就不会抛出错误,并且可以根据错误的性质向用户提供具体的反馈。这是一个非常标准的SOP。你可以在尝试'valid''或'save'或'create'之前自己做一些手动检查,但是你只是在复制ActiveRecord的功能,我不知道你为什么要这么做。 – jvillian

1

要做到这一点,最好的方法是使用关系数据库,特别是在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) 

你仍然必须处理潜在的异常,当您尝试坚持角色另外,但是。

+0

当然,在数据库级别有最终决定权,但如果发生这种情况,应用程序会抛出异常。我仍然希望在发送到数据库之前进行消毒,并避免常见情况下的异常流程。 –

+0

你的意思是像https://stackoverflow.com/a/8034383?无论如何,它需要对另一个表进行额外的查询,而在1%的情况下,您仍然必须处理异常。为什么不编写自己的更新方法来捕获这个特殊的异常情况并进行适当的处​​理,从而覆盖100%的情况? – Kache

+0

验证在db级别没有索引时不是100%有效。 IMO最佳答案。 – bkunzi01

相关问题