Comment éviter d'exécuter des rappels ActiveRecord?


140

J'ai quelques modèles qui ont des rappels after_save. En général, c'est bien, mais dans certaines situations, comme lors de la création de données de développement, je souhaite enregistrer les modèles sans exécuter les rappels. Y a-t-il un moyen simple de le faire? Quelque chose qui ressemble à ...

Person#save( :run_callbacks => false )

ou

Person#save_without_callbacks

J'ai regardé dans la documentation Rails et je n'ai rien trouvé. Cependant, d'après mon expérience, les documents Rails ne racontent pas toujours toute l'histoire.

METTRE À JOUR

J'ai trouvé un article de blog qui explique comment supprimer les rappels d'un modèle comme celui-ci:

Foo.after_save.clear

Je n'ai pas pu trouver où cette méthode est documentée, mais elle semble fonctionner.


8
Si vous faites quelque chose de destructeur ou de cher (comme l'envoi d'e-mails) dans un rappel, je vous recommande de le déplacer et de le déclencher séparément du contrôleur ou ailleurs. De cette façon, vous ne le déclencherez pas "accidentellement" en développement, etc.
ryanb

2
la solution que vous avez acceptée ne fonctionne pas pour moi. J'utilise des rails 3. J'obtiens une erreur comme celle-ci: - méthode non définie `update_without_callbacks 'pour # <User: 0x10ae9b848>
Mohit Jain

yaa ce blog a fonctionné ....
Mohit Jain


Ne Foo.after_save.clearsupprimerait -il pas les rappels pour tout le modèle? Et puis comment proposez-vous de les restaurer?
Joshua Pinter le

Réponses:


72

Cette solution est uniquement Rails 2.

Je viens juste d'enquêter et je pense avoir une solution. Il existe deux méthodes privées ActiveRecord que vous pouvez utiliser:

update_without_callbacks
create_without_callbacks

Vous allez devoir utiliser send pour appeler ces méthodes. exemples:

p = Person.new(:name => 'foo')
p.send(:create_without_callbacks)

p = Person.find(1)
p.send(:update_without_callbacks)

C'est certainement quelque chose que vous ne voudrez vraiment utiliser que dans la console ou lors de tests aléatoires. J'espère que cela t'aides!


7
Ça ne fonctionne pas pour moi. J'utilise des rails 3. J'obtiens une erreur comme celle-ci: - méthode non définie `update_without_callbacks 'pour # <User: 0x10ae9b848>
Mohit Jain

Votre suggestion ne fonctionne pas mais le billet de blog mentionné dans la partie mise à jour fonctionne.
Mohit Jain

Cela sautera également les validations.
Daniel Pietzsch le

J'ai une autre solution pour n'importe quelle version de Rails. Cela fonctionne bien pour nous. Découvrez-le dans mon article de blog: railsguides.net/2014/03/25/skip-callbacks-in-tests
ka8725

224

Utilisez update_column(Rails> = v3.1) ou update_columns(Rails> = 4.0) pour ignorer les rappels et les validations. Aussi avec ces méthodes, updated_atn'est pas mis à jour.

#Rails >= v3.1 only
@person.update_column(:some_attribute, 'value')
#Rails >= v4.0 only
@person.update_columns(attributes)

http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column

# 2: Ignorer les rappels qui fonctionnent également lors de la création d'un objet

class Person < ActiveRecord::Base
  attr_accessor :skip_some_callbacks

  before_validation :do_something
  after_validation :do_something_else

  skip_callback :validation, :before, :do_something, if: :skip_some_callbacks
  skip_callback :validation, :after, :do_something_else, if: :skip_some_callbacks
end

person = Person.new(person_params)
person.skip_some_callbacks = true
person.save

2
on dirait que cela fonctionne aussi avec 2.x, et il existe une foule d'autres méthodes qui fonctionnent de la même manière: guides.rubyonrails.org
...

