¿Cómo hacer un historial completo de texto en Django?
django-models concurrency (5)
Me gustaría tener el historial completo de un gran campo de texto editado por los usuarios, almacenado con Django.
He visto los proyectos:
Tengo un caso de uso especial que probablemente esté fuera del alcance de lo que ofrecen estos proyectos. Además, desconfío de lo bien documentados, probados y actualizados que son estos proyectos. En cualquier caso, este es el problema que enfrento:
Tengo un modelo, me gustao:
from django.db import models
class Document(models.Model):
text_field = models.TextField()
Este campo de texto puede ser grande, más de 40k, y me gustaría tener una función de guardado automático que guarde el campo cada 30 segundos más o menos. Esto podría hacer que la base de datos sea excesivamente grande, obviamente, si hay muchas copias guardadas a 40k cada una (probablemente aún 10k si está comprimido). La mejor solución que puedo pensar es mantener una diferencia entre la versión más reciente guardada y la nueva versión.
Sin embargo, me preocupan las condiciones de carrera que implican actualizaciones paralelas. Hay dos condiciones de carrera distintas que vienen a la mente (la segunda mucho más seria que la primera):
Condición de carrera de transacción HTTP : el usuario X y el documento de solicitud B del usuario B realizan cambios individualmente, produciendo Xa y Xb. Xa se guarda, la diferencia entre X0 y Xa es "Xa-0" ("a menos no"), Xa ahora se almacena como la versión oficial en la base de datos. Si Xb posteriormente guarda, sobrescribe Xa, siendo el dif Xb-a ("b menos a").
Aunque no es ideal, no estoy demasiado preocupado con este comportamiento. Los documentos se sobrescriben entre sí, y es posible que los usuarios A y B no se hayan enterado mutuamente (cada uno de los cuales comenzó con el documento X0), pero el historial conserva la integridad.
Condición de lectura / actualización de la base de datos : la condición de carrera problemática es cuando Xa y Xb se guardan al mismo tiempo sobre X0. Habrá (pseudo-) código algo así como:
def save_history(orig_doc, new_doc): text_field_diff = diff(orig_doc.text_field, new_doc.text_field) save_diff(text_field_diff)
Si Xa y Xb ambos leen X0 de la base de datos (es decir, orig_doc es X0), sus diferencias se convertirán en Xa-0 y Xb-0 (a diferencia del serializado Xa-0 luego Xb-a, o equivalentemente Xb-0 luego Xa- segundo). Cuando intente parchear los diffs para generar el historial, fallará en el parche Xa-0 o Xb-0 (que ambos se aplican a X0). La integridad de la historia se ha visto comprometida (¿o no?).
Una posible solución es un algoritmo de reconciliación automática, que detecta estos problemas ex-post . Si la reconstrucción del historial falla, se puede suponer que se ha producido una condición de carrera y, por lo tanto, aplicar el parche fallido a las versiones anteriores del historial hasta que tenga éxito.
Estaría encantado de recibir algunos comentarios y sugerencias sobre cómo abordar este problema.
Por cierto, en la medida en que es una salida útil, he notado que la atomicidad de Django se trata aquí:
- Django: ¿Cómo puedo protegerme contra la modificación concurrente de las entradas de la base de datos y aquí?
- Operaciones atómicas en Django?
Gracias por su amabilidad.
Para gestionar los diffs, es probable que desee investigar el difflib de Python.
En cuanto a la atomicidad, probablemente lo manejaría igual que las Wikis (Trac, etc.). Si el contenido ha cambiado desde que el usuario lo recuperó por última vez, solicite que anule con la nueva versión. Si está almacenando el texto y los diffs en el mismo registro, no debería ser difícil evitar las condiciones de carrera de la base de datos utilizando las técnicas de los enlaces que publicó.
Supongo que su guardado automático guarda una versión preliminar antes de que el usuario presione el botón Guardar, ¿verdad?
Si es así, no tiene que guardar los borradores, simplemente deséchelos después de que el usuario decida guardarlos de verdad, y solo guarde el historial de los guardados reales / explícitos.
Esto es lo que hice para guardar el historial de un objeto:
Para el historial de la aplicación Django:
history / __ 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)
Espero que ayude a alguien, y los comentarios serán apreciados.
Tenga en cuenta que esto no aborda la condición de carrera de mi mayor preocupación. Si, en _pre_handler "before = sender.objects.get (pk = instance.pk)" se invoca antes de que se guarde otra instancia, pero después de que otra instancia haya actualizado el historial, y la instancia actual guarda primero, habrá un ''error'' historia ''(es decir, fuera de orden). Afortunadamente diff_match_patch intenta manejar con gracia los descansos "no fatales", pero no hay garantía de éxito.
Una solución es la atomicidad. Sin embargo, no estoy seguro de cómo hacer que la condición de carrera anterior (es decir, _pre_handler) sea una operación atómica en todas las instancias de Django. ¿Una tabla HistoryLock, o un hash compartido en la memoria (memcached?) Estaría bien, ¿sugerencias?
La otra solución, como se mencionó, es un algoritmo de reconciliación. Sin embargo, los resguardos simultáneos pueden tener conflictos "genuinos" y requieren la intervención del usuario para determinar la reconciliación correcta.
Obviamente, volver a unir el historial no forma parte de los fragmentos anteriores.
Desde entonces, descubrí django-reversión , que parece funcionar bien y se mantiene de forma activa, aunque no hace diferencias para almacenar eficientemente pequeñas diferencias en pedazos grandes de texto.
El problema de almacenamiento: creo que solo debe almacenar las diferencias de dos versiones válidas consecutivas del documento. Como usted señala, el problema se vuelve obtener una versión válida cuando ocurren ediciones simultáneas.
El problema de concurrencia:
- ¿Podrías evitarlos todos juntos como Jeff sugiere o al bloquear el documento?
- De lo contrario, creo que en última instancia estás en el paradigma de editores colaborativos en línea en tiempo real como Google Docs .
Para obtener una vista ilustrada de la lata de gusanos que está abriendo, vea esta charla tecnológica de Google en 9m21s (se trata de la edición colaborativa en tiempo real de Eclipse)
Alternativamente, hay un par de patentes que detallan maneras de lidiar con estas concurrencias en el artículo de Wikipedia sobre editores colaborativos en tiempo real .