【问题标题】:In a join table, what's the best workaround for Rails' absence of a composite key?在连接表中,Rails 缺少组合键的最佳解决方法是什么?
【发布时间】:2010-10-27 05:30:11
【问题描述】:
create_table :categories_posts, :id => false do |t|
  t.column :category_id, :integer, :null => false
  t.column :post_id, :integer, :null => false
end

我有一个连接表(如上),其中的列引用了相应的 categories 表和 posts 表。我想对 categories_posts 联接表中的复合键 category_id、post_id 强制实施唯一约束。但是 Rails 不支持这一点(我相信)。

为了避免我的数据中存在具有相同 category_id 和 post_id 组合的重复行的可能性,Rails 中缺少复合键的最佳解决方法是什么

我的假设是:

  1. 默认自动编号列 (id:integer) 不会做任何事情 在这种情况下保护我的数据。
  2. ActiveScaffold 可以提供一个 解决方案,但我不确定是否 将它包含在我的 项目只是为了这个单曲 功能,特别是如果有 更优雅的答案。

【问题讨论】:

    标签: ruby-on-rails database composite-key junction-table


    【解决方案1】:

    添加包含两列的唯一索引。这将阻止您插入包含重复 category_id/post_id 对的记录。

    add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => 'by_category_and_post'
    

    【讨论】:

    • 谢谢。通过阅读各种博客文章,我认为复合索引是不可能的,并且这种语法不存在。
    • 如果用户尝试输入重复的记录,这将导致糟糕的 UI 体验。
    • @Larry - 我不能仍然使用您答案中的验证逻辑并将其与复合索引的 Rails 语法结合起来吗?
    • 是的,这就是我最初的回答所说的......你应该两者都做。请注意,索引是在迁移中添加的。 "add_index" 仅在迁移中调用,而不在模型中调用。
    • @Larry - 是的,我一直在寻找迁移的解决方案,这就是为什么这个答案非常有效。但我仍会将您的验证添加到我的模型中。再次感谢。
    【解决方案2】:

    很难推荐“正确”的方法。

    1) 务实的方法

    使用验证器,不要添加唯一复合索引。这会在 UI 中为您提供很好的消息,并且可以正常工作。

    class CategoryPost < ActiveRecord::Base
      belongs_to :category
      belongs_to :post
    
      validates_uniqueness_of :category_id, :scope => :post_id, :message => "can only have one post assigned"
    end
    

    您还可以在连接表中添加两个单独的索引以加快搜索速度:

    add_index :categories_posts, :category_id
    add_index :categories_posts, :post_id
    

    请注意(根据Rails 3 Way 一书),验证并非万无一失,因为 SELECT 和 INSERT/UPDATE 查询之间存在潜在的竞争条件。如果您必须绝对确定没有重复记录,建议使用唯一约束。

    2) 防弹方法

    在这种方法中,我们希望在数据库级别设置约束。所以意思是创建一个复合索引:

    add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => 'by_category_and_post'
    

    最大的优点是数据库的完整性很好,缺点是没有太多有用的错误报告给用户。请注意,在创建复合索引时,列的顺序很重要。

    如果您将选择性较少的列作为索引中的前导列,并将大多数选择性列放在最后,则其他对非前导索引列有条件的查询也可能会利用 INDEX SKIP SCAN。您可能需要再添加一个索引才能利用它们,但这高度依赖于数据库。

    3) 两者结合

    人们可以阅读两者的组合,但我倾向于只喜欢第一个。

    【讨论】:

    • 这应该被认为是最好的abswer,因为它提出了模型和数据完整性。
    • 我不同意只使用第一名的建议,但正如提到的所有优点和缺点,这仍然值得一票。
    【解决方案3】:

    我认为您可以更轻松地验证其中一个字段的唯一性,并将另一个字段作为范围:

    来自 API:

    validates_uniqueness_of(*attr_names)
    

    验证指定属性的值在系统中是否唯一。有助于确保只能将一个用户命名为“davidhh”。

      class Person < ActiveRecord::Base
        validates_uniqueness_of :user_name, :scope => :account_id
      end
    

    还可以根据多个作用域参数验证指定属性的值是否唯一。例如,确保某位教师每学期只能在特定课程的时间表上上课一次。

      class TeacherSchedule < ActiveRecord::Base
        validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
      end
    

    创建记录时,将执行检查以确保数据库中不存在具有指定属性(映射到列)的给定值的记录。更新记录时,会进行相同的检查,但不考虑记录本身。

    配置选项:

    * message - Specifies a custom error message (default is: "has already been taken")
    * scope - One or more columns by which to limit the scope of the uniquness constraint.
    * case_sensitive - Looks for an exact match. Ignored by non-text columns (true by default).
    * allow_nil - If set to true, skips this validation if the attribute is null (default is: false)
    * if - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The method, proc or string should return or evaluate to a true or false value.
    

    【讨论】:

    • 正如@izap 在他的回答中所说:验证并非万无一失,因为 SELECT 和 INSERT/UPDATE 查询之间存在潜在的竞争条件。
    【解决方案4】:

    当我在 Rails 中遇到此问题时,我实现了以下两项:

    1) 您应该在数据库级别声明一个唯一的复合索引,以确保 dbms 不会让创建重复记录。

    2) 为了提供比上述更流畅的错误消息,向 Rails 模型添加验证:

    validates_each :category_id, :on => :create do |record, attr, value|
      c = value; p = record.post_id
      if c && p && # If no values, then that problem 
                   # will be caught by another validator
        CategoryPost.find_by_category_id_and_post_id(c, p)
        record.errors.add :base, 'This post already has this category'
      end
    end
    

    【讨论】:

    • 根据 tvanoffson 的回答,复合索引确实是可能的,他已经提供了它的语法。在这种情况下,您在数据库级别声明复合索引的建议(虽然是一个很好的建议)将是不必要的。我的假设是复合索引在 Rails 中是不可能的,但也许我错了。
    • Tvanfosson 的解决方案与我的 #1 相同——索引是在 db 级别声明的,而不是在 Rails 或 Ruby 级别。 Rails 只能在提交 dup 时报告“SQL 错误”。这就是为什么您还想在模型中进行验证(在 Rails 级别)。
    • 当您说“在数据库级别”时,我以为您的意思是在数据库中创建它而不使用 Rails 中的 add_index 方法。我提出问题的原因是我不知道复合索引是可能的。但我不明白为什么我不能将您的验证逻辑添加到 tvanoffson 的答案中的语法中。对不起,如果那是你的意图。但我从你的回答中没有明白这一点。
    【解决方案5】:

    解决方案可以是在模型中同时添加索引和验证。

    所以在迁移中你有: add_index :categories_posts, [:category_id, :post_id], :unique => true

    在模型中: validates_uniqueness_of :category_id, :scope => [:category_id, :post_id] validates_uniqueness_of :post_id, :scope => [:category_id, :post_id]

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-09
      • 2017-06-26
      • 1970-01-01
      • 1970-01-01
      • 2023-03-30
      • 2010-11-08
      • 2010-09-11
      • 1970-01-01
      相关资源
      最近更新 更多