Django:保存时,如何检查字段是否已更改?

Pau*_*jan 273 django caching image django-models

在我的模型中,我有:

class Alias(MyBaseModel):
    remote_image = models.URLField(max_length=500, null=True, help_text="A URL that is downloaded and cached for the image. Only
 used when the alias is made")
    image = models.ImageField(upload_to='alias', default='alias-default.png', help_text="An image representing the alias")


    def save(self, *args, **kw):
        if (not self.image or self.image.name == 'alias-default.png') and self.remote_image :
            try :
                data = utils.fetch(self.remote_image)
                image = StringIO.StringIO(data)
                image = Image.open(image)
                buf = StringIO.StringIO()
                image.save(buf, format='PNG')
                self.image.save(hashlib.md5(self.string_id).hexdigest() + ".png", ContentFile(buf.getvalue()))
            except IOError :
                pass
Run Code Online (Sandbox Code Playgroud)

这首次remote_image变化很有效.

当有人修改remote_image了别名时,如何获取新图像?其次,是否有更好的方法来缓存远程图像?

Jos*_*osh 405

虽然现在有点晚了,但是让我为其他人发布这个解决方案.实质上,您希望覆盖__init__方法,models.Model以便保留原始值的副本.这使得您无需进行其他数据库查找(这总是一件好事).

class Person(models.Model):
  name = models.CharField()

  __original_name = None

  def __init__(self, *args, **kwargs):
    super(Person, self).__init__(*args, **kwargs)
    self.__original_name = self.name

  def save(self, force_insert=False, force_update=False, *args, **kwargs):
    if self.name != self.__original_name:
      # name changed - do something here

    super(Person, self).save(force_insert, force_update, *args, **kwargs)
    self.__original_name = self.name
Run Code Online (Sandbox Code Playgroud)

  • 你应该在save()的末尾添加一个self .__ original_name = self.name (28认同)
  • 而不是覆盖init,我会使用post_init-signal http://docs.djangoproject.com/en/dev/ref/signals/#post-init (24认同)
  • Django文档建议使用覆盖方法:http://docs.djangoproject.com/en/dev/topics/db/models/#overriding-predefined-model-methods (20认同)
  • 如果您有多个应用服务器针对同一个数据库工作,那么@Josh不会出现问题,因为它只跟踪内存中的变化 (17认同)
  • @lajarre,我认为你的评论有点误导.文档建议您在这样做时要小心.他们不建议反对它. (13认同)
  • @callum这样如果你对对象进行了更改,保存它,然后进行其他更改并在其上调用`save()`,它仍然可以正常工作. (10认同)
  • 有一点需要注意(现在看起来相当明显,但只是让我半小时),如果你这样做,你将永远无法再次推迟()'名字',因为它将不断检查自己. init上的名字.一个草率的解决方法是检查它是否存在于`self .__ dict__`中,如果没有则跳过设置`__original_name`.可能有更好的方法,但还没有找到它. (7认同)
  • 如果在构造函数中初始化模型,这将不起作用.这些值显然已经改变了,但它们已经在Model .__ init __()返回时设置,因此它们在检查后不会出现更改. (5认同)
  • 您不需要多个进程才能失败.只需创建指向同一个DB行的两个不同的模型实例.更改一个并保存.然后保存第二个(不更改它).第二次保存将导致更改DB(返回到原始值),但此答案中描述的方法将不会检测到它. (5认同)
  • @JensAlm是对的.这段代码在您的单进程测试服务器上看似很好,但是当您将它部署到任何类型的多处理服务器时,它将给出完全错误的结果. (4认同)
  • django doc不建议覆盖模型的__init__ https://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#django.db.models.Model (3认同)
  • 使用此方法时要小心。我在企业应用程序上成功地实现了一段时间。当稍后在项目中作为引用对象删除时,最终导致“python 递归限制”错误。查看并尝试了许多不同的策略,最后不得不将其更改为在保存方法中进行快速读取查询(带有竞争条件安全检查)。我还需要提到的是,直到我从 Django 2.xx 升级到 Django 3.2.12 并从 Python 3.7 升级到 Python 3.9 后,这一点才出现。 (3认同)
  • 更改post_init信号传递的实例不起作用.您必须覆盖init方法. (2认同)
  • 我不明白有必要在 `save()` 方法的末尾添加 `self.__original_name = self.name`。谁能解释一下? (2认同)
  • @JensAlm我认为这取决于您的特定应用程序。但是,我认为,在大多数情况下,这会很好。我们要问的问题是“用户做了一些改变值的事情”。在大多数情况下,这抓住了真正的积极或消极。我想如果用户同时在多个选项卡中工作会有些混乱。但是,如果我是那个用户,我认为我对行为的期望将会改变。 (2认同)
  • 我添加了与我的模型类似的内容,并得到“<MyModel> 对象没有属性 '__original_name'”。它适用于现有数据吗? (2认同)
  • 请注意,它*不适用于延迟字段(在查询集上使用 .only() )。如果使用 Person.objects.only('id').first(),Django 1.8 将陷入无限递归。 (2认同)
  • 它工作得很好,但是你必须处理添加新对象`if not self.id or self.name != self.__original_name:` (2认同)
  • @MarioOrlandi 竞争条件安全检查只是对数据库的快速查询,以确保您仍然拥有最“最新”的模型实例。无法保证在您使用 save 方法时模型数据是有效的。因此,我只是快速检查一下模型上次更新的时间,并将其与我的预期进行比较。对于小型网站来说,这甚至可能不是问题,我只是想涵盖我的基础。如果这 100% 是一个问题,那么数据库事务可能会工作得更好。 (2认同)

ipe*_*kiy 188

我使用以下mixin:

from django.forms.models import model_to_dict


class ModelDiffMixin(object):
    """
    A model mixin that tracks model fields' values and provide some useful api
    to know what fields have been changed.
    """

    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self.__initial = self._dict

    @property
    def diff(self):
        d1 = self.__initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        super(ModelDiffMixin, self).save(*args, **kwargs)
        self.__initial = self._dict

    @property
    def _dict(self):
        return model_to_dict(self, fields=[field.name for field in
                             self._meta.fields])
Run Code Online (Sandbox Code Playgroud)

用法:

>>> p = Place()
>>> p.has_changed
False
>>> p.changed_fields
[]
>>> p.rank = 42
>>> p.has_changed
True
>>> p.changed_fields
['rank']
>>> p.diff
{'rank': (0, 42)}
>>> p.categories = [1, 3, 5]
>>> p.diff
{'categories': (None, [1, 3, 5]), 'rank': (0, 42)}
>>> p.get_field_diff('categories')
(None, [1, 3, 5])
>>> p.get_field_diff('rank')
(0, 42)
>>>
Run Code Online (Sandbox Code Playgroud)

注意

请注意,此解决方案仅适用于当前请求的上下文.因此它主要适用于简单的情况.在并发环境中,多个请求可以同时操作同一个模型实例,您肯定需要一种不同的方法.

  • +1使用mixin.+1没有额外的数据库命中.+1有很多有用的方法/属性.我需要多次upvote. (27认同)
  • 就像Josh的回答一样,这段代码在您的单进程测试服务器上看起来会很好用,但是当你将它部署到任何类型的多处理服务器时,它会产生不正确的结果.在不查询数据库的情况下,您无法知道是否更改了数据库中的值. (17认同)
  • 真的很完美,不要执行额外的查询.非常感谢 ! (4认同)
  • Mixin非常棒,但与.only()一起使用时,此版本存在问题.如果Model至少有3个字段,对Model.objects.only('id')的调用将导致无限递归.要解决这个问题,我们应该从初始保存中删除延迟字段并更改_dict属性[有点](https://gist.github.com/pitsevich/d8cf357df3b927cf13a2) (2认同)
  • 在refresh_from_db上我们还应该重新初始化初始状态。`def refresh_from_db(self, using=None, fields=None): super().refresh_from_db(using, fields) self.__initial = self._dict` (2认同)

Chr*_*att 142

最好的方法是使用pre_save信号.在问及回答这个问题时,09年可能不是一个选项,但是今天看到这个问题的任何人都应该这样做:

@receiver(pre_save, sender=MyModel)
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something
Run Code Online (Sandbox Code Playgroud)

  • 1)该方法是一个黑客,信号基本上是为这样的用途设计的2)该方法需要对你的模型进行改动,这个方法不是3)因为你可以在对该答案的评论中看到,它有副作用,可能有问题,这个解决方案没有 (31认同)
  • 如果Josh描述的方法不涉及额外的数据库命中,为什么这是最好的方法? (6认同)
  • @Josh:"立即对变化做出反应"是什么意思?以什么方式不让你"反应"? (5认同)
  • 如果您只关心在保存之前捕获更改,这种方式很棒.但是,如果您想立即对更改做出反应,这将不起作用.我多次遇到过后一种情况(现在我正在研究一个这样的实例). (2认同)
  • 抱歉,我忘记了这个问题的范围,而是指一个完全不同的问题。就是说,我认为信号是到达此处的一种好方法(现在已经可用)。但是,我发现许多人都认为重写是一种“黑客”。我认为情况并非如此。正如这个答案所暗示的那样(http://stackoverflow.com/questions/170337/django-signals-vs-overriding-save-method),我认为当您不致力于“特定于该模型。” 就是说,我无意将这种信念强加给任何人。 (2认同)
  • @Josh:您不是说*特定于所讨论模型的更改吗?根据您链接的答案,信号最适合您想在模型之间重用相同代码的情况。我错过了什么? (2认同)

zgo*_*oda 135

现在直接回答:检查字段值是否已更改的一种方法是在保存实例之前从数据库中获取原始数据.考虑这个例子:

class MyModel(models.Model):
    f1 = models.CharField(max_length=1)

    def save(self, *args, **kw):
        if self.pk is not None:
            orig = MyModel.objects.get(pk=self.pk)
            if orig.f1 != self.f1:
                print 'f1 changed'
        super(MyModel, self).save(*args, **kw)
Run Code Online (Sandbox Code Playgroud)

使用表单时同样适用.您可以在ModelForm的clean或save方法中检测它:

class MyModelForm(forms.ModelForm):

    def clean(self):
        cleaned_data = super(ProjectForm, self).clean()
        #if self.has_changed():  # new instance or existing updated (form has data to save)
        if self.instance.pk is not None:  # new instance only
            if self.instance.f1 != cleaned_data['f1']:
                print 'f1 changed'
        return cleaned_data

    class Meta:
        model = MyModel
        exclude = []
Run Code Online (Sandbox Code Playgroud)

  • Josh的解决方案更加适合数据库.验证更改内容的额外调用是昂贵的. (22认同)
  • 在写入之前额外阅读并不是那么昂贵.如果有多个请求,跟踪更改方法也不起作用.虽然这会在获取和保存之间受到竞争条件的影响. (4认同)
  • 停止告诉人们检查“pk 不是无”它不适用,例如,如果使用 UUIDField。这只是不好的建议。 (2认同)
  • @dalore你可以通过使用`@ transaction.atomic`修饰save方法来避免竞争条件 (2认同)
  • @dalore虽然您需要确保事务隔离级别足够.在postgresql中,默认是读取提交,但[必须重复读取](https://docs.djangoproject.com/en/1.10/ref/databases/#isolation-level). (2认同)

Ser*_*rge 54

自从Django 1.8发布以来,您可以使用from_db classmethod来缓存remote_image的旧值.然后在save方法中,您可以比较字段的旧值和新值以检查值是否已更改.

@classmethod
def from_db(cls, db, field_names, values):
    new = super(Alias, cls).from_db(db, field_names, values)
    # cache value went from the base
    new._loaded_remote_image = values[field_names.index('remote_image')]
    return new

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    if (self._state.adding and self.remote_image) or \
        (not self._state.adding and self._loaded_remote_image != self.remote_image):
        # If it is first save and there is no cached remote_image but there is new one, 
        # or the value of remote_image has changed - do your stuff!
Run Code Online (Sandbox Code Playgroud)

  • 谢谢 - 这是对文档的参考:https://docs.djangoproject.com/en/1.8/ref/models/instances/#customizing-model-loading。我相信这仍然会导致上述问题,即数据库可能会在评估和比较完成之间发生变化,但这是一个不错的新选项。 (2认同)

Lee*_*nde 18

请注意,字段更改跟踪在django-model-utils中可用.

https://django-model-utils.readthedocs.org/en/latest/index.html

  • 来自 django-model-utils 的 [FieldTracker](https://django-model-utils.readthedocs.io/en/latest/utilities.html#field-tracker) 似乎工作得很好,谢谢! (5认同)

laf*_*ste 15

如果您使用的是表单,则可以使用Form的changed_data(docs):

class AliasForm(ModelForm):

    def save(self, commit=True):
        if 'remote_image' in self.changed_data:
            # do things
            remote_image = self.cleaned_data['remote_image']
            do_things(remote_image)
        super(AliasForm, self).save(commit)

    class Meta:
        model = Alias
Run Code Online (Sandbox Code Playgroud)


小智 9

游戏已经很晚了,但这是克里斯普拉特答案transaction的一个版本,通过使用块和牺牲性能来防止竞争条件select_for_update()

@receiver(pre_save, sender=MyModel)
@transaction.atomic
def do_something_if_changed(sender, instance, **kwargs):
    try:
        obj = sender.objects.select_for_update().get(pk=instance.pk)
    except sender.DoesNotExist:
        pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
    else:
        if not obj.some_field == instance.some_field: # Field has changed
            # do something
Run Code Online (Sandbox Code Playgroud)


Aar*_*lin 6

另一个迟到的答案,但如果您只是想查看新文件是否已上传到文件字段,请尝试以下操作:(改编自 Christopher Adams 对链接http://zmsmith.com/2010/05/django的评论)-check-if-a-field-has-changed/在 zach 的评论中)

更新链接:https : //web.archive.org/web/20130101010327/http : //zmsmith.com : 80/2010/05/django-check-if-a-field-has-changed/

def save(self, *args, **kw):
    from django.core.files.uploadedfile import UploadedFile
    if hasattr(self.image, 'file') and isinstance(self.image.file, UploadedFile) :
        # Handle FileFields as special cases, because the uploaded filename could be
        # the same as the filename that's already there even though there may
        # be different file contents.

        # if a file was just uploaded, the storage model with be UploadedFile
        # Do new file stuff here
        pass
Run Code Online (Sandbox Code Playgroud)


Fre*_*pos 6

我有点迟到了,但我也发现了这个解决方案: Django Dirty Fields

  • 查看门票,看起来这个包现在状况不佳(正在寻找维护者,需要在 12 月 31 日之前更改其 CI 等) (2认同)

Ami*_*ber 5

从Django 1.8开始from_db,就像Serge提到的那样.事实上,Django文档将此特定用例作为示例:

https://docs.djangoproject.com/en/dev/ref/models/instances/#customizing-model-loading

下面是一个示例,说明如何记录从数据库加载的字段的初始值


jhr*_*s21 5

这在 Django 1.8 中对我有用

def clean(self):
    if self.cleaned_data['name'] != self.initial['name']:
        # Do something
Run Code Online (Sandbox Code Playgroud)


Nim*_*sal 5

有一个属性 __dict__ 将所有字段作为键,将值作为字段值。所以我们可以只比较其中两个

只需将模型的保存功能更改为下面的功能

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
    if self.pk is not None:
        initial = A.objects.get(pk=self.pk)
        initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
        initial_json.pop('_state'), final_json.pop('_state')
        only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
        print(only_changed_fields)
    super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
Run Code Online (Sandbox Code Playgroud)

示例用法:

class A(models.Model):
    name = models.CharField(max_length=200, null=True, blank=True)
    senior = models.CharField(choices=choices, max_length=3)
    timestamp = models.DateTimeField(null=True, blank=True)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        if self.pk is not None:
            initial = A.objects.get(pk=self.pk)
            initial_json, final_json = initial.__dict__.copy(), self.__dict__.copy()
            initial_json.pop('_state'), final_json.pop('_state')
            only_changed_fields = {k: {'final_value': final_json[k], 'initial_value': initial_json[k]} for k in initial_json if final_json[k] != initial_json[k]}
            print(only_changed_fields)
        super(A, self).save(force_insert=False, force_update=False, using=None, update_fields=None)
Run Code Online (Sandbox Code Playgroud)

仅生成那些已更改字段的输出

{'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
Run Code Online (Sandbox Code Playgroud)