¿Cómo puedo evitar ejecutar devoluciones de llamada ActiveRecord?

140

Tengo algunos modelos que tienen devoluciones de llamada after_save. Por lo general, está bien, pero en algunas situaciones, como al crear datos de desarrollo, quiero guardar los modelos sin que se ejecuten las devoluciones de llamada. ¿Hay una manera simple de hacer eso? Algo parecido a ...

Person#save( :run_callbacks => false )

o

Person#save_without_callbacks

Miré en los documentos de Rails y no encontré nada. Sin embargo, en mi experiencia, los documentos de Rails no siempre cuentan toda la historia.

ACTUALIZAR

Encontré una publicación de blog que explica cómo puedes eliminar las devoluciones de llamada de un modelo como este:

Foo.after_save.clear

No pude encontrar dónde está documentado ese método, pero parece funcionar.

Ethan
fuente
8
Si está haciendo algo destructivo o costoso (como enviar correos electrónicos) en una devolución de llamada, le recomiendo mover esto y activarlo por separado del controlador o en otro lugar. De esta manera usted no va a "accidentalmente" gatillo en desarrollo, etc.
ryanb
2
La solución que aceptaste no funciona para mí. Estoy usando rails 3. Recibo un error como este: - método indefinido `update_without_callbacks 'para # <Usuario: 0x10ae9b848>
Mohit Jain
yaa esa publicación de blog funcionó ...
Mohit Jain
1
Pregunta relacionada: stackoverflow.com/questions/19449019/…
Allerin
¿No Foo.after_save.cleareliminaría las devoluciones de llamada para todo el modelo? Y entonces, ¿cómo propones restaurarlos?
Joshua Pinter

Respuestas:

72

Esta solución es solo Rails 2.

Acabo de investigar esto y creo que tengo una solución. Hay dos métodos privados de ActiveRecord que puede usar:

update_without_callbacks
create_without_callbacks

Tendrá que usar enviar para llamar a estos métodos. ejemplos:

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

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

Esto es definitivamente algo que realmente solo querrás usar en la consola o al hacer algunas pruebas aleatorias. ¡Espero que esto ayude!

efalcao
fuente
77
No está funcionando para mi. Estoy usando rails 3. Recibo un error como este: - método indefinido `update_without_callbacks 'para # <Usuario: 0x10ae9b848>
Mohit Jain
Su sugerencia no funciona, pero la publicación de blog mencionada en la parte de actualización funciona.
Mohit Jain
Esto también omitirá las validaciones.
Daniel Pietzsch
Tengo otra solución para cualquier versión de Rails. Nos funciona bien. Échale un vistazo en mi blog: railsguides.net/2014/03/25/skip-callbacks-in-tests
ka8725
224

Use update_column(Rails> = v3.1) o update_columns(Rails> = 4.0) para omitir devoluciones de llamadas y validaciones. También con estos métodos, updated_atse no actualizada.

