Django中字段更改触发的操作

时间:2009-07-29 01:42:07

标签: python django django-models

如果在我的某个模型中更改字段,我该如何处理?在这种特殊情况下,我有这个模型:

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

我希望创建单位,并且当状态从“设置”变为“活动”时,“已启动”字段将填充当前日期时间(以及其他内容)。

我怀疑需要一个模型实例方法,但是文档似乎没有太多关于以这种方式使用它们的说法。

更新:我已将以下内容添加到我的游戏课程中:

    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

8 个答案:

答案 0 :(得分:21)

已经回答了,但这里有一个使用信号,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, **kwargs):
        instance = kwargs.get('instance')
        created = kwargs.get('created')
        if instance.previous_state != instance.state or created:
            do_something_with_state_change()

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

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

答案 1 :(得分:15)

Django有一个名为signals的漂亮功能,它是在特定时间启动的有效触发器:

  • 调用模型的保存方法之前/之后
  • 调用模型的删除方法之前/之后
  • 发出HTTP请求之前/之后

阅读文档以获取完整信息,但您需要做的就是创建一个接收器功能并将其注册为信号。这通常在models.py。

中完成
from django.core.signals import request_finished

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

request_finished.connect(my_callback)

简单,嗯?

答案 2 :(得分:11)

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

棘手的部分是弄清楚该领域是否已经改变。查看此问题中的mixins和其他解决方案,以帮助您解决此问题:

答案 3 :(得分:8)

一种方法是为州添加一个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.

更新如果您希望在对模型实例进行更改时触发,则可以(而不是 of {{上面的1}}在set_state中使用__setattr__方法,如下所示:

Game

请注意,您不会在Django文档中特别发现这一点,因为它(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. )是一个标准的Python功能,记录为here,并且不是特定于Django的。

注意:不知道版本低于1.2的django,但使用__setattr__的代码无法正常工作,在尝试访问{__setattr__后,它会在第二个if之后失败{1}}。

我尝试了类似的操作,并尝试通过在self.state中强制初始化state__init__中的第一个)来解决此问题,但这会导致令人讨厌的意外行为。

我正在编辑而不是出于显而易见的原因进行评论,还有:我不会删除这段代码,因为它可能适用于较早(或未来?)版本的django,并且可能还有另一种解决方法{ {1}}我不知道的问题

答案 4 :(得分:4)

@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

答案 5 :(得分:0)

我的解决方案是将以下代码添加到应用的__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_dt字段将在sold字段更改时更改。

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

答案 6 :(得分:0)

使用脏污检测更改和覆盖保存方法 dirty field

我的上一条:Actions triggered by field change in 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)

答案 7 :(得分:0)

如果您使用 PostgreSQL,您可以创建一个触发器:

https://www.postgresql.org/docs/current/sql-createtrigger.html

示例:

CREATE TRIGGER check_update
    BEFORE UPDATE ON accounts
    FOR EACH ROW
    WHEN (OLD.balance IS DISTINCT FROM NEW.balance)
    EXECUTE FUNCTION check_account_update();