django - 在保存之前比较新旧字段值

Y.N*_*Y.N 57 python django django-signals

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

我已经尝试了save()继承和pre_save信号.它被正确触发,但我找不到实际更改字段的列表,无法比较新旧值.有一种方法?我需要它来优化预先行动.

谢谢!

Odi*_*aeb 59

There is very simple django way for doing it.

"Memorise" the values in model init like this:

def __init__(self, *args, **kwargs):
    super(MyClass, self).__init__(*args, **kwargs)
    self.initial_parametername = self.parametername
    ---
    self.initial_parameternameX = self.parameternameX
Run Code Online (Sandbox Code Playgroud)

Real life example:

At class:

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
Run Code Online (Sandbox Code Playgroud)

And then in modelform save method:

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
Run Code Online (Sandbox Code Playgroud)

  • 我更喜欢Odif的方式,因为我需要触发没有表单的模型的动作(更改来自api或来自管理站点) (5认同)
  • 使用这种方法要小心。当我尝试执行“Model.objects.delete()”操作时,我发现很多问题(包括 python 崩溃或达到递归限制),如果我想缓存的字段是外键(即使您尝试存储“self._old_”) <field>_id` 作为整数 (2认同)

Sah*_*lra 39

It is better to do this at ModelForm level.

There you get all the Data that you need for comparison in save method:

  1. self.data : Actual Data passed to the Form.
  2. self.cleaned_data : Data cleaned after validations, Contains Data eligible to be saved in the Model
  3. self.changed_data : List of Fields which have changed. This will be empty if nothing has changed

If you want to do this at Model level then you can follow the method specified in Odif's answer.

  • 嗯,这有效,但仅当您使用表单保存时,情况并非总是如此。 (2认同)

psl*_*psl 30

你也可以使用FieldTrackerDjango的模型utils的这个:

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

    tracker = FieldTracker()
    
    Run Code Online (Sandbox Code Playgroud)
  2. 现在在pre_save和post_save中你可以使用:

    instance.tracker.previous('modelfield')     # get the previous value
    instance.tracker.has_changed('modelfield')  # just check if it is changed
    
    Run Code Online (Sandbox Code Playgroud)

  • 这个很诱人,但有人报告了一个性能问题 [here](https://github.com/jazzband/django-model-utils/issues/323)。并且团队没有更新或跟进。因此,请查看 `tracker.py` 的源代码。看起来有很多作品和信号。因此,它是否值得 - 或者用例太有限以至于您只需要跟踪一两个领域。 (3认同)
  • 是的,我只是喜欢这是多么干净......另一条要求! (2认同)
  • @toscanelli,它不会向表中添加列. (2认同)

Gun*_*Fan 10

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

注意:如果您使用此方法,则没有额外的数据库查询。

这是官方文档模型实例的链接- 自定义模型加载

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
Run Code Online (Sandbox Code Playgroud)

所以现在原始值_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)
            
            
Run Code Online (Sandbox Code Playgroud)

  • 这很酷,但它不会为您提供 M2M 关系。例如,如果您尝试跟踪用户关联的组的更改,则似乎没有任何方法可以使用此技术来实现此目的。 (2认同)

You*_*Kim 6

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

__get__()的方法django.db.models.query_utils.DeferredAttribute调用的方法。中有一行,该行递归调用实例的方法。refresh_from_db()django.db.models.Modeldb_instance = db_instance_qs.get()refresh_from_db()__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))
Run Code Online (Sandbox Code Playgroud)

  • 不建议 __init 存储原始值__,即使您的方法已详细说明。__使用 from_db 代替__。请参阅有关此问题的 django 讨论:https://forum.djangoproject.com/t/recursionerror-when-deleting-a-model-instance-in-admin/7862/19 (3认同)

Sli*_*eam 5

像这样的东西也有效:

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)
Run Code Online (Sandbox Code Playgroud)

  • 在每次保存之前执行查找似乎性能不太好。 (12认同)
  • @IanE我添加了一个答案,避免了数据库查找/sf/answers/4488123671/ (2认同)