在Django的post_save信号中识别已更改的字段

87

我正在使用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字段的值是否更改来执行一条语句。
结果:

现在,我想根据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文档的post-save信号没有提到直接完成此操作的方法。

下面链接中的答案似乎很有前途,但我不知道如何使用它。我不确定最新的django版本是否支持它,因为我使用了`ipdb`来调试此问题,并发现`instance`变量没有像下面的答案中提到的`has_changed`属性。

Django:保存时,如何检查字段是否已更改?


我使用这个库:https://github.com/craigds/django-fieldsignals - guettli
8个回答

65

如果您想比较保存操作前后的状态,则可以使用 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

3
我对此的唯一更改将是 previous = sender.objects.get(id=instance.id) - Micheal J. Roberts
5
这种方法的问题在于,如果保存操作失败,那么实际上不会发生任何更改,但是您已经在 pre_save 中执行了某些代码,这可能导致不一致性。只有在 post_save 中才能确认是否成功保存。 - Rajat Jain
@RajatJain,一个在pre_save开始并在post_save后提交的事务能解决这个问题吗? - Mikhail M.
2
我建议在 pre_save 中将原始字段(即保存前的模型字段)存储为属性,并将这些 original_fields 与 new_fields(现在我们处于 post_save 状态的新模型字段)进行比较,查看有哪些更改并添加您的逻辑。 - Rajat Jain
1
@RajatJain,我该如何在post_save中访问original_fields,因为它们存储在pre_save中。 - EdG
对于任何可能无法正常工作的人,如果您的字段是ManyToMany,则需要不同的信号:m2m_changed - Akaisteph7

59

通常最好重写保存方法而不是使用信号。

来自《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

如果需要信号,请更新

如果您需要一个解耦的应用程序或者无法简单地覆盖save()方法,您可以使用pre_save信号来'监视'以前的字段。

@receiver(pre_save, sender=Mode)
def check_previous_mode(sender, instance, *args, **kwargs):
    original_mode = None
    if instance.id:
        original_mode = Mode.objects.get(pk=instance.id).mode
    
    if instance.mode != original_mode:
        #  then do this
    else:
        #  do that

这个问题在于你在保存之前做了一些更改,所以如果 save() 出现问题,你之后可能会面临一些问题。为了解决这个问题,你可以在 pre_save 中存储原始值并在 post_save 中使用。

@receiver(pre_save, sender=Mode)
def cache_previous_mode(sender, instance, *args, **kwargs):
    original_mode = None
    if instance.id:
        original_mode = Mode.objects.get(pk=instance.id).mode
    
    instance.__original_mode = original_mode:

@receiver(post_save, sender=Mode)
def post_save_mode_handler(sender, instance, created, **kwargs):
    if instance.__original_mode != instance.original_mode:
        #  then do this
    else:
        #  do that

信号的问题以及这种方法也存在的问题是需要额外查询以检查先前的值。


14
在Django中使用信号有什么问题吗?它可以帮助你很好地创建一个工作流程。 - Hussain
15
@Hussain,虽然将方法直接附加到模型上可以在一个地方看到其行为,但信号可以放置在多个不同的应用程序中,这会导致调试困难和代码难以阅读。因此,并非信号不好,而是方法更明显,如果可能的话最好坚持使用它们。 - r_black
1
我目前正在寻找一种方法来检测外键值的更改。上面描述的方法可行,但可能会导致数据库查询量激增。 - Braden Holt
@BradenHolt 你找到解决方法了吗?Python 因堆栈溢出而崩溃。 - viam0Zah
答案中的链接已经失效。 - Alan Evangelista

26

在您的模型的__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

12
注意,这种方法有缺点,详见此处的讨论。简而言之,它不是线程安全的,如果有两个 Python 实例指向同一数据库行,也可能出现错误。 - Arnaud P
2
instance.mode != instance.__original_mode 会抛出 AttributeError 异常,因为 __original_mode 会被破坏,具体描述请参见这里。在这种情况下,正确的条件语句应该是 instance.mode != instance._Mode__original_mode - t-payne

24

这是一个老问题,但我最近遇到了这种情况,并通过以下方式完成了它:

    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管理界面的更新/保存。

10

post_save方法中,您有一个kwargs参数,它是一个字典并保存了一些信息。您可以在kwargs中使用update_fields来告诉您哪些字段发生了更改。这些字段被存储为frozenset对象。您可以像这样检查哪些字段已更改:

post_save方法中,您有一个kwargs参数,它是一个字典,并保存了一些信息。您可以在kwargs中使用update_fields来告诉您哪些字段发生了更改。这些字段被存储为frozenset对象。您可以像这样检查哪些字段已更改:

@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中。


21
另一个问题是update_fields不会自动填充,它包含你在save()方法中明确传递的字段。 - ahmadkarimi12

8

我来迟了,但这可能会对其他人有所帮助。

我们可以为此创建自定义信号。

使用自定义信号,我们可以轻松完成以下操作:

  1. 是否已创建帖子
  2. 是否修改了帖子
  3. 帖子已保存但未更改任何字段

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

从后台发送信号

我们从后台发送信号,并在对象实际修改时保存该对象。

#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信号官方文档


-2

可以使用 instance._state.adding 进行识别

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

else:
    # new object insert operation
    do smthng

1
这只是说明行是否更新或新行已创建,用户想要具体知道哪些字段已更改以及更改内容,通过比较先前的实例来确定。 - Pavan Varyani

-4

在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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接