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.

Ethan
la source
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
1
Question connexe: stackoverflow.com/questions/19449019/…
Allerin
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!

efalcao
la source
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
Vikrant Chaudhary
la source
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

Siwei Shen 申思维
la source
20

rails 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset
guai
la source
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 )
Brad Werth
la source
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.

Sarah Mei
la source
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.

Luís Ramalho
la source
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
rfunduk
la source
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é.

chrisrbailey
la source
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

fringd
la source
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? }
Aleks
la source
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).

Dave Smylie
la source
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
Sasha Alexandrov
la source
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

kares
la source
1

J'ai écrit un plugin qui implémente update_without_callbacks dans Rails 3:

http://github.com/dball/skip_activerecord_callbacks

La bonne solution, je pense, est de réécrire vos modèles pour éviter les rappels en premier lieu, mais si cela n'est pas pratique à court terme, ce plugin peut vous aider.

Boule de Donald
la source
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.

oivoodoo
la source
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
tothemario
la source
1

Pour créer des données de test dans Rails, vous utilisez ce hack:

record = Something.new(attrs)
ActiveRecord::Persistence.instance_method(:create_record).bind(record).call

https://coderwall.com/p/y3yp2q/edit

Wojtek Kruszewski
la source
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.

Zinin Serge
la source
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)
Steve Friedman
la source
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.

nitecoder
la source
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

Stephan Wehner
la source
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)
Ryan Crispin Heneise
la source
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

choonkeat
la source
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.
Joshua Pinter
la source
-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'
  ...
James
la source