How can I prevent post_save recursion in Django?
Asked Answered
B

6

11

I have some problems when using signal in Django.

post_save occurs recursion because of instance.save() inside of function.

But strange thing is only one case occurs recursion.

  1. Case not occuring recursion.

models.py

class Product(TimeStampedModel):
    name = models.CharField(max_length=120)
    slug = models.SlugField(null=True, blank=True)
    description = models.CharField(max_length=400, blank=True)
    is_active = models.BooleanField(default=True)

    objects = ProductManager()

    class Meta:
        ordering = ('-created',)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse(
            "products:product_detail",
            kwargs={
                "slug": self.slug,
            }
        )

signals.py

@receiver(post_save, sender=Product)
def post_save_product(sender, instance, created, **kwargs):
    if not instance.slug:
        instance.slug = slugify(instance.name, allow_unicode=True)
        instance.save()

When I create Product using Product.objects.create() it doesn't occur recursion.

  1. Case occuring recursion

models.py

class Variation(TimeStampedModel):
    COLOR_CHOICES = (
        ('black', '흑백'),
        ('single', '단색'),
        ('multi', '컬러'),
    )
    price = models.DecimalField(
        decimal_places=2,
        max_digits=15,
        blank=True,
        null=True,
    )
    product = models.ForeignKey(Product)
    color = models.CharField(
        max_length=10,
        choices=COLOR_CHOICES,
        default='흑백'
    )
    is_active = models.BooleanField(default=True)

    class Meta:
        ordering = ('product',)

    def __str__(self):
        return "{product} - {color}".format(
            product=self.product,
            color=self.color
        )

signals.py

@receiver(post_save, sender=Variation)
def post_save_variation(sender, instance, created, **kwargs):
    if not instance.price:
        if instance.color == '흑백':
            instance.price = 40000
        elif instance.color == '단색':
            instance.price = 50000
        elif instance.color == '컬러':
            instance.price = 60000
        instance.save()

This case occurs recursion errors:

File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 708, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 745, in save_base
    update_fields=update_fields, raw=raw, using=using)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 192, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/Chois/Dropbox/Workspace/django/spacegraphy-project/spacegraphy/products/signals/post_save.py", line 24, in post_save_variation
    instance.save()
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 708, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 745, in save_base
    update_fields=update_fields, raw=raw, using=using)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/dispatch/dispatcher.py", line 192, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/Chois/Dropbox/Workspace/django/spacegraphy-project/spacegraphy/products/signals/post_save.py", line 24, in post_save_variation
    instance.save()
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 708, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 736, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/base.py", line 796, in _save_table
    base_qs = cls._base_manager.using(using)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/manager.py", line 122, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/manager.py", line 214, in get_queryset
    return self._queryset_class(model=self.model, using=self._db, hints=self._hints)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/query.py", line 171, in __init__
    self.query = query or sql.Query(self.model)
  File "/Users/Chois/.pyenv/versions/spacegraphy/lib/python3.5/site-packages/django/db/models/sql/query.py", line 155, in __init__
    self.where = where()
RecursionError: maximum recursion depth exceeded while calling a Python object

I think those two cases have same structure but only one case occurs recursion.

Have no idea why. Need helps, Thanks.

Bust answered 14/9, 2016 at 2:28 Comment(0)
D
17

Disconnect the signal before save, then connect again. https://docs.djangoproject.com/en/1.10/topics/signals/#disconnecting-signals

def post_save_product(sender, instance, **kwargs):
    post_save.disconnect(post_save_product, sender=sender)
    instance.do_stuff()
    instance.save()
    post_save.connect(post_save_product, sender=sender)
