ActiveRecord的validates_uniqueness_of
是vulnerable to race conditions。要真正确保唯一性,需要额外的保护措施。 ActiveRecord RDocs的一个建议是在数据库上创建一个唯一索引,例如通过包含在您的迁移中:
add_index :recipes, :name, :unique => true
这将确保在数据库级别名称是唯一的。但这种方法的一个缺点是,尝试保存副本时返回的ActiveRecord::StatementInvalid
异常不是很有用。在捕获此异常时,无法确定错误是由重复记录生成的,而不仅仅是由损坏的SQL生成。
正如RDocs所建议的那样,一种解决方案是解析异常附带的消息,并尝试检测“重复”或“唯一”等字,但这很麻烦,而且消息是特定于数据库后端的。对于SqlLite3,我的理解是该消息是完全通用的,并且不能以这种方式解析。
鉴于这是ActiveRecord用户的一个基本问题,我们很高兴知道是否有任何标准方法来处理这些异常。我将在下面提出我的建议;请评论或提供替代方案;谢谢!
答案 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
约束,那么
因此,除非你有一个应用程序,它可以执行许多潜在的重复插入(在这种情况下,我会考虑重新设计),我认为这在实践中很少出现问题。