在一个关联表中,Rails缺乏复合键,有什么最佳解决方法?

32
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联接表中对category_id, post_id这个组合键强制执行唯一约束条件,但是Rails不支持这样做(我认为)。
为了避免我的数据中出现相同的category_id和post_id组合导致重复行,在Rails中没有组合键的情况下,最好的解决方法是什么
我的假设是:
  1. 默认的自动编号列(id:integer)在这种情况下对保护我的数据没有任何作用。
  2. ActiveScaffold可能提供了一种解决方案,但我不确定是否为了这个单一特性而将其包含在我的项目中是否过度,特别是如果有更优雅的答案。
5个回答

41

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

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

谢谢。从阅读各种博客文章来看,我认为复合索引是不可能的,这种语法也不存在。 - pez_dispenser
如果用户尝试输入重复的记录,这将导致糟糕的用户界面体验。 - Larry K
1
@Larry - 我能否仍然使用您答案中的验证逻辑,并将其与此Rails语法结合使用以进行复合索引? - pez_dispenser
2
是的,这就是我的原始答案所说的...你应该两者都做。请注意,索引是在迁移中添加的。“add_index”仅在迁移中调用,而不是在模型中。 - Larry K
@Larry - 是的,我一直在寻找迁移方案,所以这个答案非常完美。但我仍会将您的验证添加到我的模型中。再次感谢。 - pez_dispenser
只想补充一下,当我添加重复项时,我遇到了约束条件无法触发的问题,因为我忘记了 name。请确保您添加它! - tf.rz

19

推荐"正确"的方法非常困难。

1) 实用主义方法

使用验证器并且不添加唯一组合索引。这样可以在用户界面中获得良好的消息,而且它只需要工作就行了。

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)两者的组合

您可以阅读有关两者结合的文章,但我倾向于只喜欢第一种方法。


这应该被视为最佳答案,因为它提出了模型和数据完整性。 - Paulo Fidalgo
1
我不同意只使用第一种建议,但既然所有的利弊都已经提到了,这仍然值得点赞。 - kronn

10

我认为你可以通过将另一个字段作为作用域来更容易地验证其中一个字段的唯一性:

来自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查询之间存在潜在的竞争条件,因此验证并不是绝对可靠的。 - Redoman

6

当我在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中不可能存在复合索引,但也许我是错误的。 - pez_dispenser
2
Tvanfosson的解决方案与我的#1相同 - 索引是在数据库级别而不是Rails或Ruby级别声明的。当提交重复项时,Rails只能报告“SQL错误”。这就是为什么您还想在模型中(在Rails级别)进行验证的原因。 - Larry K
当你说“在数据库层面上”时,我以为你是指在Rails中不使用add_index方法在数据库中创建它。我提出问题的原因是我不知道复合索引是可能的。但我不明白为什么我不能将你的验证逻辑添加到tvanoffson答案的语法中。如果这是你的意图,那我很抱歉。但我没有从你的回答中理解到这一点。 - pez_dispenser

1
一个解决方案是在模型中同时添加索引和验证。
因此,在迁移中你需要: 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]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接