识别 django post_save 信号中更改的字段

Posted

技术标签:

【中文标题】识别 django post_save 信号中更改的字段【英文标题】:Identify the changed fields in django post_save signal 【发布时间】:2016-08-11 16:49:54 【问题描述】:

我正在使用 django 的 post_save 信号在保存模型后执行一些语句。

class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.BooleanField()


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

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
        # do some stuff
        pass

现在我想根据mode字段的值是否发生变化来执行一条语句。

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
        # if value of `mode` has changed:
        #  then do this
        # else:
        #  do that
        pass

我查看了一些 SOF 线程和博客,但找不到解决方案。他们都试图使用 pre_save 方法或表单,这不是我的用例。 django 文档中的https://docs.djangoproject.com/es/1.9/ref/signals/#post-save 没有提到直接的方法。

下面链接中的答案看起来很有希望,但我不知道如何使用它。我不确定最新的 django 版本是否支持它,因为我使用ipdb 进行调试,发现instance 变量没有属性has_changed,如下面的答案所述。

Django: When saving, how can you check if a field has changed?

【问题讨论】:

我使用这个库:github.com/craigds/django-fieldsignals 【参考方案1】:

在您的模型的__init__ 上设置它,以便您可以访问它。

def __init__(self, *args, **kwargs):
    super(YourModel, self).__init__(*args, **kwargs)
    self.__original_mode = self.mode

现在您可以执行以下操作:

if instance.mode != instance.__original_mode:
    # do something useful

【讨论】:

小心。这种方法有缺点,正如在here 中详细讨论的那样。简而言之:它不是种族证明的,如果你有两个指向同一个数据库行的 python 实例也可能是错误的。 instance.mode != instance.__original_mode 将抛出一个 AttributeError,因为 __original_mode 将被破坏,如 here 所述。在这种情况下,正确的条件是instance.mode != instance._Mode__original_mode【参考方案2】:

通常重写保存方法比使用信号更好。

来自Two scoops of django: “使用信号作为最后的手段。”

我同意@scoopseven 的回答,即在初始化时缓存原始值,但如果可能的话,覆盖保存方法。

class Mode(models.Model):
    name = models.CharField(max_length=5)
    mode = models.BooleanField()
    __original_mode = None

    def __init__(self, *args, **kwargs):
        super(Mode, self).__init__(*args, **kwargs)
        self.__original_mode = self.mode

    def save(self, force_insert=False, force_update=False, *args, **kwargs):
        if self.mode != self.__original_mode:
            #  then do this
        else:
            #  do that

        super(Mode, self).save(force_insert, force_update, *args, **kwargs)
        self.__original_mode = self.mode

【讨论】:

在 Django 中使用信号有什么问题?它可以帮助您很好地创建工作流程。 @Hussain 虽然方法直接附加到模型并且您可以在一个地方看到它的行为,但信号可以放置在多个不同的应用程序中,并使调试成为一场噩梦并且代码的可读性降低。所以不是信号不好,而是方法更明显,如果可以的话,你最好坚持下去。 我目前正在寻找一种方法来检测外键值何时发生变化。上述方法有效,但可能导致数据库查询爆炸。 @BradenHolt 你找到解决方案了吗? Python 因堆栈溢出而崩溃 答案中的链接已损坏【参考方案3】:

如果您想比较保存操作之前和之后的状态,您可以使用pre_save 信号,它为您提供数据库更新后应该变为的实例,并且在 pre_save 中,您可以读取数据库中实例的当前状态并执行一些基于操作关于差异。

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


@receiver(pre_save, sender=MyModel)
def on_change(sender, instance: MyModel, **kwargs):
    if instance.id is None: # new object will be created
        pass # write your code here
    else:
        previous = MyModel.objects.get(id=instance.id)
        if previous.field_a != instance.field_a: # field will be updated
            pass  # write your code here

【讨论】:

我唯一的改变是previous = sender.objects.get(id=instance.id) 这种方法的问题是如果保存失败,所以实际上不会发生更改,但是您已经在 pre_save 中执行了一些会导致不一致的代码。只有在 post_save 中才能确认。 @RajatJain 可以在 pre_save 开始并在 post_save 之后提交的事务解决此问题吗? 我建议将 original_fields(即保存前的模型字段)作为属性存储在 pre_save 中,并将这些 original_fields 与 new_fields(我们现在在后期保存中的新模型字段)进行比较,看看有什么变化和添加你的逻辑【参考方案4】:

