Как сделать текст полной истории в Джанго? - PullRequest
9 голосов
/ 07 января 2009

Мне бы хотелось, чтобы вся история большого текстового поля, отредактированного пользователями, хранилась с использованием Django.

Я видел проекты:

У меня есть особый вариант использования, который, вероятно, выходит за рамки того, что предоставляют эти проекты. Кроме того, я опасаюсь, насколько хорошо задокументированы, протестированы и обновлены эти проекты. В любом случае вот проблема, с которой я сталкиваюсь:

У меня есть модель, Likeo:

from django.db import models

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

Это текстовое поле может быть большим - более 40 КБ - и я хотел бы иметь функцию автосохранения, которая сохраняет поле каждые 30 секунд или около того. Это может сделать базу данных слишком большой, очевидно, если будет много сохранений по 40 Кбайт каждый (вероятно, по-прежнему 10 Кбайт в случае сжатия). Лучшее решение, которое я могу придумать, - это сохранить разницу между самой последней сохраненной версией и новой версией.

Однако меня беспокоят условия гонки, связанные с параллельными обновлениями. На ум приходят два различных расовых состояния (второе гораздо более серьезное, чем первое):

  1. условие гонки транзакций HTTP : пользователь A и пользователь B запрашивают документ X0 и вносят индивидуальные изменения, производя Xa и Xb. Xa сохранен, разница между X0 и Xa равна «Xa-0» («меньше»), теперь Xa сохраняется в качестве официальной версии в базе данных. Если впоследствии Xb сохраняет, он перезаписывает Xa, тогда как diff будет 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-0, либо в Xb-0 (оба относятся к X0). Целостность истории была поставлена ​​под угрозу (или нарушена?).

    Одним из возможных решений является алгоритм автоматической сверки, который обнаруживает эти проблемы ex-post . Если перестройка истории не удалась, можно предположить, что произошло состояние гонки, и применить неудачный патч к предыдущим версиям истории, пока он не будет успешным.

Я был бы рад получить отзывы и предложения о том, как решить эту проблему.

Кстати, поскольку это полезный выход, я заметил, что атомарность Джанго обсуждается здесь:

Спасибо, любезно.

Ответы [ 5 ]

3 голосов
/ 11 января 2009

Вот что я сделал, чтобы сохранить историю объекта:

Для приложения 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?) Были бы хороши - предложения?

Другим решением, как уже упоминалось, является алгоритм сверки. Однако при одновременном сохранении могут возникать «подлинные» конфликты, и для определения правильной выверки может потребоваться вмешательство пользователя.

Очевидно, что объединение истории не является частью вышеупомянутых фрагментов.

2 голосов
/ 08 января 2009

Проблема с хранилищем: Я думаю, вам следует хранить различия только двух последовательных действительных версий документа. Как вы указали, проблема заключается в получении действительной версии при одновременном редактировании.

Проблема параллелизма:

  1. Не могли бы вы избежать их всех вместе, как Джефф предлагает или заблокировав документ?
  2. Если нет, то я думаю, что в конечном итоге вы находитесь в парадигме онлайн-редакторов в режиме реального времени, таких как Google Docs .

Чтобы получить иллюстрированный вид банки с червями, которую вы открываете, поймайте этот технический доклад Google в 9m21s (это о совместной работе Eclipse в режиме реального времени редактирование)

В качестве альтернативы, есть пара патентов, в которых подробно описываются способы борьбы с этими совпадениями в статье Википедии о совместных редакторах реального времени .

1 голос
/ 03 февраля 2009

С тех пор я обнаружил django-reversion , который, кажется, хорошо работает и активно поддерживается, хотя он не делает различий для эффективного хранения небольших различий в больших фрагментах текста.

1 голос
/ 08 января 2009

Я полагаю, что ваше автосохранение сохраняет черновую версию до того, как пользователь фактически нажмет кнопку сохранения, верно?

Если это так, вам не нужно сохранять сохраненные черновики, просто утилизируйте их после того, как пользователь решит сохранить реальные данные, и сохраните только историю реальных / явных сохранений.

1 голос
/ 07 января 2009

Для управления diff-файлами вы, вероятно, захотите изучить difflib .

в Python.

Что касается атомарности, я бы, вероятно, отнесся к ней так же, как к вики (Trac и т. Д.). Если содержимое изменилось с тех пор, как пользователь последний раз получил его, попросите переопределить его с новой версией. Если вы храните текст и различия в одной и той же записи, вам не составит труда избежать состязаний в базе данных, используя методы, которые вы опубликовали в ссылках.

...