【问题标题】:When saving, how can you check if a field has changed?保存时,如何检查字段是否已更改?
【发布时间】:2010-11-24 05:14:33
【问题描述】:

在我的模型中,我有:

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

remote_image 第一次更改时效果很好。

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

【问题讨论】:

    标签: django django-models


    【解决方案1】:

    本质上,您希望覆盖models.Model__init__ 方法,以便保留原始值的副本。这样您就不必再进行一次数据库查找(这总是一件好事)。

        class Person(models.Model):
            name = models.CharField()
    
            __original_name = None
    
            def __init__(self, *args, **kwargs):
                super().__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().save(force_insert, force_update, *args, **kwargs)
                self.__original_name = self.name
    

    【讨论】:

    • 我不会覆盖init,而是使用post_init-signal docs.djangoproject.com/en/dev/ref/signals/#post-init
    • Django 文档推荐覆盖方法:docs.djangoproject.com/en/dev/topics/db/models/…
    • @callum 这样如果您对对象进行更改,保存它,然后进行其他更改并再次调用save(),它仍然可以正常工作。
    • @Josh 如果您有多个应用程序服务器在同一个数据库上工作,那么它不会有问题,因为它只跟踪内存中的变化
    • @lajarre,我认为您的评论有点误导。文档建议您这样做时要小心。他们不建议反对。
    【解决方案2】:

    我使用以下混合:

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

    用法:

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

    注意

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

    【讨论】:

    • 真的很完美,不执行额外的查询。非常感谢!
    • 关于如何忽略类型更改的任何建议?它认为这是一个区别:{'field_name': (0L, u'0')}
    • @IMFletcher 在您的情况下,您处理分配给模型字段的未清理数据。这种事情超出了这个 mixin 的范围。您可以先尝试使用模型表单清理数据,该表单将在保存时免费填充您的模型字段。或手动,即 model_instance.field_name = model_form.cleaned_data['field_name']
    • Mixin 很好,但是这个版本和.only() 一起使用会出现问题。如果 Model 至少有 3 个字段,则对 Model.objects.only('id') 的调用将导致无限递归。为了解决这个问题,我们应该从初始保存中删除延迟字段并更改_dict属性a bit
    • 就像 Josh 的回答一样,这段代码看似可以在您的单进程测试服务器上正常工作,但是当您将它部署到任何类型的多进程服务器时,它会给出不正确的结果。如果不查询数据库,您无法知道是否更改了数据库中的值。
    【解决方案3】:

    最好的方法是使用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
    

    【讨论】:

    • 如果 Josh 上面描述的方法不涉及额外的数据库命中,为什么这是最好的方法?
    • 1) 该方法是一种 hack,信号基本上是为这样的用途而设计的 2) 该方法需要对您的模型进行更改,这个不需要 3) 正如您可以在 cmets 中看到的那样答案,它有可能有潜在问题的副作用,这个解决方案没有
    • 如果您只关心在保存之前捕获更改,那么这种方式非常棒。但是,如果您想立即对更改做出反应,这将不起作用。我已经多次遇到后一种情况(我现在正在研究一个这样的例子)。
    • @Josh:“立即对变化做出反应”是什么意思?这以何种方式不会让你“做出反应”?
    • 对不起,我忘记了这个问题的范围,指的是一个完全不同的问题。也就是说,我认为信号是一个很好的方法(现在它们可用)。但是,我发现很多人认为覆盖 save 是一种“hack”。我不相信这是这样的。正如这个答案所暗示的 (stackoverflow.com/questions/170337/…),我认为当您不进行“特定于相关模型”的更改时,覆盖是最佳实践。也就是说,我不打算将这种信念强加给任何人。
    【解决方案4】:

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

    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)
    

    在处理表单时也是如此。您可以在 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 = []
    

    【讨论】:

    • Josh 的解决方案对数据库更加友好。额外调用来验证更改内容的成本很高。
    • 在写之前多读一次并不贵。如果有多个请求,跟踪更改方法也不起作用。尽管这会在获取和保存之间存在竞争条件。
    • 停止告诉人们检查pk is not None 它不适用,例如如果使用 UUIDField。这只是个坏建议。
    • @dalore 你可以通过使用@transaction.atomic装饰保存方法来避免竞争条件
    • @dalore 尽管您需要确保事务隔离级别足够。在 postgresql 中,默认是读取提交,但 repeatable read is necessary.
    【解决方案5】:

    自 Django 1.8 发布以来,您可以使用 from_db 类方法来缓存 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!
    

    【讨论】:

    • 谢谢——这是对文档的引用:docs.djangoproject.com/en/1.8/ref/models/instances/…。我相信这仍然会导致上述问题,即数据库可能会在评估和比较完成之间发生变化,但这是一个不错的新选择。
    • 而不是搜索值(基于值的数量是 O(n))不是更快更清晰吗new._loaded_remote_image = new.remote_image
    • 不幸的是,我不得不撤销我之前(现已删除)的评论。虽然from_dbrefresh_from_db 调用,但实例上的属性(即已加载或先前)不会更新。结果,我找不到比__init__更好的任何理由,因为您仍然需要处理3种情况:__init__/from_dbrefresh_from_dbsave
    【解决方案6】:

    请注意,django-model-utils 中提供了字段更改跟踪。

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

    【讨论】:

    • 来自 django-model-utils 的 FieldTracker 似乎工作得很好,谢谢!
    【解决方案7】:

    如果您使用的是表单,则可以使用表单的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
    

    【讨论】:

      【解决方案8】:

      我参加聚会有点晚了,但我也找到了这个解决方案: Django Dirty Fields

      【讨论】:

      • 看一下tickets,貌似这个包现在状态不太好(找维护者,12月31号之前需要改CI等等)
      【解决方案9】:

      另一个较晚的答案,但如果您只是想查看是否有新文件已上传到文件字段,请尝试以下操作:(改编自 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
      

      【讨论】:

      • 这是检查是否上传了新文件的绝佳解决方案。比根据数据库检查名称要好得多,因为文件的名称可能相同。您也可以在pre_save 接收器中使用它。感谢分享!
      • 这是一个使用诱变剂更新文件以读取音频信息时更新数据库中音频持续时间的示例 - gist.github.com/DataGreed/1ba46ca7387950abba2ff53baf70fec2
      【解决方案10】:

      正如 Serge 提到的,从 Django 1.8 开始,有 from_db 方法。事实上,Django 文档以这个特定的用例为例:

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

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

      【讨论】:

        【解决方案11】:

        这在 Django 1.8 中适用于我

        def clean(self):
            if self.cleaned_data['name'] != self.initial['name']:
                # Do something
        

        【讨论】:

        • 你能参考一下文档吗?
        【解决方案12】:

        有一个属性 __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)
        

        示例用法:

        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)
        

        仅输出那些已更改的字段

        {'name': {'initial_value': '1234515', 'final_value': 'nim'}, 'senior': {'initial_value': 'no', 'final_value': 'yes'}}
        

        【讨论】:

        • 这就像一个魅力!您还可以在 pre_save 信号中使用它,如果您需要在更新模型本身时进行其他更改,您还可以将其保存为竞态条件,如 here 所示。
        【解决方案13】:

        您可以使用django-model-changes 来执行此操作,而无需额外的数据库查找:

        from django.dispatch import receiver
        from django_model_changes import ChangesMixin
        
        class Alias(ChangesMixin, MyBaseModel):
           # your model
        
        @receiver(pre_save, sender=Alias)
        def do_something_if_changed(sender, instance, **kwargs):
            if 'remote_image' in instance.changes():
                # do something
        

        【讨论】:

          【解决方案14】:

          游戏已经很晚了,但这是Chris Pratt's answer 的一个版本,它通过使用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
          

          【讨论】:

            【解决方案15】:

            最佳解决方案可能是在保存模型实例之前不包括额外的数据库读取操作,也不包括任何进一步的 django-library。这就是为什么 laffuste 的解决方案更可取的原因。在管理站点的上下文中,可以简单地覆盖save_model-方法,并在那里调用表单的has_changed 方法,就像上面 Sion 的回答一样。你会得到这样的结果,借鉴 Sion 的示例设置,但使用 changed_data 进行所有可能的更改:

            class ModelAdmin(admin.ModelAdmin):
               fields=['name','mode']
               def save_model(self, request, obj, form, change):
                 form.changed_data #output could be ['name']
                 #do somethin the changed name value...
                 #call the super method
                 super(self,ModelAdmin).save_model(request, obj, form, change)
            
            • 覆盖save_model:

            https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

            • 内置changed_data-字段方法:

            https://docs.djangoproject.com/en/1.10/ref/forms/api/#django.forms.Form.changed_data

            【讨论】:

              【解决方案16】:

              虽然这实际上并不能回答您的问题,但我会以不同的方式解决这个问题。

              成功保存本地副本后,只需清除remote_image 字段即可。然后在您的保存方法中,只要remote_image 不为空,您就可以随时更新图像。

              如果您想保留对 url 的引用,可以使用不可编辑的布尔字段而不是 remote_image 字段本身来处理缓存标志。

              【讨论】:

                【解决方案17】:

                在我的解决方案是覆盖目标字段类的pre_save() 方法之前,我遇到过这种情况,只有在字段已更改时才会调用它
                对 FileField 有用 示例:

                class PDFField(FileField):
                    def pre_save(self, model_instance, add):
                        # do some operations on your file 
                        # if and only if you have changed the filefield
                

                缺点:
                如果您想做任何(post_save)操作,例如在某些作业中使用创建的对象(如果某些字段已更改),则没有用

                【讨论】:

                  【解决方案18】:

                  我对@livskiy 的mixin 进行了如下扩展:

                  class ModelDiffMixin(models.Model):
                      """
                      A model mixin that tracks model fields' values and provide some useful api
                      to know what fields have been changed.
                      """
                      _dict = DictField(editable=False)
                      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.
                          """
                          object_dict = model_to_dict(self,
                                 fields=[field.name for field in self._meta.fields])
                          for field in object_dict:
                              # for FileFields
                              if issubclass(object_dict[field].__class__, FieldFile):
                                  try:
                                      object_dict[field] = object_dict[field].path
                                  except :
                                      object_dict[field] = object_dict[field].name
                  
                              # TODO: add other non-serializable field types
                          self._dict = object_dict
                          super(ModelDiffMixin, self).save(*args, **kwargs)
                  
                      class Meta:
                          abstract = True
                  

                  DictField 是:

                  class DictField(models.TextField):
                      __metaclass__ = models.SubfieldBase
                      description = "Stores a python dict"
                  
                      def __init__(self, *args, **kwargs):
                          super(DictField, self).__init__(*args, **kwargs)
                  
                      def to_python(self, value):
                          if not value:
                              value = {}
                  
                          if isinstance(value, dict):
                              return value
                  
                          return json.loads(value)
                  
                      def get_prep_value(self, value):
                          if value is None:
                              return value
                          return json.dumps(value)
                  
                      def value_to_string(self, obj):
                          value = self._get_val_from_obj(obj)
                          return self.get_db_prep_value(value)
                  

                  可以通过在模型中扩展它来使用它 同步/迁移时将添加一个 _dict 字段,该字段将存储对象的状态

                  【讨论】:

                    【解决方案19】:

                    改进所有领域的@josh 答案:

                    class Person(models.Model):
                      name = models.CharField()
                    
                    def __init__(self, *args, **kwargs):
                        super(Person, self).__init__(*args, **kwargs)
                        self._original_fields = dict([(field.attname, getattr(self, field.attname))
                            for field in self._meta.local_fields if not isinstance(field, models.ForeignKey)])
                    
                    def save(self, *args, **kwargs):
                      if self.id:
                        for field in self._meta.local_fields:
                          if not isinstance(field, models.ForeignKey) and\
                            self._original_fields[field.name] != getattr(self, field.name):
                            # Do Something    
                      super(Person, self).save(*args, **kwargs)
                    

                    澄清一下,getattr 用于获取带有字符串的person.name 之类的字段(即getattr(person, "name")

                    【讨论】:

                    • 它仍然没有进行额外的数据库查询?
                    • 我试图实现你的代码。通过编辑字段可以正常工作。但现在我插入新的有问题。我在课堂上的 FK 字段中得到了 DoesNotExist。一些提示如何解决它将不胜感激。
                    • 我刚刚更新了代码,它现在跳过了外键,所以你不需要用额外的查询来获取那些文件(非常昂贵),如果对象不存在,它会跳过额外的逻辑。
                    【解决方案20】:

                    使用 David Cramer 的解决方案怎么样:

                    http://cramer.io/2010/12/06/tracking-changes-to-fields-in-django/

                    我已经成功地像这样使用它:

                    @track_data('name')
                    class Mode(models.Model):
                        name = models.CharField(max_length=5)
                        mode = models.CharField(max_length=5)
                    
                        def save(self, *args, **kwargs):
                            if self.has_changed('name'):
                                print 'name changed'
                    
                        # OR #
                    
                        @classmethod
                        def post_save(cls, sender, instance, created, **kwargs):
                            if instance.has_changed('name'):
                                print "Hooray!"
                    

                    【讨论】:

                    【解决方案21】:

                    对@ivanperelivskiy 答案的修改:

                    @property
                    def _dict(self):
                        ret = {}
                        for field in self._meta.get_fields():
                            if isinstance(field, ForeignObjectRel):
                                # foreign objects might not have corresponding objects in the database.
                                if hasattr(self, field.get_accessor_name()):
                                    ret[field.get_accessor_name()] = getattr(self, field.get_accessor_name())
                                else:
                                    ret[field.get_accessor_name()] = None
                            else:
                                ret[field.attname] = getattr(self, field.attname)
                        return ret
                    

                    这使用了 django 1.10 的公共方法get_fields。这使代码更具前瞻性,但更重要的是还包括外键和可编辑=假的字段。

                    供参考,这里是.fields的实现

                    @cached_property
                    def fields(self):
                        """
                        Returns a list of all forward fields on the model and its parents,
                        excluding ManyToManyFields.
                    
                        Private API intended only to be used by Django itself; get_fields()
                        combined with filtering of field properties is the public API for
                        obtaining this field list.
                        """
                        # For legacy reasons, the fields property should only contain forward
                        # fields that are not private or with a m2m cardinality. Therefore we
                        # pass these three filters as filters to the generator.
                        # The third lambda is a longwinded way of checking f.related_model - we don't
                        # use that property directly because related_model is a cached property,
                        # and all the models may not have been loaded yet; we don't want to cache
                        # the string reference to the related_model.
                        def is_not_an_m2m_field(f):
                            return not (f.is_relation and f.many_to_many)
                    
                        def is_not_a_generic_relation(f):
                            return not (f.is_relation and f.one_to_many)
                    
                        def is_not_a_generic_foreign_key(f):
                            return not (
                                f.is_relation and f.many_to_one and not (hasattr(f.remote_field, 'model') and f.remote_field.model)
                            )
                    
                        return make_immutable_fields_list(
                            "fields",
                            (f for f in self._get_fields(reverse=False)
                             if is_not_an_m2m_field(f) and is_not_a_generic_relation(f) and is_not_a_generic_foreign_key(f))
                        )
                    

                    【讨论】:

                      【解决方案22】:

                      这是另一种方法。

                      class Parameter(models.Model):
                      
                          def __init__(self, *args, **kwargs):
                              super(Parameter, self).__init__(*args, **kwargs)
                              self.__original_value = self.value
                      
                          def clean(self,*args,**kwargs):
                              if self.__original_value == self.value:
                                  print("igual")
                              else:
                                  print("distinto")
                      
                          def save(self,*args,**kwargs):
                              self.full_clean()
                              return super(Parameter, self).save(*args, **kwargs)
                              self.__original_value = self.value
                      
                          key = models.CharField(max_length=24, db_index=True, unique=True)
                          value = models.CharField(max_length=128)
                      

                      根据文档:validating objects

                      “full_clean() 执行的第二步是调用 Model.clean()。应该重写此方法以对您的模型执行自定义验证。 此方法应用于提供自定义模型验证,并根据需要修改模型上的属性。例如,您可以使用它来自动为字段提供值,或者进行需要访问多个字段的验证:"

                      【讨论】:

                        【解决方案23】:

                        我对@iperelivskiy 的解决方案的看法:在大规模上,为每个__init__ 创建_initial dict 是昂贵的,而且大多数时候是不必要的。我稍微更改了 mixin,以便它仅在您明确告诉它这样做时才记录更改(通过调用 instance.track_changes):

                        from typing import KeysView, Optional
                        from django.forms import model_to_dict
                        
                        class TrackChangesMixin:
                            _snapshot: Optional[dict] = None
                        
                            def track_changes(self):
                                self._snapshot = self.as_dict
                        
                            @property
                            def diff(self) -> dict:
                                if self._snapshot is None:
                                    raise ValueError("track_changes wasn't called, can't determine diff.")
                                d1 = self._snapshot
                                d2 = self.as_dict
                                diffs = [(k, (v, d2[k])) for k, v in d1.items() if str(v) != str(d2[k])]
                                return dict(diffs)
                        
                            @property
                            def has_changed(self) -> bool:
                                return bool(self.diff)
                        
                            @property
                            def changed_fields(self) -> KeysView:
                                return self.diff.keys()
                        
                            @property
                            def as_dict(self) -> dict:
                                return model_to_dict(self, fields=[field.name for field in self._meta.fields])
                        

                        【讨论】:

                          【解决方案24】:

                          作为 SmileyChris 回答的扩展,您可以为 last_updated 向模型添加一个日期时间字段,并为在检查更改之前允许它达到的最大年龄设置某种限制

                          【讨论】:

                            【解决方案25】:

                            @ivanlivski 的 mixin 很棒。

                            我已经把它扩展到

                            • 确保它适用于小数字段。
                            • 公开属性以简化使用

                            更新的代码可在此处获得: https://github.com/sknutsonsf/python-contrib/blob/master/src/django/utils/ModelDiffMixin.py

                            为了帮助刚接触 Python 或 Django 的人,我将给出一个更完整的示例。 这种特殊用途是从数据提供者处获取文件并确保数据库中的记录反映该文件。

                            我的模型对象:

                            class Station(ModelDiffMixin.ModelDiffMixin, models.Model):
                                station_name = models.CharField(max_length=200)
                                nearby_city = models.CharField(max_length=200)
                            
                                precipitation = models.DecimalField(max_digits=5, decimal_places=2)
                                # <list of many other fields>
                            
                               def is_float_changed (self,v1, v2):
                                    ''' Compare two floating values to just two digit precision
                                    Override Default precision is 5 digits
                                    '''
                                    return abs (round (v1 - v2, 2)) > 0.01
                            

                            加载文件的类有这些方法:

                            class UpdateWeather (object)
                                # other methods omitted
                            
                                def update_stations (self, filename):
                                    # read all existing data 
                                    all_stations = models.Station.objects.all()
                                    self._existing_stations = {}
                            
                                    # insert into a collection for referencing while we check if data exists
                                    for stn in all_stations.iterator():
                                        self._existing_stations[stn.id] = stn
                            
                                    # read the file. result is array of objects in known column order
                                    data = read_tabbed_file(filename)
                            
                                    # iterate rows from file and insert or update where needed
                                    for rownum in range(sh.nrows):
                                        self._update_row(sh.row(rownum));
                            
                                    # now anything remaining in the collection is no longer active
                                    # since it was not found in the newest file
                                    # for now, delete that record
                                    # there should never be any of these if the file was created properly
                                    for stn in self._existing_stations.values():
                                        stn.delete()
                                        self._num_deleted = self._num_deleted+1
                            
                            
                                def _update_row (self, rowdata):
                                    stnid = int(rowdata[0].value) 
                                    name = rowdata[1].value.strip()
                            
                                    # skip the blank names where data source has ids with no data today
                                    if len(name) < 1:
                                        return
                            
                                    # fetch rest of fields and do sanity test
                                    nearby_city = rowdata[2].value.strip()
                                    precip = rowdata[3].value
                            
                                    if stnid in self._existing_stations:
                                        stn = self._existing_stations[stnid]
                                        del self._existing_stations[stnid]
                                        is_update = True;
                                    else:
                                        stn = models.Station()
                                        is_update = False;
                            
                                    # object is new or old, don't care here            
                                    stn.id = stnid
                                    stn.station_name = name;
                                    stn.nearby_city = nearby_city
                                    stn.precipitation = precip
                            
                                    # many other fields updated from the file 
                            
                                    if is_update == True:
                            
                                        # we use a model mixin to simplify detection of changes
                                        # at the cost of extra memory to store the objects            
                                        if stn.has_changed == True:
                                            self._num_updated = self._num_updated + 1;
                                            stn.save();
                                    else:
                                        self._num_created = self._num_created + 1;
                                        stn.save()
                            

                            【讨论】:

                              【解决方案26】:

                              如果您对重写 save 方法不感兴趣,您可以这样做

                                model_fields = [f.name for f in YourModel._meta.get_fields()]
                                valid_data = {
                                      key: new_data[key]
                                      for key in model_fields
                                      if key in new_data.keys()
                                }
                              
                                for (key, value) in valid_data.items():
                                      if getattr(instance, key) != value:
                                         print ('Data has changed')
                              
                                      setattr(instance, key, value)
                              
                               instance.save()
                              

                              【讨论】:

                                【解决方案27】:

                                我找到了这个包django-lifecycle。 它使用 django 信号来定义@hook 装饰器,非常健壮和可靠。我用过,很幸福。

                                【讨论】:

                                • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
                                猜你喜欢
                                • 2021-02-27
                                • 2013-08-11
                                • 1970-01-01
                                • 2019-09-27
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                • 1970-01-01
                                相关资源
                                最近更新 更多