【问题标题】:Make sure has_many :through association is unique on creation确保 has_many :through 关联在创建时是唯一的
【发布时间】:2017-07-14 21:58:00
【问题描述】:

如果您在记录创建时保存 has_many :through 关联,您如何确保关联具有唯一对象。唯一性由一组自定义属性定义。

考虑:

 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 和角色是否唯一的最佳方法是什么?

【问题讨论】:

    标签: ruby-on-rails activerecord associations


    【解决方案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
    

    关系数据库旨在保证引用完整性,因此请使用它。任何仅 ruby​​/rails 的解决方案都会有竞争条件和/或效率非常低。

    如果您想提供用户友好的消息并选中“以防万一”,请继续检查:

    already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
    

    不过,当您尝试持续添加角色时,您仍然需要处理潜在的异常。

    【讨论】:

    • 当然,在数据库级别有最终决定权总是好的,但是如果发生这种情况,应用程序会抛出异常。我仍然想在发送到数据库之前进行清理,并避免常见情况下的异常流。
    • 你的意思是像stackoverflow.com/a/8034383?无论如何,它需要对另一个表进行额外的查询,在 1% 的情况下,您仍然必须处理异常。为什么不编写自己的更新方法来捕获这个特殊的异常情况并优雅地处理它,从而覆盖 100% 的情况?
    • 如果没有数据库级别的索引,验证不会 100% 有效。 IMO 最佳答案。
    • @Kache - 将 uniq: true 放在关联上并不能解决分配问题,只能放在查询上。覆盖更新是可能的,但想象一下必须如何实现捕获异常。您必须专门捕获唯一索引 AR 异常,然后仍确定该异常是否适用于这种特定情况,然后仍然进行重复数据删除并分配给关联。拥有执行此操作的 before_validation 要简单得多。但问题仍然存在:如何对关联进行重复数据删除?
    【解决方案2】:

    只需进行多字段验证。类似的东西:

    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
    

    这样可以确保用户在给定项目中只能拥有一个角色 - 如果这正是您要寻找的。​​p>

    正如 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: 参数是可选的,但在字段名称的连接过长(并引发错误)的情况下会很方便。

    【讨论】:

    • 谢谢。但这对验证错误有好处,但是,问题实际上是关于如何在验证之前进行清理,而不仅仅是错误输出。如果有人两次放置相同的角色,我可以抛出错误并让他们修复它,或者我可以提供良好的用户体验并为他们删除重复数据。
    • 如果您执行valid? 之类的操作,您将不会抛出错误,并且可以根据错误的性质向您的用户提供具体的反馈。这是一个非常标准的SOP。您可以在尝试valid?savecreate 或其他任何东西之前自己进行一些手动检查,但是您只是在复制ActiveRecord 功能,我不确定您为什么要这样做.
    猜你喜欢
    • 2014-06-03
    • 1970-01-01
    • 1970-01-01
    • 2015-12-17
    • 2012-09-18
    • 2014-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多