2

I have a situation where when one of my models is saved MyModel I want to check a field, and trigger the same change in any other Model with the same some_key.

The code works fine, but its recursively calling the signals. As a result I am wasting CPU/DB/API calls. I basically want to bypass the signals during the .save(). Any suggestions?

class MyModel(models.Model):
    #bah
    some_field = #
    some_key = #

#in package code __init__.py 
@receiver(models_.post_save_for, sender=MyModel)
def my_model_post_processing(sender, **kwargs):
 # do some unrelated logic...
 logic = 'fun!  '


 #if something has changed... update any other field with the same id
 cascade_update = MyModel.exclude(id=sender.id).filter(some_key=sender.some_key)
 for c in cascade_update:
     c.some_field  = sender.some_field 
     c.save()
Nix
  • 57,072
  • 29
  • 149
  • 198

4 Answers4

8

Disconnect the signal before calling save and then reconnect it afterwards:

post_save.disconnect(my_receiver_function, sender=MyModel)
instance.save()
post_save.connect(my_receiver_function, sender=MyModel)
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    This scares me (in case something goes wrong...) ... but it will work. – Nix Dec 28 '11 at 22:48
  • What could go wrong? The only thing running between the disconnect and connect is the save, and if that didn't work, you wouldn't even be here. – Chris Pratt Dec 28 '11 at 22:51
  • Connecting it back up could go wrong... I think this is the approach, its just falls into the looks really scary category. – Nix Dec 29 '11 at 13:36
  • This is actually the sort of unofficially approved method of doing it. Many people, including myself, have code like this somewhere in their app, and it works flawlessly. There's never going to be a problem connecting a signal, unless you pass it invalid data in the first place (non-existent method, non-existent model as sender, etc.). – Chris Pratt Dec 29 '11 at 15:16
  • Disconnecting a signal is not a DRY and consistent solution. If another instance of the same model is saved at the same time, the signal won't be triggered. – Charlesthk May 01 '15 at 11:02
  • @Charlesthk True. This is an actual concern, because AFAIK the Python GIL will yield during save(), because I/O is happening with the DB. – Ben Simmons Aug 13 '15 at 00:24
5

Disconnecting a signal is not a DRY and consistent solution, such as using update() instead of save().

To bypass signal firing on your model, a simple way to go is to set an attribute on the current instance to prevent upcoming signals firing.

This can be done using a simple decorator that checks if the given instance has the 'skip_signal' attribute, and if so prevents the method from being called:

from functools import wraps

def skip_signal(signal_func):
    @wraps(signal_func)
    def _decorator(sender, instance, **kwargs):
        if hasattr(instance, 'skip_signal'):
            return None
        return signal_func(sender, instance, **kwargs)  
    return _decorator

Based on your example, that gives us:

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=MyModel)
@skip_signal()
def my_model_post_save(sender, instance, **kwargs):
    instance.some_field = my_value
    # Here we flag the instance with 'skip_signal'
    # and my_model_post_save won't be called again
    # thanks to our decorator, avoiding any signal recursion
    instance.skip_signal  = True
    instance.save()

Hope This helps.

Julio Marins
  • 10,039
  • 8
  • 48
  • 54
Charlesthk
  • 9,394
  • 5
  • 43
  • 45
3

A solution may be use update() method to bypass signal:

cascade_update = MyModel.exclude(
                     id=sender.id).filter(
                     some_key=sender.some_key).update(
                     some_field  = sender.some_field )

"Be aware that the update() method is converted directly to an SQL statement. It is a bulk operation for direct updates. It doesn't run any save() methods on your models, or emit the pre_save or post_save signals"

dani herrera
  • 48,760
  • 8
  • 117
  • 177
0

You could move related objects update code into MyModel.save method. No playing with signal is needed then:

class MyModel(models.Model):
    some_field = #
    some_key = #

    def save(self, *args, **kwargs):
        super(MyModel, self).save(*args, **kwargs)
        for c in MyModel.objects.exclude(id=self.id).filter(some_key=self.some_key):
            c.some_field = self.some_field 
            c.save()
demalexx
  • 4,661
  • 1
  • 30
  • 34
  • This would work, but we really want this logic in signal code. – Nix Dec 29 '11 at 13:35
  • In addition to suggested methods, what if add some attribute to a model before calling save, and in signal handler check it. If attribute set - skip processing. – demalexx Dec 29 '11 at 14:34