如何确定我的ActiveRecord对象是否违反了唯一的数据库密钥/索引?

时间:2009-04-07 07:07:31

标签: ruby-on-rails database exception activerecord indexing

ActiveRecord的validates_uniqueness_ofvulnerable to race conditions。要真正确保唯一性,需要额外的保护措施。 ActiveRecord RDocs的一个建议是在数据库上创建一个唯一索引,例如通过包含在您的迁移中:

add_index :recipes, :name, :unique => true

这将确保在数据库级别名称是唯一的。但这种方法的一个缺点是,尝试保存副本时返回的ActiveRecord::StatementInvalid异常不是很有用。在捕获此异常时,无法确定错误是由重复记录生成的,而不仅仅是由损坏的SQL生成。

正如RDocs所建议的那样,一种解决方案是解析异常附带的消息,并尝试检测“重复”或“唯一”等字,但这很麻烦,而且消息是特定于数据库后端的。对于SqlLite3,我的理解是该消息是完全通用的,并且不能以这种方式解析。

鉴于这是ActiveRecord用户的一个基本问题,我们很高兴知道是否有任何标准方法来处理这些异常。我将在下面提出我的建议;请评论或提供替代方案;谢谢!

2 个答案:

答案 0 :(得分:7)

解析错误信息并不是那么糟糕,但感觉很糟糕。我碰到的一个建议(不记得在哪里)似乎很有吸引力,在救援区你可以检查数据库,看看是否有重复的记录。如果有,那么StatementInvalid可能是因为重复而你可以相应地处理它。如果没有,那么StatementInvalid必须来自其他东西,你必须以不同的方式处理它。

所以基本的想法,假设recipe.name上的唯一索引如上:

begin
  recipe.save!
rescue ActiveRecord::StatementInvalid
  if Recipe.count(:conditions => {:name => recipe.name}) > 0
    # It's a duplicate
  else
    # Not a duplicate; something else went wrong
  end
end

我尝试使用以下内容自动执行此检查:

class ActiveRecord::Base
  def violates_unique_index?(opts={})
    raise unless connection
    unique_indexes = connection.indexes(self.class.table_name).select{|i|i.unique}
    unique_indexes.each do |ui|
      conditions = {}
      ui.columns.each do |col|
        conditions[col] = send(col)
      end
      next if conditions.values.any?{|c|c.nil?} and !opts[:unique_includes_nil]
      return true if self.class.count(:conditions => conditions) > 0
    end
    return false
  end
end

现在您应该可以在救援区中使用generic_record.violates_unique_index?来决定如何处理StatementInvalid。

希望有帮助!其他方法?

答案 1 :(得分:2)

这真的是一个大问题吗?

如果您使用唯一索引和validates_uniqueness_of约束,那么

  • 将保持数据完整性
  • 你最坏的 只有两个分开时才会出错 请求尝试插入非唯一的 同时划行

因此,除非你有一个应用程序,它可以执行许多潜在的重复插入(在这种情况下,我会考虑重新设计),我认为这在实践中很少出现问题。