#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: omitiendo las devoluciones de llamada que también funcionan al crear un objeto

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
fuente
2
Parece que también funciona con 2.x, y hay una gran cantidad de otros métodos que funcionan de manera similar: guides.rubyonrails.org/…
rogerdpack
15
Esto no aborda :create_without_callbacks:( ¿Cómo puedo ejecutar algo similar a eso? (Trabajado en Rails2, eliminado en Rails3).
nzifnab
Suponiendo que @persones una variable en un controlador en alguna parte, esta solución significa que las personas que leen su clase de modelo no podrán entender las devoluciones de llamada. Verán after_create :something_cooly pensarán "¡genial, algo genial sucede después de crear!". Para comprender realmente su clase de modelo, tendrán que examinar todos sus controladores, buscando todos los pequeños lugares donde ha decidido inyectar lógica. No me gusta> o <;;
Ziggy
1
reemplace skip_callback ..., if: :skip_some_callbackscon after_create ..., unless: :skip_some_callbackspara ejecutar esto correctamente con after_create.
sakurashinken
28

Actualizado:

La solución de @Vikrant Chaudhary parece mejor:

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

Mi respuesta original:

ver este enlace: ¿Cómo omitir las devoluciones de llamada de ActiveRecord?

en Rails3,

Supongamos que tenemos una definición de clase:

class User < ActiveRecord::Base
  after_save :generate_nick_name
end 

Enfoque1:

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

Enfoque2: cuando desee omitirlos en sus archivos rspec o lo que sea, intente esto:

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

NOTA: una vez hecho esto, si no está en el entorno rspec, debe restablecer las devoluciones de llamada:

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

funciona bien para mí en rieles 3.0.5

Siwei Shen 申思维
fuente
20

rieles 3:

MyModel.send("_#{symbol}_callbacks") # list  
MyModel.reset_callbacks symbol # reset
guai
fuente
11
Agradable. También MyModel.skip_callback (: create,: after,: my_callback) para un control preciso ... vea ActiveSupport :: Callbacks :: ClassMethods docs para todos los lobang
tardate
44
Información útil: el 'símbolo' en reset_callbacksno es :after_save, sino más bien :save. apidock.com/rails/v3.0.9/ActiveSupport/Callbacks/ClassMethods/…
nessur
19

Si el objetivo es simplemente insertar un registro sin devoluciones de llamada o validaciones, y le gustaría hacerlo sin recurrir a gemas adicionales, agregar comprobaciones condicionales, usar SQL RAW o inutilizar su código de salida de alguna manera, considere usar una "sombra" object "apuntando a su tabla db existente. Al igual que:

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

Esto funciona con todas las versiones de Rails, es seguro para subprocesos y elimina por completo todas las validaciones y devoluciones de llamadas sin modificaciones en su código existente. Simplemente puede incluir esa declaración de clase justo antes de su importación real, y debería estar listo. Solo recuerde usar su nueva clase para insertar el objeto, como:

ImportedPerson.new( person_attributes )
Brad Werth
fuente
44
La mejor solución de la historia. Elegante y simple!
Rafael Oliveira
1
Esto funcionó muy bien para mí porque era algo que quería hacer solo en la prueba, simular el estado de la base de datos "antes", sin contaminar mi objeto de modelo de producción con maquinaria para saltear opcionalmente las devoluciones de llamada.
Douglas Lovell
1
Con mucho, la mejor respuesta
robomc
1
Votado porque muestra cómo solucionar las restricciones de rieles existentes y me ayudó a comprender cómo funciona realmente todo el objeto MVC. Tan simple y limpio.
Michael Schmitz
17

Puede intentar algo como esto en su modelo de Persona:

after_save :something_cool, :unless => :skip_callbacks

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

EDITAR: after_save no es un símbolo, pero esa es al menos la milésima vez que he intentado hacerlo uno.

Sarah Mei
fuente
1
Realmente creo que esta es la mejor respuesta aquí. De esta manera, la lógica que determina cuándo se omite la devolución de llamada está disponible en el modelo, y no tiene fragmentos de código locos en todas partes que revierten la lógica empresarial o eluden la encapsulación send. KOODOS
Ziggy
10

Puedes usar update_columns:

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

Actualiza los atributos dados de un objeto, sin llamar a guardar, por lo tanto, omitiendo validaciones y devoluciones de llamada.

Luís Ramalho
fuente
7

La única forma de evitar todas las devoluciones de llamada after_save es hacer que la primera devuelva false.

Quizás podría intentar algo como (no probado):

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
fuente
1
Me encanta probar (no probado). Paseo de la emoción.
Adamantish
Probado y funciona. Creo que esta es una solución muy buena y limpia, ¡gracias!
kernification
5

Parece que una forma de manejar esto en Rails 2.3 (ya que update_without_callbacks ha desaparecido, etc.), sería usar update_all, que es uno de los métodos que omite las devoluciones de llamadas según la sección 12 de la Guía de validaciones y devoluciones de llamadas de Rails .

Además, tenga en cuenta que si está haciendo algo en su devolución de llamada after_, eso hace un cálculo basado en muchas asociaciones (es decir, una asociación has_many, donde también acepta "atributos_de_temados"), deberá volver a cargar la asociación, en caso de que sea parte del guardado , uno de sus miembros fue eliminado.

chrisrbailey
fuente
4

https://gist.github.com/576546

simplemente descargue este parche de mono en config / initializers / skip_callbacks.rb

luego

Project.skip_callbacks { @project.save }

o similar.

todo crédito al autor

fringd
fuente
4

La mayor up-votedrespuesta puede parecer confusa en algunos casos.

Puede usar solo una simple ifverificación si desea omitir una devolución de llamada, como esta:

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

Una solución que debería funcionar en todas las versiones de Rails sin el uso de una gema o complemento es simplemente emitir declaraciones de actualización directamente. p.ej

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

Esto puede (o no) ser una opción dependiendo de cuán compleja sea su actualización. Esto funciona bien para, por ejemplo banderas de actualización de un registro desde dentro de una devolución de llamada after_save (sin retriggering la devolución de llamada).

Dave Smylie
fuente
No estoy seguro de por qué el voto negativo, pero todavía creo que la respuesta anterior es legítima. A veces, la mejor manera de evitar problemas con el comportamiento de ActiveRecord es evitar el uso de ActiveRecord.
Dave Smylie
Votado por principio para contrarrestar el -1. Acabamos de tener un problema de producción (con una larga historia detrás) que nos obligó a crear un nuevo registro (no una actualización) y la devolución de llamadas habría sido catastrófica. Todas las respuestas anteriores son hacks, lo admitan o no, e ir a la base de datos fue la mejor solución. Hay condiciones legítimas para esto. Aunque uno debe tener cuidado con la inyección SQL con el #{...}.
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
fuente
1

Ninguno de estos puntos al without_callbackscomplemento que simplemente hace lo que necesita ...

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 funciona con Rails 2.x

kares
fuente
1

Escribí un complemento que implementa update_without_callbacks en Rails 3:

http://github.com/dball/skip_activerecord_callbacks

Creo que la solución correcta es reescribir sus modelos para evitar devoluciones de llamada en primer lugar, pero si eso no es práctico a corto plazo, este complemento puede ayudar.

Bola de Donald
fuente
1

Si está utilizando Rails 2. Puede usar la consulta SQL para actualizar su columna sin ejecutar devoluciones de llamada y validaciones.

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

Creo que debería funcionar en cualquier versión de rieles.

oivoodoo
fuente
1

Cuando necesito un control total sobre la devolución de llamada, creo otro atributo que se usa como un interruptor. Simple y efectivo:

Modelo:

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

Prueba:

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
fuente
1

Para crear datos de prueba en Rails, usa este truco:

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

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

Wojtek Kruszewski
fuente
1

Puede usar la gema sneaky-save: https://rubygems.org/gems/sneaky-save .

Tenga en cuenta que esto no puede ayudar a guardar asociaciones sin validaciones. Lanza el error 'created_at no puede ser nulo' ya que inserta directamente la consulta sql a diferencia de un modelo. Para implementar esto, necesitamos actualizar todas las columnas de db generadas automáticamente.

Zinin Serge
fuente
1

Necesitaba una solución para Rails 4, así que se me ocurrió esto:

aplicación / modelos / preocupaciones / 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

en cualquier modelo:

include SaveWithoutCallbacks

entonces tú puedes:

record.save_without_callbacks

o

Model::WithoutCallbacks.create(attributes)
Steve Friedman
fuente
0

¿Por qué querrías poder hacer esto en desarrollo? Seguramente esto significará que está creando su aplicación con datos no válidos y, como tal, se comportará de manera extraña y no como espera en la producción.

Si desea llenar su base de datos de desarrollo con datos, un mejor enfoque sería construir una tarea de rastrillo que utilizara la gema falsa para crear datos válidos e importarlos a la base de datos creando tantos o pocos registros como desee, pero si es un talón empeñado y tengo una buena razón, supongo que actualizar_sin_callbacks y create_without_callbacks funcionarán bien, pero cuando intentes doblar rieles a tu voluntad, pregúntate a ti mismo que tienes una buena razón y si lo que estás haciendo es realmente una buena idea.

Nitecoder
fuente
No estoy tratando de guardar sin validaciones, solo sin devoluciones de llamada. Mi aplicación está utilizando devoluciones de llamada para escribir algo de HTML estático en el sistema de archivos (algo así como un CMS). No quiero hacer eso mientras carga datos de desarrollo.
Ethan
Fue solo un pensamiento, supongo que siempre que en el pasado he visto este tipo de preguntas, está tratando de sortear las cosas por malas razones.
nitecoder
0

Una opción es tener un modelo separado para tales manipulaciones, usando la misma tabla:

class NoCallbacksModel < ActiveRecord::Base
  set_table_name 'table_name_of_model_that_has_callbacks'

  include CommonModelMethods # if there are
  :
  :

end

(El mismo enfoque podría facilitar las cosas para evitar las validaciones)

Stephan

Stephan Wehner
fuente
0

Otra forma sería usar ganchos de validación en lugar de devoluciones de llamada. Por ejemplo:

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

De esa forma, puede obtener do_something de forma predeterminada, pero puede anularlo fácilmente con:

@person = Person.new
@person.save(false)
Ryan Crispin Heneise
fuente
3
Esto parece una mala idea: debe usar las cosas para su propósito previsto. Lo último que desea es que sus validaciones tengan efectos secundarios.
chug2k
0

Algo que debería funcionar con todas las versiones ActiveRecordsin depender de las opciones o métodos de grabación activos que pueden o no existir.

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: use un "modelo de registro activo diferente" sobre la misma tabla

choonkeat
fuente
0

Para devoluciones de llamada personalizadas, use an attr_accessory an unlessen la devolución de llamada.

Defina su modelo de la siguiente manera:

class Person << ActiveRecord::Base

  attr_accessor :skip_after_save_callbacks

  after_save :do_something, unless: :skip_after_save_callbacks

end

Y luego, si necesita guardar el registro sin presionar las after_savedevoluciones de llamada que definió, establezca el skip_after_save_callbacksatributo virtual en 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
fuente
-5

No es la forma más limpia, pero podría ajustar el código de devolución de llamada en una condición que verifique el entorno de Rails.

if Rails.env == 'production'
  ...
James
fuente