2008-10-19 22 views
27

Si agrego una devolución de llamada after_save a un modelo ActiveRecord, y en esa devolución de llamada utilizo update_attribute para cambiar el objeto, la devolución de llamada se vuelve a llamar y entonces ocurre un 'desbordamiento de pila' (jeje, no pude resistir).Uso de la devolución de llamada after_save para modificar el mismo objeto sin activar la devolución de llamada nuevamente (recursión)

¿Es posible evitar este comportamiento, quizás deshabilitar la devolución de llamada durante su ejecución? ¿O hay otro enfoque?

Gracias!

Respuesta

13

Una solución consiste en establecer una variable en la clase, y comprobar su valor en el after_save.

  1. Primero verifícalo. (si var)
  2. Asignarlo a un valor "falso" antes de llamar a update_attribute.
  3. llamada update_attribute.
  4. Asignarlo a un valor 'verdadero'.
  5. final

De esta manera, sólo va a intentar salvar dos veces. Es probable que esto afecte a su base de datos dos veces, lo que puede o no ser deseable.

Tengo la vaga sensación de que hay algo incorporado, pero esta es una forma bastante infalible de evitar un punto específico de recursión en casi cualquier aplicación. También recomendaría revisar el código nuevamente, ya que es probable que lo que esté haciendo en after_save se realice antes de save. Hay momentos en que esto no es cierto, pero son bastante raros.

+0

¡Impresionante! También busqué un enfoque integrado, pero hasta ahora parece que no hay ninguno, pero sería genial si pudieras establecer una propiedad especial para decirle a Rails que suspenda temporalmente esa devolución de llamada ... tu enfoque es algo así como eso , ¡muchas gracias! – Ivan

6

Compruebe cómo se implementa update_attribute. Utilice el método de envío en su lugar:

send(name.to_s + '=', value) 
10

¿Podría usar la devolución de llamada before_save en su lugar?

3

Si usa before_save, puede modificar cualquier parámetro adicional antes de que se complete el guardado, lo que significa que no tendrá que llamar explícitamente a guardar.

2

Gracias chicos, el problema es que puedo actualizar otros objetos también (hermanos si se quiere) ... se olvidó de mencionar esa parte ...

Así before_save está fuera de la cuestión, ya que si el guardar falla todas las modificaciones a los otros objetos tendrían que revertirse y eso podría volverse desordenado :)

3

Este código ni siquiera intenta resolver problemas de subprocesamiento o concurrencia, al igual que los rieles propiamente dichos. Si necesita esa característica, ¡preste atención!

Básicamente, la idea es llevar un conteo al nivel de las llamadas recursivas de "guardar", y solo permitir after_save cuando salga del nivel superior. También querrá agregar el manejo de excepciones.

def before_save 
    @attempted_save_level ||= 0 
    @attempted_save_level += 1 
end 

def after_save 
    if (@attempted_save_level == 1) 
    #fill in logic here 

    save #fires before_save, incrementing save_level to 2, then after_save, which returns without taking action 

    #fill in logic here 

    end 
    @attempted_save_level -= 1 # reset the "prevent infinite recursion" flag 
end 
+0

¡Eso es inteligente, gracias! – Ivan

7

También puede ver el plugin Without_callbacks. Agrega un método a AR que le permite saltarse ciertas devoluciones de llamada para un bloque determinado. Ejemplo:

def your_after_save_func 
    YourModel.without_callbacks(:your_after_save_func) do 
    Your updates/changes 
    end 
end 
+0

No sabía nada de ese complemento, será útil, gracias. – Ivan

1

Tuve este problema también.Necesito guardar un atributo que depende de la identificación del objeto. Lo resuelto mediante el uso de invocación condicional para la devolución de llamada ...

Class Foo << ActiveRecord::Base 
    after_save :init_bar_attr, :if => "bar_attr.nil?" # just make sure this is false after the callback runs 

    def init_bar_attr  
     self.bar_attr = "my id is: #{self.id}"  

     # careful now, let's save only if we're sure the triggering condition will fail  
     self.save if bar_attr 
    end 
10

no vi esta respuesta, así que pensé que me gustaría añadir que en caso de que cualquier persona que busca ayuda sobre este tema. (La sugerencia de ScottD's sin_callbacks está cerca.)

ActiveRecord proporciona update_without_callbacks para esta situación, pero es un método privado. Use enviar para tener acceso a él de todos modos. Estar en una devolución de llamada para el objeto que está guardando es exactamente la razón para usar esto.

También hay otro SO hilo aquí que cubre esta bastante bien: How can I avoid running ActiveRecord callbacks?

1

A veces esto es debido a que no especificando attr_accessible en modelos. Cuando update_attribute desea editar los atributos, si descubre que no son accesibles y crea nuevos objetos en su lugar. Al guardar los nuevos objetos, entrará en un ciclo interminable.

0

que tenía una necesidad de gsub los nombres de ruta en un bloque de texto cuando su registro se ha copiado en un contexto diferente:

attr_accessor :original_public_path 
after_save :replace_public_path, :if => :original_public_path 

private 

def replace_public_path 
    self.overview = overview.gsub(original_public_path, public_path) 
    self.original_public_path = nil 

    save 
end 

La clave para detener la recursión fue asignar el valor del atributo y a continuación, establezca el atributo en nil para que no se cumpla la condición :if en el siguiente guardado.

0

Puede utilizar after_save en asociación con if como sigue:

after_save :after_save_callback, if: Proc.new { 
               //your logic when to call the callback 
               } 

o

after_save :after_save_callback, if: :call_if_condition 

def call_if_condition 
    //condition for when to call the :after_save_callback method 
end 

call_if_condition es un método. Defina el escenario cuando llamar al after_save_callback en ese método

Cuestiones relacionadas