Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent post_save recursion in Django?

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.

like image 981
user3595632 Avatar asked Sep 14 '16 02:09

user3595632


4 Answers

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)
like image 117
Andrey Shipilov Avatar answered Oct 09 '22 22:10

Andrey Shipilov


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

like image 27
Andrey But Avatar answered Oct 09 '22 22:10

Andrey But


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

like image 31
mevaka Avatar answered Oct 09 '22 22:10

mevaka


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
like image 43
knbk Avatar answered Oct 09 '22 20:10

knbk