2009-04-07 4 views
6

validates_uniqueness_of d'ActiveRecord est vulnerable to race conditions. Pour vraiment assurer l'unicité nécessite des garanties supplémentaires. Une suggestion du ActiveRecord RDocs est de créer un index unique sur la base de données, par exemple en incluant dans vos migrations:Comment puis-je déterminer si mon objet ActiveRecord enfreint une clé/index de base de données unique?

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

Cela permettra d'assurer au niveau de la base de données que le nom est unique. Mais un inconvénient de cette approche est que l'exception ActiveRecord::StatementInvalid retournée lors de la tentative d'enregistrement d'une copie n'est pas très utile. On ne peut pas être sûr en attrapant cette exception que l'erreur a été générée par un enregistrement en double et pas seulement par un code SQL cassé. Une solution, comme le suggèrent les RDocs, consiste à analyser le message fourni avec l'exception et à essayer de détecter des mots tels que «duplicate» ou «unique», mais c'est kludgy et le message est spécifique au backend de base de données. Pour SqlLite3, je crois comprendre que le message est totalement générique et ne peut pas être analysé de cette façon. Etant donné qu'il s'agit d'un problème fondamental pour les utilisateurs d'ActiveRecord, il serait bon de savoir s'il existe une approche standard pour gérer ces exceptions. Je vais offrir ma suggestion ci-dessous; veuillez commenter ou fournir des alternatives; Merci!

Répondre

7

L'analyse du message d'erreur n'est pas si mauvaise, mais semble kludgy. Une suggestion que j'ai croisée (ne me souviens pas où) qui semble attrayante est que dans le bloc de sauvetage, vous pouvez vérifier la base de données pour voir s'il y a en fait un enregistrement en double. Si c'est le cas, il est fort probable que le StatementInvalid soit en raison du doublon et que vous puissiez le gérer en conséquence. Si ce n'est pas le cas, le StatementInvalid doit provenir d'autre chose et vous devez le gérer différemment.

L'idée de base, en supposant un index unique sur recipe.name comme ci-dessus:

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 

Je tentais d'automatiser cette vérification avec les éléments suivants:

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 

Alors maintenant, vous devriez être en mesure d'utiliser generic_record.violates_unique_index? dans votre bloc de secours pour décider comment gérer StatementInvalid.

Espérons que c'est utile! D'autres approches?

+0

J'ai implémenté ceci et semble faire le travail. Tenté de retirer validates_uniqueness car cela fait essentiellement la même chose plus efficacement. Publiera toutes les mises à jour pertinentes. – Chinasaur

+0

Un Nitpick très mineur: vous obtenez la mauvaise erreur si le doublon est supprimé entre l'obtention de l'exception et l'interrogation de la copie. – mpartel

2

Est-ce vraiment un gros problème?

Si vous utilisez un index unique avec une contrainte validates_uniqueness_of, puis

  • L'intégrité des données sera maintenue
  • Vous allez au pire seulement obtenir une erreur lorsque deux demandes séparées tentent d'insérer un non -Unique ligne simultanément

donc, sauf si vous avez une application qui fait beaucoup de inserts en double potentiels (dans ce cas, je regardais redesig Je vois cela rarement poser problème dans la pratique.

+0

À droite, vous obtiendrez rarement l'exception. Mais j'ai toujours pensé que ce serait bien de le gérer :). Une question découlant de votre réponse: si je peux l'éviter, est-ce que je veux même utiliser 'validates_uniqueness_of'? Si ma réponse fonctionne, je serais tenté de jeter au moins un: si dessus ... – Chinasaur

Questions connexes