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


72

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

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

谢谢!


1
从数据库中获取旧值,save然后检查每个字段是否相等,该怎么办?
J0HN 2014年

您想要哪种优化?
Leonardo.Z

@ J0HN在获取,比较和保存过程中更改的da。
Leonardo.Z 2014年

我认为,是否可以并且必须降低性能?
YN 2014年

Answers:


70

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

像这样“记住”模型init中的值:

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

5
我更喜欢Odif的方式,因为我需要触发不带表单的模型操作(更改来自api或管理网站之后)
YN

什么时候__init__叫?它也仅适用于初始创建或后续更新吗?
wasabigeek

每次创建模型实例时都会调用Init。如果实例在其生命周期内被服务器更新过几次,则__init__仅在开始时才调用。
Odif Yltsaeb '17

如果使用savebulk_create
Julio Marins 18'May

不,不会。但是,模型保存几乎从来不会突然发生,没有先前的交互。has_changed方法可以在所有这些地方使用。创建对象时,您无需检查它是否已更改……
Odif Yltsaeb,

48

最好在ModelForm级别执行此操作

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

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

如果要在模型级别执行此操作,则可以遵循Odif答案中指定的方法。


1
我同意您的回答,self.instance也可以在此问题中使用。
lehins 2014年

@AlexeyKuleshevich同意,但仅表单_post_cleanis_valid->errors->full_clean->_post_clean)之前,此后实例将更新为包括新值。访问中form.clean_fieldname()form.clean()似乎确定提供的是他们的第一个电话。
jozxyqk 2015年

2
很好,这是可行的,但是仅当您使用表单保存时,情况并非总是如此。
吉瓦尔

是啊,没错。如果您不使用表单,则不能这样做。但是使用表格是理想的方法。
萨希尔·卡拉

self.changed_data对我来说是个新手
Mohammed Shareef C

34

您也可以从django-model-utils使用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
    

4
是的,我只是喜欢这有多干净...要求的另一条线!
凯文·帕克

但是,此跟踪器字段是表中的真实列吗?还是只是假田?
toscanelli '16

3
@toscanelli,它不会在表中添加列。
texnic

1
只是提醒您一定要进行迁移,然后再次迁移,否则将出现属性错误,例如:找不到“ tracker”。
Amoroso

3
这个很吸引人,但是有人在这里报告性能问题。团队没有任何更新或跟进。因此,请查看的源代码tracker.py。看起来很多工作和信号。因此,这是否值得-或用例太有限,以至于您只需要跟踪一两个字段即可。
John Pang

4

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

注意:如果使用此方法,则没有其他数据库查询。

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

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关系。例如,如果您尝试跟踪与用户关联的组的更改,则使用该技术似乎没有任何方法可以做到。
shacker

3

我的用例是,每当某个字段更改其值时,我都需要在模型中设置一个非规格化的值。但是,由于要监视的字段是m2m关系,因此我不想在每次调用save时都要执行DB查找,以检查非规范化字段是否需要更新。因此,我改写了这个小混合(使用@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

3

这样的事情也可以:

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)

9
在每次保存之前执行查找似乎不太高效。
伊恩E

1
我同意:“在每次保存之前执行查找似乎并不十分有效”。但这取决于上下文。无论如何,您有什么建议?
Akhorus

2
@IanE我添加了一个避免数据库查找的答案stackoverflow.com/a/64116052/3446669
GunnerFan

2

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

这是一个很好的声明式方法可以解决的问题:

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())
    ])

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


2

我同意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

0

实现此目标的另一种方法是使用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

0

在现代Django中,将重要的问题添加到上述答案中已接受的答案的内容中。使用或时,您可以陷入无限递归deferonlyQuerySet API。

__get__()django.db.models.query_utils.DeferredAttribute调用refresh_from_db()方法django.db.models.Model。有一条线db_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))
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.