Comment prévenir les changements de base de données à l'intérieur d'un filtre Rails ActiveRecord de before_create de se faire annulées quand il retourne faux?
-
22-09-2019 - |
Question
J'ai ajouté un filtre before_create à un de mes modèles Rails ActiveRecord et à l'intérieur ce filtre que je fais des mises à jour de base de données.
Parfois, je reviens faux du filtre pour empêcher la création du modèle cible, mais qui est à l'origine de tous les autres changements de base de données que j'ai fait (alors que l'intérieur du filtre) pour obtenir annulées.
Comment puis-je empêcher cela?
Mise à jour # 1 : Voici un code pseudo expliquant mon problème:
class Widget < ActiveRecord::Base
before_create :update_instead
def update_instead
if some_condition?
update_some_record_in_same_model # this is getting rolled back
return false # don't create a new record
else
return true # let it continue
end
end
end
Mise à jour # 2 : Quelques bonnes réponses ci-dessous, mais chacun avait ses défauts. J'ai fini par overridding la méthode de création comme ceci:
def create
super unless update_instead? # yes I reversed the return values from above
end
La solution
Je viens de le faire récemment. Vous devez demander expressément une autre connexion de AR. Ensuite, exécutez vos modifications sur cette connexion. De cette façon, si la création échoue et annule la transaction, les modifications de votre rappel ont déjà été commis dans une autre transaction.
Ignorer ma réponse ci-dessus. L'exemple de code que vous venez de donner des choses vraiment clarifié.
class Foo < ActiveRecord::Base
before_create :update_instead
def update_instead
dbconn = self.class.connection_pool.checkout
dbconn.transaction do
dbconn.execute("update foos set name = 'updated'")
end
self.class.connection_pool.checkin(dbconn)
false
end
end
>> Foo.create(:name => 'sam')
=> #<Foo id: nil, name: "sam", created_at: nil, updated_at: nil>
>> Foo.all
=> [#<Foo id: 2, name: "updated", created_at: "2009-10-21 15:12:55", updated_at: "2009-10-21 15:12:55">]
Autres conseils
A l'aide d'une transaction dans le filtre.
Avez-vous essayé de créer écraser / enregistrer et leurs versions destructrices? ActiveRecord :: Base.create, ActiveRecord :: Base.save et leurs versions destructrices sont enveloppés dans une transaction, ils sont aussi ce que callbacks déclencheur et validations. Si vous surchargeons il, que les choses fait par super fera partie d'une transaction. Si vous avez besoin yo validateurs avant, alors vous pouvez appeler explicitement valide pour les exécuter tous.
Exemple:
before_create :before_create_actions_that_can_be_rolled_back
def create
if valid? && before_create_actions_that_wont_be_rolled_back
super
end
end
def before_create_actions_that_wont_be_rolled_back
# exactly what it sounds like
end
def before_create_actions_that_can_be_rolled_back
# exactly what it sounds like
end
Caveat: Avec ces modifications les méthodes seront appelées dans cet ordre:
- avant la validation (on_create)
- validate
- après validation (on_create)
- before_create_actions_that_wont_be_rolled_back
- avant la validation (on_create)
- validate
- après validation (on_create)
- avant d'enregistrer callbacks
- avant de créer callbacks
- enregistrement est créé
- après créer callbacks
- après sauver callbacks
Si un échec ou si la validation tout rappel retourne false dans les étapes 5-12 la base de données sera annulée à l'état où il était avant l'étape 5.
Si elle est valide? échoue ou ne before_create_actions_that_wont_be_rolled_back que sera interrompu toute la chaîne.
ces changements pourraient se faire dans un lieu after_create
?
Il utilise le même concept que la réponse de Rich Cavanaugh. J'ai ajouté un modèle de parent il est donc clair que le filtre est en train de faire. La clé consiste à utiliser un fil + un automatique check-out / check-in d'une connexion séparée. Remarque: vous devez vous assurer que votre: valeur de piscine est située à au moins 2 dans votre cahier des charges de connexion, selon le nombre de threads simultanés vous exécutez. Je pense que ce défaut à 5.
class Gadget < ActiveRecord::Base
has_many :widgets
end
class Widget < ActiveRecord::Base
belongs_to :gadget
before_create :update_instead
def update_some_record_in_same_model
# the thread forces a new connection to be checked out
Thread.new do
ActiveRecord::Base.connection_pool.with_connection do |conn|
# try this part without the 2 surrounding blocks and it will be rolled back
gadget.touch_count += 1
gadget.save!
end
end.join
end
def some_condition?
true
end
def update_instead
if some_condition?
update_some_record_in_same_model # this is getting rolled back
p [:touch_count_in_filter, gadget.reload.touch_count]
return false # don't create a new record
else
return true # let it continue
end
end
end
Test:
g = Gadget.create(:name => 'g1')
puts "before:"
p [:touch_count, g.reload.touch_count]
p [:widget_count, Widget.count]
g.widgets.create(:name => 'w1')
puts "after:"
# Success means the count stays incremented
p [:touch_count, g.reload.touch_count]
p [:widget_count, Widget.count]
Pour en savoir plus: