【问题标题】:Dirty fields in djangodjango中的脏字段
【发布时间】:2008-09-21 11:27:24
【问题描述】:

在我的应用程序中,我需要在保存模型时保存更改的值(旧的和新的)。任何示例或工作代码?

我需要这个来预先审核内容。例如,如果用户更改模型中的某些内容,那么管理员可以在单独的表中查看所有更改,然后决定是否应用它们。

【问题讨论】:

  • 我看到过关于脏字段的类似问题,但这是同一个问题;为了让管理员查看发生了什么变化,您首先需要确定发生了什么变化......

标签: python django


【解决方案1】:

我发现 Armin 的想法非常有用。这是我的变体;

class DirtyFieldsMixin(object):
    def __init__(self, *args, **kwargs):
        super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
        self._original_state = self._as_dict()

    def _as_dict(self):
        return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])

    def get_dirty_fields(self):
        new_state = self._as_dict()
        return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])

编辑:我已经测试了这个顺便说一句。

很抱歉排长队。不同之处在于(除了名称)它只缓存本地非关系字段。换句话说,它不会缓存父模型的字段(如果存在)。

还有一件事;保存后需要重置_original_state dict。但我不想覆盖save() 方法,因为大多数时候我们会在保存后丢弃模型实例。

def save(self, *args, **kwargs):
    super(Klass, self).save(*args, **kwargs)
    self._original_state = self._as_dict()

【讨论】:

  • django-dirtyfields 应用提供了相同类型的 mixin。
  • @dnozay,这并不奇怪,因为 django-dirtyfields 在 "credits" 下声明它源于这个 stackoverflow 问题
【解决方案2】:

Django 当前正在将所有列发送到数据库,即使您只是更改了一个。要改变这一点,需要对数据库系统进行一些更改。这可以很容易地在现有代码上实现,方法是向模型添加一组脏字段并为其添加列名,每次__set__ 一个列值。

如果您需要该功能,我建议您查看 Django ORM,实现它并将补丁放入 Django trac。添加它应该很容易,它也会帮助其他用户。当你这样做时,添加一个每次设置列时调用的钩子。

如果您不想破解 Django 本身,您可以复制对象创建时的字典并进行比较。

也许有这样的mixin:

class DiffingMixin(object):

    def __init__(self, *args, **kwargs):
        super(DiffingMixin, self).__init__(*args, **kwargs)
        self._original_state = dict(self.__dict__)

    def get_changed_columns(self):
        missing = object()
        result = {}
        for key, value in self._original_state.iteritems():
            if key != self.__dict__.get(key, missing):
                result[key] = value
        return result

 class MyModel(DiffingMixin, models.Model):
     pass

此代码未经测试,但应该可以工作。当您调用model.get_changed_columns() 时,您会得到所有更改值的字典。这当然不适用于列中的可变对象,因为原始状态是字典的平面副本。

【讨论】:

  • 这可能早就过期了,但应该是if value != self.__dict__.get(key, missing):
  • 您能否详细说明__set__ 方法?听起来它可以满足我当前的需求,但我无法在它方面取得任何进展。
【解决方案3】:

添加第二个答案,因为自最初发布此问题以来发生了很多变化

现在 Django 世界中有许多应用程序可以解决这个问题。你可以在 Django Packages 网站上找到完整的 list of model auditing and history apps

我写了a blog post 比较了其中一些应用程序。这篇文章现在有 4 年历史了,有点过时了。不过,解决这个问题的不同方法似乎是相同的。

方法:

  1. 将所有历史更改以序列化格式 (JSON?) 存储在单个表中
  2. 将所有历史更改存储在一个表中,以反映每个模型的原始更改
  3. 将所有历史更改存储在与原始模型相同的表中(我不推荐这样做)

django-reversion 包似乎仍然是这个问题最流行的解决方案。它采用第一种方法:序列化更改而不是镜像表。

几年前我复活了django-simple-history。它采用第二种方法:镜像每个表。

所以我建议使用应用程序来解决这个问题。在这一点上,有几个流行的方法效果很好。

哦,如果您只是在寻找脏字段检查而不是存储所有历史更改,请查看 FieldTracker from django-model-utils

