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 可能会提供一个解决方案,但我不确定仅仅为了这个单一功能将它包含在我的项目中是否有点矫枉过正,特别是如果有更优雅的答案。
4

5 回答 5

41

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

add_index :categories_posts, [ :category_id, :post_id ], :unique => true, :name => 'by_category_and_post'
于 2009-05-19T04:59:23.800 回答
18

很难推荐“正确”的方法

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

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

于 2011-08-16T08:41:54.500 回答
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.
于 2009-05-19T06:55:21.450 回答
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
于 2009-05-19T05:00:39.180 回答
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]

于 2010-08-27T09:23:44.863 回答