【问题标题】:django - comparing old and new field value before savingdjango - 在保存之前比较新旧字段值
【发布时间】:2014-06-15 04:16:39
【问题描述】:

我有一个 django 模型,我需要在保存之前比较字段的新旧值。

我已经尝试过save() 继承和pre_save 信号。它被正确触发,但我找不到实际更改字段的列表,也无法比较新旧值。有办法吗?我需要它来优化预保存操作。

谢谢!

【问题讨论】:

  • save 方法中从数据库中获取旧值而不是检查每个字段是否相等呢?
  • 你想要什么样的优化?
  • @J0HN 在获取、比较和保存过程中更改的 da 之间的值。
  • 我认为,它可以而且必须降低性能?

标签: python django django-signals


【解决方案1】:

有一种非常简单的 django 方法。

像这样“记住”模型初始化中的值:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.initial_parametername = self.parametername
    ---
    self.initial_parameternameX = self.parameternameX

现实生活中的例子:

上课时:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.__important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
    for field in self.__important_fields:
        setattr(self, '__original_%s' % field, getattr(self, field))

def has_changed(self):
    for field in self.__important_fields:
        orig = '__original_%s' % field
        if getattr(self, orig) != getattr(self, field):
            return True
    return False

然后在modelform中保存方法:

def save(self, force_insert=False, force_update=False, commit=True):
    # Prep the data
    obj = super(MyClassForm, self).save(commit=False)

    if obj.has_changed():

        # If we're down with commitment, save this shit
        if commit:
            obj.save(force_insert=True)

    return obj

