【问题标题】:Actions triggered by field change in Django由 Django 中的字段更改触发的操作
【发布时间】:2010-11-14 22:09:05
【问题描述】:

当我的一个模型中的字段发生更改时,我如何进行操作?在这种特殊情况下,我有这个模型:

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

【问题讨论】:

标签: python django django-models


【解决方案1】:

已回答,但这里是使用信号的示例,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)

【讨论】:

  • 对于那些仍在寻找上述问题的解决方案的人来说,这对我来说就像一个魅力。唯一的修改是我必须在我的 models.py 开头导入 post_save 和 post_init 函数:from django.db.models.signals import post_save, post_init
【解决方案2】:

基本上,你需要重写save方法,检查state字段是否改变,如果需要设置started,然后让模型基类完成持久化到数据库。

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

【讨论】:

  • 我不确定我对在 Django 的 ORM 中重写方法的感觉如何。 IMO,使用 django.db.models.signals.post_save 会更好地完成。
  • @cpharmston:我认为这是可以接受的:docs.djangoproject.com/en/dev/topics/db/models/… -- 当模型以外的实体想要被通知时,我使用信号,但在这种情况下,实体是模型本身,所以只是覆盖保存(这对于面向对象的方法来说是相当传统的)。
  • 我上次尝试时覆盖 save() 对批量管理操作不起作用(我认为是 1.0)
  • 在我看来使用信号更优雅,但在这种情况下,覆盖 save() 似乎是预期的事情。
  • FWIW 在进行批量更新时既不会调用 save 也不会调用信号。请参阅示例下方的注释docs.djangoproject.com/en/dev/ref/models/querysets/#update
【解决方案3】:

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)

简单吧?

【讨论】:

    【解决方案4】:

    一种方法是为状态添加一个设置器。就是普通的方法,没什么特别的。

    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.
    

    更新:如果您希望在每当模型实例发生更改时触发此功能,您可以(代替@987654323 @ 以上)在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 功能,记录在 here 中,并且不是特定于 Django 的。

    注意:不知道早于 1.2 的 django 版本,但是使用 __setattr__ 的这段代码不起作用,它会在尝试访问 self.state 时在第二个 if 之后失败。

    我尝试了类似的方法,并尝试通过在 __new__ 中强制初始化 state(首先在 __init__ 然后)来解决此问题,但这会导致令人讨厌的意外行为。

    出于明显的原因,我正在编辑而不是评论:我没有删除这段代码,因为它可能适用于较旧(或未来?)版本的 django,并且可能有另一种解决方法 @ 987654334@我不知道的问题

    【讨论】:

    • 是的,但我不清楚如何在从管理页面进行编辑时使用这种方法。
    【解决方案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
    

    【讨论】:

      【解决方案6】:

      我的解决方案是将以下代码放入应用程序的__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方法,并以新旧对象作为参数。

      【讨论】:

        【解决方案7】:

        使用 Dirty 检测变化并覆盖保存方法 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)
        

        【讨论】:

          【解决方案8】:

          如果你使用 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();
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-04-14
            • 2016-06-21
            • 2020-04-17
            • 2013-04-03
            • 1970-01-01
            • 2021-08-03
            • 1970-01-01
            相关资源
            最近更新 更多