这是一个老问题,但我最近遇到了这种情况,我通过执行以下操作完成了它:

    class Mode(models.Model):
    
        def save(self, *args, **kwargs):
            if self.pk:
                # If self.pk is not None then it's an update.
                cls = self.__class__
                old = cls.objects.get(pk=self.pk)
                # This will get the current model state since super().save() isn't called yet.
                new = self  # This gets the newly instantiated Mode object with the new values.
                changed_fields = []
                for field in cls._meta.get_fields():
                    field_name = field.name
                    try:
                        if getattr(old, field_name) != getattr(new, field_name):
                            changed_fields.append(field_name)
                    except Exception as ex:  # Catch field does not exist exception
                        pass
                kwargs['update_fields'] = changed_fields
            super().save(*args, **kwargs)

这更有效,因为它可以捕获来自应用程序和 django-admin 的所有更新/保存。

【讨论】:

【参考方案5】:

post_save 方法中,您有kwargs 参数,它是一个字典并包含一些信息。您在kwargs 中有update_fields,它会告诉您哪些字段发生了变化。此字段存储为forzenset 对象。您可以像这样检查哪些字段发生了变化:

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
    if not created:
        for item in iter(kwargs.get('update_fields')):
            if item == 'field_name' and instance.field_name == "some_value":
               # do something here

但是这个解决方案有一个问题。例如,如果您的字段值为 10,并且您再次将此字段更新为 10,则此字段将再次位于 update_fields 中。

【讨论】:

另一个问题是update_fields没有自动填充。它包含您在save() 方法中显式传递的字段。【参考方案6】:

我来晚了,但它可以对其他人有所帮助。

我们可以为此制作自定义信号。

使用自定义信号我们可以轻松地做这些事情:

    帖子是否已创建 帖子修改与否 帖子已保存,但任何字段均未更改

   class Post(models.Model):
   # some fields 

自定义信号

**带参数的信号**

from django.dispatch import Signal, receiver
# provide arguments for your call back function
post_signal = Signal(providing_args=['sender','instance','change','updatedfields'])

用回调函数注册信号

# register your signal with receiver decorator 
@receiver(post_signal)
def post_signalReciever(sender,**kwargs):
    print(kwargs['updatedfields'])
    print(kwargs['change'])

从 post-admin 发送信号

我们从 Post admin 发送信号,并在实际修改时保存对象

#sending the signals 
class PostAdmin(admin.ModelAdmin):
   # filters or fields goes here 

   #save method 
   def save_model(self, request, obj, form, change):


    if not change and form.has_changed():  # new  post created
        super(PostAdmin, self).save_model(request, obj, form, change)
        post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
        print('Post created')
    elif change and form.has_changed(): # post is actually modified )
        super(PostAdmin, self).save_model(request, obj, form, change)
        post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
        print('Post modified')
    elif change and not form.has_changed() :
        print('Post not created or not updated only saved ')  

另请参阅:

Django Signals official doc

【讨论】:

【参考方案7】:

您可以在 django 信号中使用update_fields

@receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):

    # only update instance
    if not created:

        update_fields = kwargs.get('update_fields') or set()

        # value of `mode` has changed:
        if 'mode' in update_fields:
            # then do this
            pass
        else:
            # do that
            pass

【讨论】:

【参考方案8】:

这可以使用 instance._state.adding

来识别
if not instance._state.adding:
    # update to existing record
    do smthng

else:
    # new object insert operation
    do smthng

【讨论】:

这只是说明是在一行上发生了更新还是创建了新行,用户想通过比较以前的实例来具体了解哪些字段已更改以及更改是什么

以上是关于识别 django post_save 信号中更改的字段的主要内容,如果未能解决你的问题,请参考以下文章

未调用 post_save 信号

Django:如何在 post_save 信号中访问原始(未修改)实例

Django 从 post_save 信号访问 ManyToMany 字段

如何防止灯具与 django post_save 信号代码冲突?

Django post_save() 信号实现

Django post_save 信号和 celery 任务之间可能的竞争条件