如何在Django中完成文本的完整历史记录?

时间:2009-01-07 15:36:02

标签: django django-models concurrency parallel-processing atomic

我希望获得用户编辑的大型文本字段的完整历史记录,使用Django进行存储。

我见过这些项目:

我有一个特殊的用例,可能超出了这些项目提供的范围。此外,我对这些项目的文档记录,测试和更新情况保持警惕。无论如何,这是我面临的问题:

我有一个模特,喜欢:

from django.db import models

class Document(models.Model):
   text_field = models.TextField()

这个文本字段可能很大 - 超过40k - 我希望有一个自动保存功能,每30秒左右保存一次该字段。这可能会使数据库变得非常大,显然,如果每次40k都有很多保存(如果压缩可能仍然是10k)。我能想到的最佳解决方案是保持最新保存版本和新版本之间的差异。

但是,我担心涉及并行更新的竞争条件。有两种不同的竞争条件可供考虑(第二种情况比第一次严重得多):

  1. HTTP事务竞争条件:用户A和用户B请求文档X0,并单独进行更改,生成Xa和Xb。 Xa被保存,X0和Xa之间的差异是“Xa-0”(“少了一点”),Xa现在被存储为数据库中的正式版本。如果Xb随后保存,它会覆盖Xa,差异为Xb-a(“b less a”)。

    虽然不理想,但我并不过分担心这种行为。这些文件互相覆盖,用户A和B可能彼此不知道(每个文件都以文件X0开头),但历史保留了完整性。

  2. 数据库读取/更新竞争条件:有问题的竞争条件是Xa和Xb同时保存在X0上。会有(伪)代码类似于:

     def save_history(orig_doc, new_doc):
         text_field_diff = diff(orig_doc.text_field, new_doc.text_field)
         save_diff(text_field_diff)
    

    如果Xa和Xb都从数据库读取X0(即orig_doc是X0),它们的差异将变为Xa-0和Xb-0(与序列化的Xa-0相反,然后是Xb-a,或等效的Xb-0)然后是Xa-b)。当您尝试将差异拼接在一起以生成历史记录时,它将在修补程序Xa-0或Xb-0(均适用于X0)上失败。历史的完整性已经受到损害(或有吗?)。

    一种可能的解决方案是自动协调算法,可以检测事后这些问题。如果重建历史记录失败,可能会假设已发生竞争条件,因此将失败的修补程序应用于历史记录的先前版本,直到成功为止。

  3. 我很高兴能就如何解决这个问题提供一些反馈和建议。

    顺便说一下,只要它是一个有用的出路,我注意到Django的原子性在这里讨论:

    谢天谢地。

5 个答案:

答案 0 :(得分:3)

以下是我为保存对象历史所做的工作:

对于Django应用程序历史记录:

<强>历史/ __ INIT __ PY:

"""
history/__init__.py
"""
from django.core import serializers
from django.utils import simplejson as json
from django.db.models.signals import pre_save, post_save

# from http://code.google.com/p/google-diff-match-patch/
from contrib.diff_match_patch import diff_match_patch

from history.models import History

def register_history(M):
  """
  Register Django model M for keeping its history

  e.g. register_history(Document) - every time Document is saved,
  its history (i.e. the differences) is saved.
  """
  pre_save.connect(_pre_handler, sender=M)
  post_save.connect(_post_handler, sender=M)

def _pre_handler(signal, sender, instance, **kwargs):
  """
  Save objects that have been changed.
  """
  if not instance.pk:
    return

  # there must be a before, if there's a pk, since
  # this is before the saving of this object.
  before = sender.objects.get(pk=instance.pk)

  _save_history(instance, _serialize(before).get('fields'))

def _post_handler(signal, sender, instance, created, **kwargs):
  """
  Save objects that are being created (otherwise we wouldn't have a pk!)
  """
  if not created:
     return

  _save_history(instance, {})

def _serialize(instance):
   """
   Given a Django model instance, return it as serialized data
   """
   return serializers.serialize("python", [instance])[0]

def _save_history(instance, before):
  """
  Save two serialized objects
  """
  after = _serialize(instance).get('fields',{})

  # All fields.
  fields = set.union(set(before.keys()),set(after.keys()))

  dmp = diff_match_patch()

  diff = {}

  for field in fields:
    field_before = before.get(field,False)
    field_after = after.get(field,False)

    if field_before != field_after:
      if isinstance(field_before, unicode) or isinstance(field_before, str):
      # a patch
        diff[field] = dmp.diff_main(field_before,field_after)
      else:
        diff[field] = field_before

  history = History(history_for=instance, diff=json.dumps(diff))
  history.save()

<强>历史/ models.py

"""
history/models.py
"""

from django.db import models

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

from contrib import diff_match_patch as diff

class History(models.Model):
     """
     Retain the history of generic objects, e.g. documents, people, etc..
  """

  content_type = models.ForeignKey(ContentType, null=True)

  object_id = models.PositiveIntegerField(null=True)

  history_for = generic.GenericForeignKey('content_type', 'object_id')

  diff = models.TextField()

  def __unicode__(self):
       return "<History (%s:%d):%d>" % (self.content_type, self. object_id, self.pk)

希望能帮助某人,并且会对评论表示赞赏。

请注意,解决了我最关心的竞争条件。如果在_pre_handler“before = sender.objects.get(pk = instance.pk)”之前调用另一个实例保存,但在其他实例更新了历史记录之后,并且当前实例首先保存,则会出现'破坏历史'(即无序)。值得庆幸的是,diff_match_patch试图优雅地处理“非致命”休息,但并不能保证成功。

一种解决方案是原子性。我不知道如何将上述竞争条件(即_pre_handler)作为Django所有实例的原子操作。一个HistoryLock表,或内存中的共享哈希(memcached?)会没问题 - 建议?

如上所述,另一种解决方案是协调算法。但是,并发保存可能存在“真正的”冲突,需要用户干预才能确定正确的对帐。

显然,将历史拼凑在一起并不是上述片段的一部分。

答案 1 :(得分:2)

存储问题:我认为您应该只存储文档的两个连续有效版本的差异。正如您所指出的,当发生并发编辑时,问题就变成了有效的版本。

并发问题:

  1. 你可以像Jeff suggests一样或通过锁定文件来避免它们吗?
  2. 如果没有,我认为您最终会处于在线协作实时编辑器(如Google文档)的范例中。
  3. 要获得图示视图您正在开启的蠕虫病毒 this google tech-talk at 9m21s (这是关于Eclipse的协作实时编辑)

    或者,有一些专利详细说明了在Wikipedia article on collaborative real-time editors处理这些并发的方法。

答案 2 :(得分:1)

为了管理差异,您可能想要研究Python的difflib

关于原子性,我可能会像Wikis(Trac等)一样处理它。如果自用户上次检索内容后内容已更改,请求他们使用新版本覆盖。如果您将文本和差异存储在同一记录中,使用您发布的链接中的技术来避免数据库竞争条件应该不难。

答案 3 :(得分:1)

我认为,您的自动保存会在用户实际按下保存按钮之前保存草稿版本,对吗?

如果是这样,您不必保留草稿保存,只需在用户决定保存为真实后再处理它们,并且只保留真实/显式保存的历史记录。

答案 4 :(得分:1)

我已经发现了django-reversion,它似乎运行良好并且得到了积极的维护,尽管它没有做差异来有效地将小差异存储到大块文本中。