【讨论】:

  • 我更喜欢 Odif 的方式,因为我需要触发没有表单的模型的操作(在更改来自 api 或来自管理站点之后)
  • 何时调用__init__?它是否仅适用于初始创建或后续更新?
  • 每次创建模型实例时都会调用 Init。如果实例在其生命周期内多次更新,则 __init__ 仅在开始时被调用。
  • 如果模型用savebulk_create保存在其他地方,这个不包括
  • 小心这种方法。我在尝试执行时发现了很多问题(包括 python 崩溃或达到递归限制),即 Model.objects.delete() 如果我想缓存的字段是外键(即使您尝试将 self._old_<field>_id 存储为整数
【解决方案2】:

最好在 ModelForm 级别 执行此操作。

您可以在保存方法中获得比较所需的所有数据:

  1. self.data:传递给表单的实际数据。
  2. self.cleaned_data :验证后清理的数据,包含可以保存在模型中的数据
  3. self.changed_data :已更改的字段列表。如果没有任何变化,这将为空

如果您想在模型级别执行此操作,则可以按照 Odif 的答案中指定的方法。

【讨论】:

  • 我同意你的回答,self.instance 也可以在这个问题上使用。
  • @AlexeyKuleshevich 同意,但只表单的_post_clean (is_valid->errors->full_clean->_post_clean) 之后,将更新实例以包含新值。访问form.clean_fieldname()form.clean() 似乎没问题,只要这是他们的第一次通话。
  • 这行得通,但前提是您使用表单保存,但情况并非总是如此。
  • 是的,没错。如果您不使用表单,则无法执行此操作。但是使用表单是理想的方式。
  • self.changed_data 对我来说是新的
【解决方案3】:

您也可以为此使用FieldTracker 中的FieldTracker

  1. 只需将跟踪器字段添加到您的模型:

    tracker = FieldTracker()
    
  2. 现在您可以在 pre_save 和 post_save 中使用:

    instance.tracker.previous('modelfield')     # get the previous value
    instance.tracker.has_changed('modelfield')  # just check if it is changed
    

【讨论】:

  • 是的,我就是喜欢这里的干净程度……又是一条要求!
  • 但是这个tracker字段是表中的真实列?还是只是一个假字段?
  • @toscanelli,它不会向表中添加列。
  • 提醒一下,确保进行迁移并再次迁移,否则会出现属性错误,例如:'tracker' not found.
  • 这个很诱人,但有人报告了性能问题here。并且团队没有更新或跟进。所以,查看tracker.py的源代码。它看起来像很多作品和信号。因此,它是否值得 - 或者用例太有限以至于您只需要跟踪一两个字段。
【解决方案4】:

Django 1.8+及以上(包括Django 2.x和3.x),有一个from_db类方法,可用于自定义从数据库加载时创建模型实例。

注意:如果使用此方法,NO会额外查询数据库。

这里是官方文档Model instance - Customize model loading的链接

from django.db import Model

class MyClass(models.Model):
    
    @classmethod
    def from_db(cls, db, field_names, values):
        instance = super().from_db(db, field_names, values)
        
        # save original values, when model is loaded from database,
        # in a separate attribute on the model
        instance._loaded_values = dict(zip(field_names, values))
        
        return instance

所以现在原始值在模型的_loaded_values 属性中可用。您可以在 save 方法中访问此属性,以检查某些值是否正在更新。

class MyClass(models.Model):
    field_1 = models.CharField(max_length=1)

    @classmethod
    def from_db(cls, db, field_names, values):
        ...
        # use code from above

    def save(self, *args, **kwargs):

        # check if a new db row is being added
        # When this happens the `_loaded_values` attribute will not be available
        if not self._state.adding:

            # check if field_1 is being updated
            if self._loaded_values['field_1'] != self.field_1:
                # do something

        super().save(*args, **kwargs)
            
            

【讨论】:

  • 这很酷,但它不会为您提供 M2M 关系。例如,如果您尝试跟踪用户关联的组的更改,则似乎没有任何方法可以使用此技术。
【解决方案5】:

我的用例是,每当某个字段更改其值时,我需要在模型中设置一个非规范化值。但是,由于被监控的字段是 m2m 关系,我不想在调用 save 时进行数据库查找以检查非规范化字段是否需要更新。所以,我写了这个小混音(使用@Odif Yitsaeb 的答案作为灵感),以便仅在必要时更新非规范化字段。

class HasChangedMixin(object):
    """ this mixin gives subclasses the ability to set fields for which they want to monitor if the field value changes """
    monitor_fields = []

    def __init__(self, *args, **kwargs):
        super(HasChangedMixin, self).__init__(*args, **kwargs)
        self.field_trackers = {}

    def __setattr__(self, key, value):
        super(HasChangedMixin, self).__setattr__(key, value)
        if key in self.monitor_fields and key not in self.field_trackers:
            self.field_trackers[key] = value

    def changed_fields(self):
        """
        :return: `list` of `str` the names of all monitor_fields which have changed
        """
        changed_fields = []
        for field, initial_field_val in self.field_trackers.items():
            if getattr(self, field) != initial_field_val:
                changed_fields.append(field)

        return changed_fields

【讨论】:

    【解决方案6】:

    类似的方法也可以:

    class MyModel(models.Model):
        my_field = fields.IntegerField()
    
        def save(self, *args, **kwargs):
           # Compare old vs new
           if self.pk:
               obj = MyModel.objects.values('my_value').get(pk=self.pk)
               if obj['my_value'] != self.my_value:
                   # Do stuff...
                   pass
           super().save(*args, **kwargs)
    

    【讨论】:

    • 在每次保存之前执行查找似乎不是很高效。
    • “在每次保存之前执行查找似乎不是很高效”我同意。但这取决于上下文。无论如何,你有什么建议?
    • @IanE 我添加了一个避免数据库查找的答案stackoverflow.com/a/64116052/3446669
    【解决方案7】:

    这是一个应用程序,可让您在保存模型之前访问字段的先前值和当前值:django-smartfields

    下面是如何在一个不错的声明式 may 中解决这个问题:

    from django.db import models
    from smartfields import fields, processors
    from smartfields.dependencies import Dependency
    
    class ConditionalProcessor(processors.BaseProcessor):
    
        def process(self, value, stashed_value=None, **kwargs):
            if value != stashed_value:
                # do any necessary modifications to new value
                value = ... 
            return value
    
    class MyModel(models.Model):
        my_field = fields.CharField(max_length=10, dependencies=[
            Dependency(processor=ConditionalProcessor())
        ])
    

    此外,只有在该字段的值被替换的情况下才会调用此处理器

    【讨论】:

      【解决方案8】:

      我同意 Sahil 的观点,即使用 ModelForm 更好、更容易地做到这一点。但是,您将自定义 ModelForm 的 clean 方法并在那里执行验证。就我而言,如果设置了模型上的字段,我想阻止对模型实例的更新。

      我的代码如下所示:

      from django.forms import ModelForm
      
      class ExampleForm(ModelForm):
          def clean(self):
              cleaned_data = super(ExampleForm, self).clean()
              if self.instance.field:
                  raise Exception
              return cleaned_data
      

      【讨论】:

        【解决方案9】:

        实现此目的的另一种方法是使用post_initpost_save 信号来存储模型的初始状态。

        @receiver(models.signals.post_init)
        @receiver(models.signals.post_save)
        def _set_initial_state(
            sender: Type[Any],
            instance: Optional[models.Model] = None,
            **kwargs: Any,
        ) -> None:
            """
            Store the initial state of the model
            """
        
            if isinstance(instance, MyModel):
                instance._initial_state = instance.state
        

        其中stateMyModel 中字段的名称,_initial_state 是初始版本,在初始化/保存模式时复制。

        请注意,如果 state 是容器类型(例如 dict),您可能希望酌情使用 deepcopy

        【讨论】:

        • 我刚刚尝试过这种方法,但我收到一个错误,提示该实例没有state 属性。你的意思是instance._state?无论哪种方式,您如何访问初始字段值? instance._state 似乎没有存储这些。
        • state 是您要保存的变量的名称。 _initial_state 是保存的副本。用任何合适的变量名替换。
        • 我收到您的instance._initial_state = instance.state 错误,因为instance.state 不存在。我收到一条错误消息,提示“实例没有状态属性”。
        • state 是您模型中的字段吗?
        • 正确,state 是模型中字段的名称。
        【解决方案10】:

        在现代 Django 中,在上述答案中,有一个非常重要的问题要添加到 the answer accepted 的内容中。当您使用deferonly QuerySet API 时,您可能会陷入无限递归

        __get__()django.db.models.query_utils.DeferredAttribute 方法调用refresh_from_db()django.db.models.Model 方法。 refresh_from_db()中有一行db_instance = db_instance_qs.get(),此行递归调用实例的__init__()方法。

        因此,有必要添加确保目标属性不被延迟。

        def __init__(self, *args, **kwargs):
            super(MyClass, self).__init__(*args, **kwargs)
        
            deferred_fields = self.get_deferred_fields()
            important_fields = ['target_type', 'target_id', 'target_object', 'number', 'chain', 'expiration_date']
        
            self.__important_fields = list(filter(lambda x: x not in deferred_fields, important_fields))
            for field in self.__important_fields:
                setattr(self, '__original_%s' % field, getattr(self, field))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-06-29
          • 2013-06-22
          • 1970-01-01
          • 2012-02-14
          • 2018-07-03
          • 2023-03-23
          • 1970-01-01
          • 2017-06-05
          相关资源
          最近更新 更多