Я бы хотел, чтобы полная история большого текстового поля, редактируемого пользователями, сохранялась с помощью Django.
Я видел проекты:
У меня есть особый вариант использования, который, вероятно, выходит за рамки того, что предоставляют эти проекты. Кроме того, я настороженно отношусь к тому, насколько хорошо документированы, протестированы и обновлены эти проекты. В любом случае, вот проблема, с которой я сталкиваюсь:
У меня есть такая модель, как:
from django.db import models
class Document(models.Model):
text_field = models.TextField()
Это текстовое поле может быть большим - более 40 КБ - и я хотел бы иметь функцию автосохранения, которая сохраняет поле каждые 30 секунд или около того. Это может сделать базу данных слишком большой, очевидно, если будет много сохранений по 40 КБ каждое (возможно, все еще 10 КБ, если заархивировать). Лучшее решение, которое я могу придумать, - это сохранить разницу между самой последней сохраненной версией и новой версией.
Однако меня беспокоят условия гонки, связанные с параллельными обновлениями. На ум приходят два различных состояния гонки (второе намного серьезнее первого):
Состояние гонки HTTP-транзакций: пользователь A и пользователь B запрашивают документ X0 и вносят изменения индивидуально, создавая Xa и Xb. Xa сохраняется, разница между X0 и Xa составляет «Xa-0» («меньше нет»), причем Xa теперь хранится как официальная версия в базе данных. Если Xb впоследствии сохраняет, он перезаписывает Xa, при этом разница будет Xb-a («b меньше 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- б). Когда вы пытаетесь склеить различия для создания истории, это не сработает либо на патче Xa-0, либо на Xb-0 (оба применяются к X0). Целостность истории была нарушена (или есть?).
Одно из возможных решений - алгоритм автоматического согласования, который обнаруживает эти проблемы Постфактум. Если восстановление истории завершается неудачно, можно предположить, что возникла гонка, и поэтому применять неудачный патч к предыдущим версиям истории до тех пор, пока он не будет успешным.
Я был бы рад получить отзывы и предложения о том, как решить эту проблему.
Кстати, поскольку это полезный выход, я заметил, что здесь обсуждается атомарность Django:
Большое спасибо.





Для управления различиями вы, вероятно, захотите изучить Python дифлиб.
Что касается атомарности, я бы, вероятно, поступил так же, как и вики (Trac и т. д.). Если содержимое изменилось с момента последнего его получения пользователем, запросите его замену новой версией. Если вы храните текст и различия в одной и той же записи, не составит труда избежать состояния гонки базы данных, используя методы, указанные в размещенных вами ссылках.
Difflib великолепен, спасибо. Я до сих пор не выяснил атомарность, но думаю, что это выполнимо.
Проблема с хранением: Я думаю, вам следует хранить различия только двух последовательных действительных версий документа. Как вы отметили, проблема заключается в получении действительной версии при одновременном редактировании.
Проблема параллелизма:
Чтобы получить иллюстрированный вид банки с червями, вы открываете catch этот технический разговор Google в 9:21 (речь идет о совместном редактировании Eclipse в реальном времени)
В качестве альтернативы, есть пара патентов, в которых подробно описаны способы решения этих проблем в Статья в Википедии о совместных редакторах в реальном времени.
Очень полезные ссылки, спасибо. Очень интересная проблема. Возможно, я ищу золотую середину: одновременное редактирование без сложностей совместного редактирования в реальном времени.
Ваше автосохранение, я полагаю, сохраняет черновую версию до того, как пользователь действительно нажмет кнопку сохранения, верно?
Если это так, вам не нужно хранить черновики сохранений, просто удалите их после того, как пользователь решит сохранить их в реальном времени, и сохраните только историю реальных / явных сохранений.
Хорошее предложение. Мне нравится идея неявного ведения истории - чтобы вы могли вернуться назад и сказать: «О, верно». Однако за это приходится платить. :)
Вот что я сделал, чтобы сохранить историю объекта:
Для истории приложений 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()
history / 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?) Подойдут - предложения?
Другое решение, как уже упоминалось, - это алгоритм согласования. Однако одновременные сохранения могут иметь «настоящие» конфликты и требовать вмешательства пользователя для определения правильного согласования.
Очевидно, что сбор истории по кусочкам не является частью приведенных выше фрагментов.
С тех пор я обнаружил также джанго-реверсия, который, кажется, хорошо работает и активно поддерживается, хотя он не выполняет различий для эффективного хранения небольших различий в больших фрагментах текста.
Это не полный ответ, поэтому я просто оставлю его в комментариях. Попробуйте посмотреть на поле Django RCS: code.google.com/p/django-rcsfield Это системы контроля версий для управления полем. Статья о том, как заставить работать: lethain.com/entry/2008/oct/15/setting-up-django-rcsfield