10

我想拥有用户编辑的大型文本字段的完整历史记录,使用 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”(“a less not”),Xa 现在作为正式版本存储在数据库中。如果 Xb 随后保存,它将覆盖 Xa,差异为 Xb-a(“b 减去 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-乙)。当您尝试将差异修补在一起以生成历史记录时,它会在补丁 Xa-0 或 Xb-0(两者都适用于 X0)上失败。历史的完整性已经受到损害(或者已经受到损害?)。

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

我很高兴收到一些关于如何解决这个问题的反馈和建议。

顺便说一句,就它是一种有用的出路而言,我注意到这里讨论了 Django 原子性:

非常感谢你。

4

5 回答 5

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)" 在另一个实例保存之前被调用,但在另一个实例更新历史记录之后,当前实例首先保存,将会有一个 'broken历史”(即乱序)。值得庆幸的是 diff_match_patch 尝试优雅地处理“非致命”中断,但不能保证成功。

一种解决方案是原子性。不过,我不确定如何使上述竞争条件(即_pre_handler)成为跨所有Django 实例的原子操作。HistoryLock 表或内存中的共享哈希(memcached?)会很好 - 建议?

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

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

于 2009-01-11T19:13:32.800 回答
3

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

并发问题:

  1. 你能像Jeff 建议的那样一起避免它们,或者通过锁定文档来避免它们吗?
  2. 如果不是,我认为您最终会成为在线协作实时编辑器(如 Google Docs )的范式。

要获得您正在打开的蠕虫罐的图解视图,请在 9 分 21 秒观看这个谷歌技术讲座(它是关于 Eclipse 的协作实时编辑)

或者,在Wikipedia 关于协作实时编辑器的文章中,有一些专利详细说明了处理这些并发的方法。

于 2009-01-07T23:24:24.380 回答
1

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

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

于 2009-01-07T18:18:57.007 回答
1

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

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

于 2009-01-08T00:38:04.543 回答
1

从那以后,我也发现了django-reversion,它似乎运行良好并且得到积极维护,尽管它不能有效地将小差异存储到大段文本中。

于 2009-02-03T15:30:09.267 回答