Django中由字段更改触发的操作

54

当我的一个模型中的字段发生改变时,如何让操作发生?在这个特定情况下,我有这个模型:

class Game(models.Model):
    STATE_CHOICES = (
        ('S', 'Setup'),
        ('A', 'Active'),
        ('P', 'Paused'),
        ('F', 'Finished')
        )
    name = models.CharField(max_length=100)
    owner = models.ForeignKey(User)
    created = models.DateTimeField(auto_now_add=True)
    started = models.DateTimeField(null=True)
    state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')

当状态从设置(Setup)转为激活(Active)时,我想创建一个Units,并将“started”字段填充为当前日期时间(以及其他内容)。

我怀疑需要使用模型实例方法,但文档似乎没有对此使用方式做出很多解释。

更新: 我已经将以下代码添加到我的Game类中:

    def __init__(self, *args, **kwargs):
        super(Game, self).__init__(*args, **kwargs)
        self.old_state = self.state

    def save(self, force_insert=False, force_update=False):
        if self.old_state == 'S' and self.state == 'A':
            self.started = datetime.datetime.now()
        super(Game, self).save(force_insert, force_update)
        self.old_state = self.state

我已根据您的评论更新了我的答案。 - Vinay Sajip
2
django-model-utils实现了一个监视器字段,对于您的启动字段案例非常有用:https://django-model-utils.readthedocs.org/en/latest/fields.html#monitorfield - jdcaballerov
8个回答

46

已有答案,但以下是使用信号、post_init 和 post_save 的示例。

from django.db.models.signals import post_save, post_init

class MyModel(models.Model):
    state = models.IntegerField()
    previous_state = None

    @staticmethod
    def post_save(sender, instance, created, **kwargs):
        if instance.previous_state != instance.state or created:
            do_something_with_state_change()

    @staticmethod
    def remember_state(sender, instance, **kwargs):
        instance.previous_state = instance.state

post_save.connect(MyModel.post_save, sender=MyModel)
post_init.connect(MyModel.remember_state, sender=MyModel)

2
对于那些仍在寻找解决上述问题的人,这对我来说非常有效。唯一的编辑是我不得不在models.py的开头导入post_save和post_init函数:from django.db.models.signals import post_save, post_init - Sam
似乎有点晚了,但是我使用上述代码达到了预期的结果。但是,当我使用ORM中的“only”来查询对象时,比如MyModel.objects.only('id'),它会引发递归错误(RecursionError)。我猜想这是由post_init引起的。 - stick Cour
众所周知,这个问题会在django<=2.2中出现,可能已经在更新版本中得到了修复。 - stick Cour

23

基本上,您需要覆盖save方法,检查state字段是否已更改,如有必要,设置started,然后让模型基类完成持久化到数据库的操作。

棘手的部分是找出字段是否已更改。查看此问题中的 mixins 和其他解决方案以帮助您:


1
我不确定如何看待在Django的ORM中覆盖方法。在我看来,使用django.db.models.signals.post_save会更好地实现这一点。 - chuckharmston
4
我认为这很可行:http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods——当除了模型之外的实体想要被通知时,我使用信号,但在这种情况下,实体是模型本身,所以只需覆盖save方法(这在面向对象的方法中相当常见)。 - ars
1
上次我尝试重写save()方法来处理批量管理操作时,好像没有生效(我想是1.0版本)。 - Luper Rouch
使用信号对我来说更加优雅,但在这种情况下,覆盖save()似乎是预期的事情。 - Jeff Bradberry
5
对于批量更新操作,save方法和信号都不会被调用。请参见http://docs.djangoproject.com/en/dev/ref/models/querysets/#update中示例下方的注意事项。FWIW是"For What It's Worth"的缩写,表示附带说明这一点并非特别重要。 - Gabriel Grant

20

Django有一个很棒的功能叫做信号, 它们实际上是在特定时间触发的触发器:

  • 在调用模型的保存方法之前/之后
  • 在调用模型的删除方法之前/之后
  • 在进行HTTP请求之前/之后

阅读完整信息的文档,但你所需要做的就是创建一个接收函数并将其注册为信号。这通常在models.py中完成。

from django.core.signals import request_finished

def my_callback(sender, **kwargs):
    print "Request finished!"

request_finished.connect(my_callback)

简单,对吧?


9

一种方法是添加一个状态的setter。这只是一个普通的方法,没有什么特别之处。

class Game(models.Model):
   # ... other code

    def set_state(self, newstate):
        if self.state != newstate:
            oldstate = self.state
            self.state = newstate
            if oldstate == 'S' and newstate == 'A':
                self.started = datetime.now()
                # create units, etc.
更新: 如果您希望每当对模型实例进行更改时触发此操作,您可以(set_state以上的方法)在Game中使用一个类似于以下内容的__setattr__方法:
def __setattr__(self, name, value):
    if name != "state":
        object.__setattr__(self, name, value)
    else:
        if self.state != value:
            oldstate = self.state
            object.__setattr__(self, name, value) # use base class setter
            if oldstate == 'S' and value == 'A':
                self.started = datetime.now()
                # create units, etc.
