繁体   English   中英

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

[英]django - comparing old and new field value before saving

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

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

谢谢!

有一种非常简单的 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

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

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

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

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

您也可以为此使用django-model-utilsFieldTracker

  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

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

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

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

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 关系,我不想在调用 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

像这样的东西也有效:

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)

这是一个应用程序,可让您在保存模型之前访问字段的先前值和当前值: 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())
    ])

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

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

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

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

django.db.models.Model__get__()方法调用django.db.models.query_utils.DeferredAttributerefresh_from_db()方法。 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))

这是我的做法。 例如,比较字段“状态”。 并检查用户的权限。

管理员.py

    def save_model(self, request, obj, form, change):
    if change is False:
        obj.created_by = request.user
    else:
        # check if field_1 is being updated
        if obj._loaded_values['state'] != obj.state and not request.user.has_perm('mtasks.change_status', obj):
            messages.set_level(request, messages.ERROR)
            messages.error(request, "You don't have permission to change state")
            return
        
    super().save_model(request, obj, form, change)

在模型.py

class ClassName
    ...    
    @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,
        instance._loaded_values = dict(zip(field_names, values))    
        return instance

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM