如何防止 Django 中的 post_save 递归?

问题描述 投票:0回答:6

我在Django中使用

signal
时遇到了一些问题。

post_save
发生递归,因为
instance.save()
在函数内部。

但奇怪的是只有一种情况发生递归。

  1. 不发生递归的情况。

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()

当我使用

Product
创建
Product.objects.create()
时,它不会发生递归。

  1. 案例发生递归

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()

本例出现递归错误:

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

我认为这两个案例具有相同的结构,但只有一个案例发生递归。

不知道为什么。需要帮助,谢谢。

django django-signals
6个回答
17
投票

保存前断开信号,然后重新连接。 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)

15
投票

如果你想避免在

post_save
信号中递归,只需使用
Model.objects.filter(id=id).update(object=object)


14
投票

只需使用

pre_save
,你不需要再次使用里面的
.save()
方法。


6
投票

在第二种情况下,您正在将

instance.color
的数据库值与显示值进行比较。这些永远不会匹配。您应该检查数据库值:

@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()

同样,您应该将默认值设置为数据库值,即

default = 'black'

在您的原始代码中,所有检查都会失败,并且

instance.price
永远不会更新为非空值。调用
instance.save()
会再次触发
post_save
信号,
not instance.price
仍然为真,再次保存实例,不设置价格。这就是你看到的无限递归。

第一个例子中,

slug
一直设置为非空值,所以第二次触发
post_save
信号时,
if not instance.slug
检查会失败,第三次不会保存实例时间。

在这两种情况下,如果未设置 slug/price,您将至少保存实例两次。为防止这种情况,您可以使用

pre_save
信号。您不必在信号处理程序中再次保存实例:

@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

0
投票

你可以这样做

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

0
投票

我使用 .save() 递归触发我的接收函数时遇到了同样的问题。

我通过使用 update() 方法而不是 save() 解决了这个问题。这是我的代码:

@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'])

精华部分在这里:

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']})

事实上,我首先迭代我的所有字段(在顾问模型中)并将顾问实例的属性设置为匹配用户实例属性的值(在用户模型中)。

然后,进入更新方法,我正在解压一个字典理解,它将使用每个字段的名称和这个相关字段的值。

由于更新方法来自 QuerySet 类,不能从实例调用,我不得不过滤 Consultants 模型,其中主键设置为我的顾问实例的 ID。由此,我能够使用 update() 方法

那个的结论是我现在可以通过更新顾问模型自动更新我的用户表,反之亦然。

© www.soinside.com 2019 - 2024. All rights reserved.