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?

StackOverflow https://stackoverflow.com/questions/1597434

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
Était-ce utile?

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:

  1. avant la validation (on_create)
  2. validate
  3. après validation (on_create)
  4. before_create_actions_that_wont_be_rolled_back
  5. avant la validation (on_create)
  6. validate
  7. après validation (on_create)
  8. avant d'enregistrer callbacks
  9. avant de créer callbacks
  10. enregistrement est créé
  11. après créer callbacks
  12. 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:

scroll top