ActiveRecord的validates_uniqueness_of是
vulnerable to race conditions.要真正确保唯一性,需要额外的保护措施. ActiveRecord RDocs的一个建议是在数据库上创建一个唯一索引,例如通过包含在您的迁移中:
add_index :recipes,:name,:unique => true
这将确保在数据库级别名称是唯一的.但是这种方法的一个缺点是,在尝试保存副本时返回的ActiveRecord :: StatementInvalid异常不是很有用.在捕获此异常时,无法确定错误是由重复记录生成的,而不仅仅是由损坏的sql生成的.
正如RDocs所建议的那样,一种解决方案是解析异常附带的消息,并尝试检测诸如“重复”或“唯一”之类的字,但这很麻烦,而且消息是特定于数据库后端的.对于sqlLite3,我的理解是该消息是完全通用的,并且不能以这种方式解析.
鉴于这是ActiveRecord用户的一个基本问题,很高兴知道是否有任何标准方法来处理这些异常.我将在下面提出我的建议;请评论或提供替代方案;谢谢!
解决方法
解析错误消息并不是那么糟糕,但感觉很糟糕.我碰到的一个建议(不记得在哪里)似乎很有吸引力,在救援区你可以检查数据库,看看是否确实存在重复记录.如果有,那么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.
希望有所帮助!其他方法?