在连接表中,Rails缺少复合键的最佳解决方法是什么?

create_table :categories_posts, :id => false do |t| t.column :category_id, :integer, :null => false t.column :post_id, :integer, :null => false end 

我有一个连接表(如上所述),其中的列引用了相应的类别表和posts表。 我想在categories_posts连接表中对复合键category_id,post_id强制执行唯一约束。 但是Rails不支持这个(我相信)。

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

我的假设是:

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

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

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

我认为您可以更容易地validation其中一个字段的唯一性,另一个作为范围:

来自API:

 validates_uniqueness_of(*attr_names) 

validation指定属性的值在整个系统中是否唯一。 用于确保只有一个用户可以命名为“davidhh”。

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

它还可以根据多个范围参数validation指定属性的值是否唯一。 例如,确保教师每学期只能按照特定class级的时间表进行一次。

  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 (eg :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. 

推荐“正确”的方法非常困难

1) 务实的方法

使用validation器,不要添加唯一的复合索引。 这为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一书), validation不是万无一失的,因为SELECT和INSERT / UPDATE查询之间存在潜在的竞争条件 。 如果您必须绝对确定没有重复记录,建议使用唯一约束。

2) 防弹方法

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

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

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

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

3) 两者的结合

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

当我在rails中遇到此问题时,我实现了以下两个方面:

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

2)为了提供比上述更平滑的错误消息,请在Rails模型中添加validation:

 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 

解决方案可以是在模型中添加索引和validation。

所以在迁移中你有: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]