¿Hay alguna forma de protegerse contra modificaciones simultáneas de la misma entrada de la base de datos por parte de dos o más usuarios?
Sería aceptable mostrar un mensaje de error al usuario que realiza la segunda operación de confirmación / guardado, pero los datos no deben sobrescribirse en silencio.
Creo que bloquear la entrada no es una opción, ya que un usuario puede usar el botón "Atrás" o simplemente cerrar su navegador, dejando el candado para siempre.
Respuestas:
Así es como hago el bloqueo optimista en Django:
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\ .update(updated_field=new_value, version=e.version+1) if not updated: raise ConcurrentModificationException()
El código enumerado anteriormente se puede implementar como método en Custom Manager .
Estoy haciendo las siguientes suposiciones:
Estas suposiciones son suficientes para garantizar que nadie más haya actualizado la entrada antes. Si se actualizan varias filas de esta manera, debe utilizar transacciones.
ADVERTENCIA Django Doc :
fuente
filter
, ambos reciben una lista idéntica sin modificare
y luego ambos llaman al mismo tiempoupdate
? No veo ningún semáforo que bloquee el filtrado y la actualización simultáneamente. EDITAR: oh, ahora entiendo el filtro perezoso. Pero, ¿cuál es la validez de asumir que update () es atómico? seguramente la base de datos maneja el acceso concurrenteEsta pregunta es un poco vieja y mi respuesta un poco tardía, pero después de lo que entiendo, esto se ha solucionado en Django 1.4 usando:
select_for_update(nowait=True)
ver los documentos
Por supuesto, esto solo funcionará si el back-end admite la función "seleccionar para actualización", que por ejemplo, sqlite no lo hace. Desafortunadamente:
nowait=True
no es compatible con MySql, allí debe usar:,nowait=False
que solo se bloqueará hasta que se libere el bloqueo.fuente
En realidad, las transacciones no le ayudan mucho aquí ... a menos que desee que las transacciones se ejecuten en varias solicitudes HTTP (que probablemente no desee).
Lo que usamos habitualmente en esos casos es "Bloqueo optimista". El ORM de Django no lo admite hasta donde yo sé. Pero ha habido alguna discusión sobre la adición de esta función.
Así que estás solo. Básicamente, lo que debe hacer es agregar un campo de "versión" a su modelo y pasarlo al usuario como un campo oculto. El ciclo normal para una actualización es:
Para implementar el bloqueo optimista, cuando guarda los datos, verifica si la versión que recibió del usuario es la misma que la de la base de datos, y luego actualice la base de datos e incremente la versión. Si no es así, significa que ha habido un cambio desde que se cargaron los datos.
Puede hacer eso con una sola llamada SQL con algo como:
UPDATE ... WHERE version = 'version_from_user';
Esta llamada actualizará la base de datos solo si la versión sigue siendo la misma.
fuente
Django 1.11 tiene tres opciones convenientes para manejar esta situación según los requisitos de la lógica de su negocio:
Something.objects.select_for_update()
bloqueará hasta que el modelo quede libreSomething.objects.select_for_update(nowait=True)
y detectarDatabaseError
si el modelo está actualmente bloqueado para actualizarSomething.objects.select_for_update(skip_locked=True)
no devolverá los objetos que están actualmente bloqueadosEn mi aplicación, que tiene flujos de trabajo interactivos y por lotes en varios modelos, encontré estas tres opciones para resolver la mayoría de mis escenarios de procesamiento concurrente.
La "espera"
select_for_update
es muy conveniente en los procesos secuenciales por lotes; quiero que se ejecuten todos, pero déjeles que se tomen su tiempo. Senowait
usa cuando un usuario desea modificar un objeto que actualmente está bloqueado para actualizarse; solo les diré que se está modificando en este momento.El
skip_locked
es útil para otro tipo de actualización, cuando los usuarios pueden activar una nueva exploración de un objeto, y no me importa quién lo active, siempre que se active, por loskip_locked
que me permite omitir silenciosamente los activadores duplicados.fuente
Para referencia futura, consulte https://github.com/RobCombs/django-locking . Se bloquea de una manera que no deja bloqueos eternos, mediante una combinación de desbloqueo de JavaScript cuando el usuario abandona la página y tiempos de espera de bloqueo (por ejemplo, en caso de que el navegador del usuario se bloquee). La documentación es bastante completa.
fuente
Probablemente debería usar el middleware de transacciones de django al menos, incluso independientemente de este problema.
En cuanto a su problema real de tener varios usuarios editando los mismos datos ... sí, use el bloqueo. O:
Verifique con qué versión está actualizando un usuario (¡hágalo de forma segura, para que los usuarios no puedan simplemente piratear el sistema para decir que estaban actualizando la última copia!), Y solo actualice si esa versión es actual. De lo contrario, envíe al usuario una nueva página con la versión original que estaba editando, la versión enviada y las nuevas versiones escritas por otros. Pídales que combinen los cambios en una versión completamente actualizada. Puede intentar fusionarlos automáticamente con un conjunto de herramientas como diff + patch, pero de todos modos necesitará que el método de fusión manual funcione para los casos de falla, así que comience con eso. Además, deberá preservar el historial de versiones y permitir que los administradores reviertan los cambios, en caso de que alguien, involuntaria o intencionalmente, eche a perder la combinación. Pero probablemente deberías tener eso de todos modos.
Es muy probable que haya una aplicación / biblioteca de django que haga la mayor parte de esto por usted.
fuente
Otra cosa a buscar es la palabra "atómico". Una operación atómica significa que el cambio de su base de datos ocurrirá con éxito o fallará obviamente. Una búsqueda rápida muestra esta pregunta sobre operaciones atómicas en Django.
fuente
La idea de arriba
updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\ .update(updated_field=new_value, version=e.version+1) if not updated: raise ConcurrentModificationException()
se ve muy bien y debería funcionar bien incluso sin transacciones serializables.
El problema es cómo aumentar el comportamiento de .save () sordo para no tener que hacer plomería manual para llamar al método .update ().
Miré la idea del Administrador personalizado.
Mi plan es anular el método Manager _update al que llama Model.save_base () para realizar la actualización.
Este es el código actual en Django 1.3
def _update(self, values, **kwargs): return self.get_query_set()._update(values, **kwargs)
Lo que hay que hacer en mi humilde opinión es algo como:
def _update(self, values, **kwargs): #TODO Get version field value v = self.get_version_field_value(values[0]) return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)
Algo similar debe suceder al eliminar. Sin embargo, eliminar es un poco más difícil ya que Django está implementando algo de vudú en esta área a través de django.db.models.deletion.Collector.
Es extraño que una herramienta moderna como Django carezca de orientación para Optimictic Concurency Control.
Actualizaré esta publicación cuando resuelva el acertijo. Con suerte, la solución será de una manera pitónica agradable que no implique toneladas de codificación, vistas extrañas, omitir piezas esenciales de Django, etc.
fuente
Para estar seguro, la base de datos debe admitir transacciones .
Si los campos son de "formato libre", por ejemplo, texto, etc. y necesita permitir que varios usuarios puedan editar los mismos campos (no puede tener la propiedad de un solo usuario para los datos), puede almacenar los datos originales en un variable. Cuando el usuario se compromete, verifique si los datos de entrada han cambiado de los datos originales (si no, no necesita molestar a la base de datos reescribiendo los datos antiguos), si los datos originales comparados con los datos actuales en la base de datos son los mismos puede guardar, si ha cambiado, puede mostrarle al usuario la diferencia y preguntarle qué hacer.
Si los campos son números, por ejemplo, saldo de la cuenta, número de artículos en una tienda, etc., puede manejarlo de manera más automática si calcula la diferencia entre el valor original (almacenado cuando el usuario comenzó a completar el formulario) y el nuevo valor que puede iniciar una transacción leer el valor actual y agregar la diferencia, luego finalizar la transacción. Si no puede tener valores negativos, debe cancelar la transacción si el resultado es negativo e informar al usuario.
No sé django, así que no puedo darte los cod3s ..;)
fuente
Desde aquí:
Cómo evitar sobrescribir un objeto que otra persona ha modificado
Supongo que la marca de tiempo se mantendrá como un campo oculto en el formulario del que está intentando guardar los detalles.
def save(self): if(self.id): foo = Foo.objects.get(pk=self.id) if(foo.timestamp > self.timestamp): raise Exception, "trying to save outdated Foo" super(Foo, self).save()
fuente