【讨论】:

    【解决方案4】:

    您没有对您的特定用例或需求进行太多说明。特别是,了解您需要如何处理更改信息(您需要存储多长时间?)会很有帮助。如果您只需要将其存储用于临时目的,@S.Lott 的会话解决方案可能是最好的。如果您想要对存储在数据库中的对象的所有更改进行完整的审计跟踪,请尝试AuditTrail solution

    更新:我在上面链接的 AuditTrail 代码是我见过的最接近适用于您的案例的完整解决方案的代码,尽管它有一些限制(对多对多字段)。它将所有以前版本的对象存储在数据库中,因此管理员可以回滚到任何以前的版本。如果您希望更改在获得批准之前不生效,则必须稍作修改。

    您还可以基于 @Armin Ronacher 的 DiffingMixin 之类的东西构建自定义解决方案。您可以将 diff 字典(可能是腌制的?)存储在一个表中,供管理员稍后查看并在需要时应用(您需要编写代码来获取 diff 字典并将其应用到实例)。

    【讨论】:

      【解决方案5】:

      如果您使用自己的事务(不是默认的管理应用程序),您可以保存对象的前后版本。您可以将之前的版本保存在会话中,也可以将其放在表单中的“隐藏”字段中。隐藏字段是安全的噩梦。因此,请使用会话来保留该用户所发生事情的历史记录。

      此外,当然,您必须获取前一个对象,以便对其进行更改。因此,您有多种方法可以监控差异。

      def updateSomething( request, object_id ):
          object= Model.objects.get( id=object_id )
          if request.method == "GET":
              request.session['before']= object
              form= SomethingForm( instance=object )
          else request.method == "POST"
              form= SomethingForm( request.POST )
              if form.is_valid():
                  # You have before in the session
                  # You have the old object
                  # You have after in the form.cleaned_data
                  # Log the changes
                  # Apply the changes to the object
                  object.save()
      

      【讨论】:

        【解决方案6】:

        我扩展了 Trey Hunner 的解决方案以支持 m2m 关系。希望这会帮助其他寻找类似解决方案的人。

        from django.db.models.signals import post_save
        
        DirtyFieldsMixin(object):
            def __init__(self, *args, **kwargs):
                super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
                post_save.connect(self._reset_state, sender=self.__class__,
                    dispatch_uid='%s._reset_state' % self.__class__.__name__)
                self._reset_state()
        
            def _as_dict(self):
                fields =  dict([
                    (f.attname, getattr(self, f.attname))
                    for f in self._meta.local_fields
                ])
                m2m_fields = dict([
                    (f.attname, set([
                        obj.id for obj in getattr(self, f.attname).all()
                    ]))
                    for f in self._meta.local_many_to_many
                ])
                return fields, m2m_fields
        
            def _reset_state(self, *args, **kwargs):
                self._original_state, self._original_m2m_state = self._as_dict()
        
            def get_dirty_fields(self):
                new_state, new_m2m_state = self._as_dict()
                changed_fields = dict([
                    (key, value)
                    for key, value in self._original_state.iteritems()
                    if value != new_state[key]
                ])
                changed_m2m_fields = dict([
                    (key, value)
                    for key, value in self._original_m2m_state.iteritems()
                    if sorted(value) != sorted(new_m2m_state[key])
                ])
                return changed_fields, changed_m2m_fields
        

        可能还希望合并两个字段列表。为此,替换最后一行

        return changed_fields, changed_m2m_fields
        

        changed_fields.update(changed_m2m_fields)
        return changed_fields
        

        【讨论】:

        • @trey,这看起来很适合 m2m。您是否测试过它是否有效?另外,这是使用更新的 _meta API 为最新的 Django 更新的吗?
        • @Neil:AuditTrail 代码在几年前被打包到 djnago-simple-history 中。我添加了第二个答案,指出了今天推荐的解决方案。自原始回答以来发生了很多变化。感谢您恢复这个!
        【解决方案7】:

        继续 Muhuk 的建议并添加 Django 的信号和唯一的 dispatch_uid,您可以在不覆盖 save() 的情况下重置保存状态:

        from django.db.models.signals import post_save
        
        class DirtyFieldsMixin(object):
            def __init__(self, *args, **kwargs):
                super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
                post_save.connect(self._reset_state, sender=self.__class__, 
                                    dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
                self._reset_state()
        
            def _reset_state(self, *args, **kwargs):
                self._original_state = self._as_dict()
        
            def _as_dict(self):
                return dict([(f.name, getattr(self, f.name)) for f in self._meta.local_fields if not f.rel])
        
            def get_dirty_fields(self):
                new_state = self._as_dict()
                return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
        

        这将在保存后清除原始状态,而无需覆盖 save()。该代码有效,但不确定在 __init__ 处连接信号的性能损失是什么

        【讨论】:

          【解决方案8】:

          我扩展了 muhuk 和 smn 的解决方案,包括对外键和一对一字段的主键进行差异检查:

          from django.db.models.signals import post_save
          
          class DirtyFieldsMixin(object):
              def __init__(self, *args, **kwargs):
                  super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
                  post_save.connect(self._reset_state, sender=self.__class__,
                                      dispatch_uid='%s-DirtyFieldsMixin-sweeper' % self.__class__.__name__)
                  self._reset_state()
          
              def _reset_state(self, *args, **kwargs):
                  self._original_state = self._as_dict()
          
              def _as_dict(self):
                  return dict([(f.attname, getattr(self, f.attname)) for f in self._meta.local_fields])
          
              def get_dirty_fields(self):
                  new_state = self._as_dict()
                  return dict([(key, value) for key, value in self._original_state.iteritems() if value != new_state[key]])
          

          唯一的区别是_as_dict我把最后一行改成了

          return dict([
              (f.name, getattr(self, f.name)) for f in self._meta.local_fields
              if not f.rel
          ])
          

          return dict([
              (f.attname, getattr(self, f.attname)) for f in self._meta.local_fields
          ])
          

          这个mixin,和上面的一样,可以这样使用:

          class MyModel(DirtyFieldsMixin, models.Model):
              ....
          

          【讨论】:

            【解决方案9】:

            基于上述@Trey 和@Tony 的具有m2m 支持的更新解决方案(使用更新的dirtyfields 和新的_meta API 以及一些错误修复)。这对我来说已经通过了一些基本的光照测试。

            from dirtyfields import DirtyFieldsMixin
            class M2MDirtyFieldsMixin(DirtyFieldsMixin):
                def __init__(self, *args, **kwargs):
                    super(M2MDirtyFieldsMixin, self).__init__(*args, **kwargs)
                    post_save.connect(
                        reset_state, sender=self.__class__,
                        dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format(
                            name=self.__class__.__name__))
                    reset_state(sender=self.__class__, instance=self)
            
                def _as_dict_m2m(self):
                    if self.pk:
                        m2m_fields = dict([
                            (f.attname, set([
                                obj.id for obj in getattr(self, f.attname).all()
                            ]))
                            for f,model in self._meta.get_m2m_with_model()
                        ])
                        return m2m_fields
                    return {}
            
                def get_dirty_fields(self, check_relationship=False):
                    changed_fields = super(M2MDirtyFieldsMixin, self).get_dirty_fields(check_relationship)
                    new_m2m_state = self._as_dict_m2m()
                    changed_m2m_fields = dict([
                        (key, value)
                        for key, value in self._original_m2m_state.iteritems()
                        if sorted(value) != sorted(new_m2m_state[key])
                    ])
                    changed_fields.update(changed_m2m_fields)
                    return changed_fields
            
            def reset_state(sender, instance, **kwargs):
                # original state should hold all possible dirty fields to avoid
                # getting a `KeyError` when checking if a field is dirty or not
                instance._original_state = instance._as_dict(check_relationship=True)
                instance._original_m2m_state = instance._as_dict_m2m()
            

            【讨论】:

              【解决方案10】:

              对于大家的信息,muhuk 的解决方案在 python2.6 下失败,因为它引发了一个异常,指出 'object.__ init __()' 不接受任何参数...

              编辑:嗬!显然这可能是我滥用了 mixin ...我没有注意并将其声明为最后一个父级,因此对 init 的调用最终出现在对象父级而不是下一个与钻石图继承一样的父级!所以请忽略我的评论:)

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2011-06-11
                • 2023-03-15
                • 2015-09-10
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多