我希望获得用户编辑的大型文本字段的完整历史记录,使用Django进行存储。
我见过这些项目:
我有一个特殊的用例,可能超出了这些项目提供的范围。此外,我对这些项目的文档记录,测试和更新情况保持警惕。无论如何,这是我面临的问题:
我有一个模特,喜欢:
from django.db import models
class Document(models.Model):
text_field = models.TextField()
这个文本字段可能很大 - 超过40k - 我希望有一个自动保存功能,每30秒左右保存一次该字段。这可能会使数据库变得非常大,显然,如果每次40k都有很多保存(如果压缩可能仍然是10k)。我能想到的最佳解决方案是保持最新保存版本和新版本之间的差异。
但是,我担心涉及并行更新的竞争条件。有两种不同的竞争条件可供考虑(第二种情况比第一次严重得多):
HTTP事务竞争条件:用户A和用户B请求文档X0,并单独进行更改,生成Xa和Xb。 Xa被保存,X0和Xa之间的差异是“Xa-0”(“少了一点”),Xa现在被存储为数据库中的正式版本。如果Xb随后保存,它会覆盖Xa,差异为Xb-a(“b less a”)。
虽然不理想,但我并不过分担心这种行为。这些文件互相覆盖,用户A和B可能彼此不知道(每个文件都以文件X0开头),但历史保留了完整性。
数据库读取/更新竞争条件:有问题的竞争条件是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)上失败。历史的完整性已经受到损害(或有吗?)。
一种可能的解决方案是自动协调算法,可以检测事后这些问题。如果重建历史记录失败,可能会假设已发生竞争条件,因此将失败的修补程序应用于历史记录的先前版本,直到成功为止。
我很高兴能就如何解决这个问题提供一些反馈和建议。
顺便说一下,只要它是一个有用的出路,我注意到Django的原子性在这里讨论:
谢天谢地。
答案 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)
存储问题:我认为您应该只存储文档的两个连续有效版本的差异。正如您所指出的,当发生并发编辑时,问题就变成了有效的版本。
并发问题:
要获得图示视图您正在开启的蠕虫病毒 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,它似乎运行良好并且得到了积极的维护,尽管它没有做差异来有效地将小差异存储到大块文本中。