请注意,在 Django 文档中你不会特别找到这个(__setattr__)内容,因为它是 Python 的标准功能,在此有文档记录,与 Django 无关。 注意:不了解早于 1.2 版本的 Django,但是使用 __setattr__ 的此代码将无法工作,在尝试访问 self.state 之后的第二个 if 就会失败。 我尝试了类似的东西,并试图通过强制初始化 state(首先在 __init__ 中,然后在 __new__ 中)来解决这个问题,但这将导致令人讨厌的意外行为。 出于明显的原因,我正在编辑而不是评论。此外:我不删除这段代码,因为也许它可以在旧版(或未来?)的 Django 中工作,并且可能还有另一种解决 self.state 问题的方法我不知道。

1
是的,但我不清楚如何使这样的方法在编辑时被使用,比如从管理员页面。 - Jeff Bradberry

5

@dcramer提出了一个我认为更优雅的解决方案,用于解决这个问题。

https://gist.github.com/730765

from django.db.models.signals import post_init

def track_data(*fields):
    """
    Tracks property changes on a model instance.

    The changed list of properties is refreshed on model initialization
    and save.

    >>> @track_data('name')
    >>> class Post(models.Model):
    >>>     name = models.CharField(...)
    >>> 
    >>>     @classmethod
    >>>     def post_save(cls, sender, instance, created, **kwargs):
    >>>         if instance.has_changed('name'):
    >>>             print "Hooray!"
    """

    UNSAVED = dict()

    def _store(self):
        "Updates a local copy of attributes values"
        if self.id:
            self.__data = dict((f, getattr(self, f)) for f in fields)
        else:
            self.__data = UNSAVED

    def inner(cls):
        # contains a local copy of the previous values of attributes
        cls.__data = {}

        def has_changed(self, field):
            "Returns ``True`` if ``field`` has changed since initialization."
            if self.__data is UNSAVED:
                return False
            return self.__data.get(field) != getattr(self, field)
        cls.has_changed = has_changed

        def old_value(self, field):
            "Returns the previous value of ``field``"
            return self.__data.get(field)
        cls.old_value = old_value

        def whats_changed(self):
            "Returns a list of changed attributes."
            changed = {}
            if self.__data is UNSAVED:
                return changed
            for k, v in self.__data.iteritems():
                if v != getattr(self, k):
                    changed[k] = v
            return changed
        cls.whats_changed = whats_changed

        # Ensure we are updating local attributes on model init
        def _post_init(sender, instance, **kwargs):
            _store(instance)
        post_init.connect(_post_init, sender=cls, weak=False)

        # Ensure we are updating local attributes on model save
        def save(self, *args, **kwargs):
            save._original(self, *args, **kwargs)
            _store(self)
        save._original = cls.save
        cls.save = save
        return cls
    return inner

1

我的解决方案是将以下代码放入应用程序的__init__.py中:

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


@receiver(signals.pre_save)
def models_pre_save(sender, instance, **_):
    if not sender.__module__.startswith('myproj.myapp.models'):
        # ignore models of other apps
        return

    if instance.pk:
        old = sender.objects.get(pk=instance.pk)
        fields = sender._meta.local_fields

        for field in fields:
            try:
                func = getattr(sender, field.name + '_changed', None)  # class function or static function
                if func and callable(func) and getattr(old, field.name, None) != getattr(instance, field.name, None):
                    # field has changed
                    func(old, instance)
            except:
                pass

并且在我的模型类中添加<field_name>_changed静态方法:

class Product(models.Model):
    sold = models.BooleanField(default=False, verbose_name=_('Product|sold'))
    sold_dt = models.DateTimeField(null=True, blank=True, verbose_name=_('Product|sold datetime'))

    @staticmethod
    def sold_changed(old_obj, new_obj):
        if new_obj.sold is True:
            new_obj.sold_dt = timezone.now()
        else:
            new_obj.sold_dt = None

如果 sold 字段发生变化,那么 sold_dt 字段也会发生变化。

模型中定义的任何字段的更改都将触发 <field_name>_changed 方法,并将旧对象和新对象作为参数。


0

0

使用Dirty来检测更改并覆盖保存方法 dirty field

我的先前回答:Django中由字段更改触发的操作

class Game(DirtyFieldsMixin, models.Model):
    STATE_CHOICES = (
        ('S', 'Setup'),
        ('A', 'Active'),
        ('P', 'Paused'),
        ('F', 'Finished')
        )
    state = models.CharField(max_length=1, choices=STATE_CHOICES, default='S')

    def save(self, *args, **kwargs):
        if self.is_dirty():
            dirty_fields = self.get_dirty_fields()
            if 'state' in dirty_fields:
                Do_some_action()
        super().save(*args, **kwargs)

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