15
Cela ne répond pas :create_without_callbacks: (Comment puis-je exécuter quelque chose de similaire? (Travaillé dans Rails2, supprimé dans Rails3).
nzifnab

En supposant qu'il y @personait une variable dans un contrôleur quelque part, cette solution signifie que les personnes lisant votre classe de modèle ne pourront pas comprendre les rappels. Ils verront after_create :something_coolet penseront "génial, il se passe quelque chose de cool après la création!". Pour comprendre réellement votre classe de modèle, ils devront greper tous vos contrôleurs, à la recherche de tous les petits endroits où vous avez décidé d'injecter de la logique. Je n'aime pas ça> o <;;
Ziggy

1
remplacez-le skip_callback ..., if: :skip_some_callbackspar after_create ..., unless: :skip_some_callbackspour l'exécuter correctement avec after_create.
sakurashinken

28

Actualisé:

La solution de @Vikrant Chaudhary semble meilleure:

#Rails >= v3.1 only
@person.update_column(:some_attribute, 'value')
#Rails >= v4.0 only
@person.update_columns(attributes)

Ma réponse originale:

voir ce lien: Comment ignorer les rappels ActiveRecord?

dans Rails3,

supposons que nous ayons une définition de classe:

class User < ActiveRecord::Base
  after_save :generate_nick_name
end 

Approche1:

User.send(:create_without_callbacks)
User.send(:update_without_callbacks)

Approach2: Lorsque vous voulez les ignorer dans vos fichiers rspec ou autre, essayez ceci:

User.skip_callback(:save, :after, :generate_nick_name)
User.create!()

REMARQUE: une fois que cela est fait, si vous n'êtes pas dans l'environnement rspec, vous devez réinitialiser les rappels:

User.set_callback(:save, :after, :generate_nick_name)

fonctionne bien pour moi sur les rails 3.0.5


20

rails 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset

11
Agréable. Aussi MyModel.skip_callback (: create,: after,: my_callback) pour un contrôle précis .. voir ActiveSupport :: Callbacks :: ClassMethods docs pour tous les lobang
tardate

4
Information utile: le 'symbole' reset_callbacksn'est pas :after_save, mais plutôt :save. apidock.com/rails/v3.0.9/ActiveSupport/Callbacks/ClassMethods/…
nessur

19

Si l'objectif est simplement d'insérer un enregistrement sans callback ou validations, et que vous souhaitez le faire sans recourir à des gemmes supplémentaires, en ajoutant des vérifications conditionnelles, en utilisant RAW SQL ou en utilisant votre code existant de quelque manière que ce soit, envisagez d'utiliser un "shadow object "pointant vers votre table db existante. Ainsi:

class ImportedPerson < ActiveRecord::Base
  self.table_name = 'people'
end

Cela fonctionne avec toutes les versions de Rails, est threadsafe et élimine complètement toutes les validations et rappels sans aucune modification de votre code existant. Vous pouvez simplement lancer cette déclaration de classe juste avant votre importation réelle, et vous devriez être prêt à partir. N'oubliez pas d'utiliser votre nouvelle classe pour insérer l'objet, comme:

ImportedPerson.new( person_attributes )

4
La meilleure solution JAMAIS. Élégant et simple!
Rafael Oliveira

1
Cela a très bien fonctionné pour moi car c'était quelque chose que je voulais faire uniquement en test, pour simuler l'état "avant" de la base de données, sans polluer mon objet de modèle de production avec des machines pour éventuellement sauter les rappels.
Douglas Lovell

1
De loin la meilleure réponse
robomc

1
Approuvé car il montre comment contourner les contraintes de rails existantes et m'a aidé à comprendre comment tout l'objet MVC fonctionne vraiment. Si simple et propre.
Michael Schmitz le

17

Vous pouvez essayer quelque chose comme ceci dans votre modèle Person:

after_save :something_cool, :unless => :skip_callbacks

def skip_callbacks
  ENV[RAILS_ENV] == 'development' # or something more complicated
end

EDIT: after_save n'est pas un symbole, mais c'est au moins la 1000e fois que j'essaye d'en faire un.


1
Je pense vraiment que c'est la meilleure réponse ici. De cette façon, la logique qui détermine le moment où le rappel est ignoré est disponible dans le modèle, et vous ne disposez pas de fragments de code fous partout annulant la logique métier ou contournant l'encapsulation avec send. KOODOS
Ziggy

10

Vous pouvez utiliser update_columns:

User.first.update_columns({:name => "sebastian", :age => 25})

Met à jour les attributs donnés d'un objet, sans appeler save, donc en ignorant les validations et les rappels.


7

Le seul moyen d'empêcher tous les rappels after_save est que le premier retourne false.

Vous pourriez peut-être essayer quelque chose comme (non testé):

class MyModel < ActiveRecord::Base
  attr_accessor :skip_after_save

  def after_save
    return false if @skip_after_save
    ... blah blah ...
  end
end

...

m = MyModel.new # ... etc etc
m.skip_after_save = true
m.save

1
J'adore essayer (non testé). Tour à sensations fortes.
Adamantish

Testé et ça marche. Je pense que c'est une très bonne solution propre, merci!
kernification le

5

Il semble qu'une façon de gérer cela dans Rails 2.3 (puisque update_without_callbacks est parti, etc.), serait d'utiliser update_all, qui est l'une des méthodes qui saute les rappels selon la section 12 du Guide Rails sur les validations et les rappels .

Notez également que si vous faites quelque chose dans votre callback after_, qui fait un calcul basé sur de nombreuses associations (c'est-à-dire un has_many assoc, où vous faites également accept_nested_attributes_for), vous devrez recharger l'association, au cas où dans le cadre de la sauvegarde , l'un de ses membres a été supprimé.


4

https://gist.github.com/576546

vider simplement ce monkey-patch dans config / initializers / skip_callbacks.rb

puis

Project.skip_callbacks { @project.save }

ou semblable.

tout le crédit à l'auteur


4

La plupart des up-votedréponses peuvent sembler déroutantes dans certains cas.

Vous pouvez utiliser une simple ifvérification si vous souhaitez ignorer un rappel, comme ceci:

after_save :set_title, if: -> { !new_record? && self.name_changed? }

3

Une solution qui devrait fonctionner sur toutes les versions de Rails sans utiliser de gemme ou de plugin consiste simplement à émettre des instructions de mise à jour directement. par exemple

ActiveRecord::Base.connection.execute "update table set foo = bar where id = #{self.id}"

Cela peut (ou non) être une option en fonction de la complexité de votre mise à jour. Cela fonctionne bien pour les drapeaux de mise à jour , par exemple sur un enregistrement de l' intérieur d' un rappel after_save (sans redéclencher le rappel).


Je ne sais pas pourquoi le vote négatif, mais je pense toujours que la réponse ci-dessus est légitime. Parfois, le meilleur moyen d'éviter les problèmes de comportement d'ActiveRecord est d'éviter d'utiliser ActiveRecord.
Dave Smylie

A voté en principe pour contrer le -1. Nous venons d'avoir un problème de production (avec une longue histoire derrière) qui nous obligeait à créer un nouvel enregistrement (pas une mise à jour) et le déclenchement de rappels aurait été catastrophique. Toutes les réponses ci-dessus sont des hacks, qu'ils l'admettent ou non, et aller à la base de données était la meilleure solution. Il existe des conditions légitimes pour cela. Bien qu'il faille se méfier de l'injection SQL avec le #{...}.
sinisterchipmunk

1
# for rails 3
  if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
    def update_without_callbacks
      attributes_with_values = arel_attributes_values(false, false, attribute_names)
      return false if attributes_with_values.empty?
      self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
    end
  end

1

Aucun de ces éléments ne pointe vers un without_callbacksplugin qui fait juste ce dont vous avez besoin ...

class MyModel < ActiveRecord::Base
  before_save :do_something_before_save

  def after_save
    raise RuntimeError, "after_save called"
  end

  def do_something_before_save
    raise RuntimeError, "do_something_before_save called"
  end
end

o = MyModel.new
MyModel.without_callbacks(:before_save, :after_save) do
  o.save # no exceptions raised
end

http://github.com/cjbottaro/without_callbacks fonctionne avec Rails 2.x



1

Si vous utilisez Rails 2. Vous pouvez utiliser une requête SQL pour mettre à jour votre colonne sans exécuter de rappels et de validations.

YourModel.connection.execute("UPDATE your_models SET your_models.column_name=#{value} WHERE your_models.id=#{ym.id}")

Je pense que cela devrait fonctionner dans toutes les versions de rails.


1

Lorsque j'ai besoin d'un contrôle total sur le rappel, je crée un autre attribut qui est utilisé comme commutateur. Simple et efficace:

Modèle:

class MyModel < ActiveRecord::Base
  before_save :do_stuff, unless: :skip_do_stuff_callback
  attr_accessor :skip_do_stuff_callback

  def do_stuff
    puts 'do stuff callback'
  end
end

Tester:

m = MyModel.new()

# Fire callbacks
m.save

# Without firing callbacks
m.skip_do_stuff_callback = true
m.save

# Fire callbacks again
m.skip_do_stuff_callback = false
m.save


1

Vous pouvez utiliser la gemme de sauvegarde sournoise: https://rubygems.org/gems/sneaky-save .

Notez que cela ne peut pas aider à enregistrer des associations sans validations. Il renvoie l'erreur 'created_at cannot be null' car il insère directement la requête SQL contrairement à un modèle. Pour implémenter cela, nous devons mettre à jour toutes les colonnes générées automatiquement de db.


1

J'avais besoin d'une solution pour Rails 4, alors j'ai trouvé ceci:

app / models / concern / save_without_callbacks.rb

module SaveWithoutCallbacks

  def self.included(base)
    base.const_set(:WithoutCallbacks,
      Class.new(ActiveRecord::Base) do
        self.table_name = base.table_name
      end
      )
  end

  def save_without_callbacks
    new_record? ? create_without_callbacks : update_without_callbacks
  end

  def create_without_callbacks
    plain_model = self.class.const_get(:WithoutCallbacks)
    plain_record = plain_model.create(self.attributes)
    self.id = plain_record.id
    self.created_at = Time.zone.now
    self.updated_at = Time.zone.now
    @new_record = false
    true
  end

  def update_without_callbacks
    update_attributes = attributes.except(self.class.primary_key)
    update_attributes['created_at'] = Time.zone.now
    update_attributes['updated_at'] = Time.zone.now
    update_columns update_attributes
  end

end

dans n'importe quel modèle:

include SaveWithoutCallbacks

Ensuite vous pouvez:

record.save_without_callbacks

ou

Model::WithoutCallbacks.create(attributes)

0

Pourquoi voudriez-vous pouvoir faire cela en développement? Cela signifiera sûrement que vous construisez votre application avec des données non valides et, en tant que telle, elle se comportera de manière étrange et non comme vous vous y attendez en production.

Si vous souhaitez remplir votre base de données de développement avec des données, une meilleure approche serait de créer une tâche de râteau qui utilise le gem faker pour créer des données valides et les importer dans la base de données en créant autant ou peu d'enregistrements que vous le souhaitez, mais si vous êtes talon plié dessus et avoir une bonne raison, je suppose que update_without_callbacks et create_without_callbacks fonctionneront bien, mais lorsque vous essayez de plier les rails à votre guise, demandez-vous que vous avez une bonne raison et si ce que vous faites est vraiment une bonne idée.


Je n'essaye pas de sauvegarder sans validations, juste sans rappels. Mon application utilise des rappels pour écrire du HTML statique dans le système de fichiers (un peu comme un CMS). Je ne veux pas faire cela lors du chargement des données de développement.
Ethan

C'était juste une pensée, je suppose que chaque fois que j'ai vu ce genre de question dans le passé, c'est essayer de contourner des choses pour de mauvaises raisons.
nitecoder

0

Une option consiste à avoir un modèle distinct pour de telles manipulations, en utilisant le même tableau:

class NoCallbacksModel < ActiveRecord::Base
  set_table_name 'table_name_of_model_that_has_callbacks'

  include CommonModelMethods # if there are
  :
  :

end

(La même approche pourrait faciliter les choses pour contourner les validations)

Stéphan


0

Une autre façon serait d'utiliser des hooks de validation au lieu de callbacks. Par exemple:

class Person < ActiveRecord::Base
  validate_on_create :do_something
  def do_something
    "something clever goes here"
  end
end

De cette façon, vous pouvez obtenir le do_something par défaut, mais vous pouvez facilement le remplacer avec:

@person = Person.new
@person.save(false)

3
Cela semble être une mauvaise idée - vous devriez utiliser les choses aux fins auxquelles elles sont destinées. La dernière chose que vous voulez, ce sont vos validations pour avoir des effets secondaires.
chug2k

0

Quelque chose qui devrait fonctionner avec toutes les versions de ActiveRecordsans dépendre des options ou des méthodes activerecord qui peuvent exister ou non.

module PlainModel
  def self.included(base)
    plainclass = Class.new(ActiveRecord::Base) do
      self.table_name = base.table_name
    end
    base.const_set(:Plain, plainclass)
  end
end


# usage
class User < ActiveRecord::Base
  include PlainModel

  validates_presence_of :email
end

User.create(email: "")        # fail due to validation
User::Plain.create(email: "") # success. no validation, no callbacks

user = User::Plain.find(1)
user.email = ""
user.save

TLDR: utilisez un "modèle d'activation d'enregistrement différent" sur la même table


0

Pour les rappels personnalisés, utilisez un attr_accessoret un unlessdans le rappel.

Définissez votre modèle comme suit:

class Person << ActiveRecord::Base

  attr_accessor :skip_after_save_callbacks

  after_save :do_something, unless: :skip_after_save_callbacks

end

Et puis, si vous devez sauvegarder l'enregistrement sans toucher les after_saverappels que vous avez définis, définissez l' skip_after_save_callbacksattribut virtuel sur true.

person.skip_after_save_callbacks #=> nil
person.save # By default, this *will* call `do_something` after saving.

person.skip_after_save_callbacks = true
person.save # This *will not* call `do_something` after saving.

person.skip_after_save_callbacks = nil # Always good to return this value back to its default so you don't accidentally skip callbacks.

-5

Ce n'est pas la manière la plus propre, mais vous pouvez envelopper le code de rappel dans une condition qui vérifie l'environnement Rails.

if Rails.env == 'production'
  ...
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.