post_save.connect(post_save_product, sender= Product)
Damiandamiani answered 14/9, 2016 at 5:28 Comment(2)
As noted in another SO answer, this solution is dangerous: concurrent request can falsely invoke or not invoke the signal (while it's deactivated). Do not use this method if you want to avoid big debugging headaches!!Berger
It's a clever solution with the field checking to avoid save again and again and go to the infinite recursive loop. Thank you for the idea of the trick!Alkalify
E
16

If you want to avoid recursion in post_save signal, just use Model.objects.filter(id=id).update(object=object)

Exacting answered 30/1, 2018 at 0:40 Comment(3)
MyModel.objects.filter(id=instance.id).update(instance=instance)Mailer
MyModel.objects.filter(id=instance.id).update(field_named=value)Perihelion
This is by far the best solution detailed. Indeed, not only does it satisfy the requirement not to fire an unwanted signal. It is also the cleanest, fastest way to update a record. It also avoids hitting race conditions.Linnette
L
15

Just use pre_save , you don't need to use .save() method inside it again.

Lytic answered 16/5, 2019 at 5:19 Comment(0)
I
6

In the second case, you are comparing the database value of instance.color to the display value. These will never match. You should check against the database value instead:

@receiver(post_save, sender=Variation)
def post_save_variation(sender, instance, created, **kwargs):
    if not instance.price:
        if instance.color == 'black':
            instance.price = 40000
        elif instance.color == 'single':
            instance.price = 50000
        elif instance.color == 'multi':
            instance.price = 60000
        instance.save()

Similarly you should set the default to the database value, i.e. default = 'black'.

In your original code, all the checks will fail, and instance.price is never updated to a non-empty value. The call to instance.save() will trigger the post_save signal again, not instance.price is still true, and the instance is saved again without setting the price. This is the infinite recursion you're seeing.

In the first example, the slug is always set to a non-empty value, so when the post_save signal is triggered the second time, the if not instance.slug check will fail, and the instance will not be saved a third time.

In both cases you're saving the instance at least twice if the slug/price is not set. To prevent this, you can use the pre_save signal. You won't have to save the instance again in the signal handler:

@receiver(pre_save, sender=Variation)
def pre_save_variation(sender, instance, **kwargs):
    if not instance.price:
        if instance.color == 'black':
            instance.price = 40000
        elif instance.color == 'single':
            instance.price = 50000
        elif instance.color == 'multi':
            instance.price = 60000
Interlineate answered 14/9, 2016 at 9:16 Comment(0)
L
0

You can do something like this

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

@receiver(post_save, sender=MyModel)
def my_model_post_save(sender, instance, **kwargs):
    if kwargs.get('raw', False):  # skip signal for objects created during fixture loading
        return
    if not getattr(instance, '_skip_signal', False):
        try:
            instance._skip_signal = True
            # Your post_save code here
            instance.save()
        finally:
           instance._skip_signal = False
Larch answered 5/4, 2023 at 16:11 Comment(0)
W
0

I had the same problem by using .save() that recursively triggers my receiver function.

I fixed this problem by using update() method instead of save(). Here is my code:

@receiver(post_save, sender=User)
def update_consultant_fields(instance, created, **kwargs):
    if not created:
        try:
            consultant = instance.as_consultant
        except Consultants.DoesNotExist:
            consultant = Consultants.objects.create(user=instance)
        for field in ['first_name', 'last_name', 'email']:
            setattr(consultant, field, getattr(instance, field, None))
        Consultants.objects.filter(pk=consultant.id).update(**{field: getattr(instance, field) for field in ['first_name', 'last_name', 'email']})


@receiver(post_save, sender=Consultants)
def update_user_fields(instance, created, **kwargs):
    if not created:
        try:
            user = instance.user
        except User.DoesNotExist:
            return
        for field in ['first_name', 'last_name', 'email']:
            setattr(user, field, getattr(instance, field, None))
        user.save(update_fields=['first_name', 'last_name', 'email'])

The essential part is here:

for field in ['first_name', 'last_name', 'email']:
            setattr(consultant, field, getattr(instance, field, None))
        Consultants.objects.filter(pk=consultant.id).update(**{field: getattr(instance, field) for field in ['first_name', 'last_name', 'email']})

Indeed, I am first iterating into all my fields (in Consultants model) and setting consultant instance's attribute to the value of matching user instance's attribute (in User model).

Then, into the update method, I am unpacking a dictionary comprehension that will takes the name of each field with the value of this related_field.

Since update method is from the QuerySet class and can not be called from an instance, I had to filter Consultants model where the primary key is set to the id of my consultant instance. From this, I was able to use update() method

The conclusion of that is that I am now able to automatically update my user table by updating Consultants model and vice versa.

Wits answered 12/4, 2023 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.