Django: ¿Cómo puedo protegerme contra la modificación simultánea de las entradas de la base de datos?

Si hay una forma de protegerse contra las modificaciones concurrentes de la misma entrada de la base de datos por 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 silenciosamente.

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 locking para siempre.

Así es como hago un locking 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 un método en Custom Manager .

Estoy haciendo las siguientes suposiciones:

  • filter (). update () dará como resultado una única consulta de base de datos porque el filtro es flojo
  • una consulta de base de datos es atómica

Estas suposiciones son suficientes para garantizar que nadie más haya actualizado la entrada antes. Si se actualizan varias filas de esta manera, debe usar las transacciones.

ADVERTENCIA Django Doc :

Tenga en cuenta que el método update () se convierte directamente a una statement SQL. Es una operación masiva para actualizaciones directas. No ejecuta ningún método save () en sus modelos, ni emite las señales pre_save o post_save

Esta pregunta es un poco vieja y mi respuesta un poco tarde, pero después de lo que entiendo, esto se ha corregido en Django 1.4 usando:

 select_for_update(nowait=True) 

ver los documentos

Devuelve un conjunto de consulta que bloqueará las filas hasta el final de la transacción, generando una instrucción SELECT … FOR UPDATE SQL en las bases de datos compatibles.

Por lo general, si otra transacción ya ha adquirido un locking en una de las filas seleccionadas, la consulta se bloqueará hasta que se libere el locking. Si este no es el comportamiento que desea, llame a select_for_update (nowait = True). Esto hará que la llamada no sea bloqueada. Si un locking en conflicto ya ha sido adquirido por otra transacción, DatabaseError se generará cuando se evalúe el conjunto de preguntas.

Por supuesto, esto solo funcionará si el servidor admite la función “seleccionar para actualizar”, que por ejemplo sqlite no funciona. Desafortunadamente: nowait=True no es compatible con MySql, allí tienes que usar: nowait=False , que solo se bloqueará hasta que se libere el locking.

En realidad, las transacciones no te ayudan mucho aquí … a menos que quieras que las transacciones se ejecuten en múltiples solicitudes HTTP (lo que probablemente no quieras).

Lo que generalmente usamos en esos casos es “Bloqueo optimista”. El ORM de Django no lo admite, hasta donde yo sé. Pero ha habido cierta discusión sobre agregar esta característica.

Entonces 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:

  1. leer los datos y mostrarlos al usuario
  2. usuario modificar datos
  3. usuario publicar los datos
  4. la aplicación la guarda en la base de datos.

Para implementar el locking optimista, cuando guarde los datos, verifique si la versión que obtuvo del usuario es la misma que la de la base de datos, luego actualice la base de datos e incremente la versión. Si no lo son, 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.

Django 1.11 tiene tres opciones convenientes para manejar esta situación dependiendo de los requisitos de su lógica de negocios:

  • Something.objects.select_for_update() se bloqueará hasta que el modelo se vuelva gratuito
  • Something.objects.select_for_update(nowait=True) y captura DatabaseError si el modelo está actualmente bloqueado para la actualización
  • Something.objects.select_for_update(skip_locked=True) no devolverá los objetos que están actualmente bloqueados

En 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 select_for_update “esperar” select_for_update es muy conveniente en procesos por lotes secuenciales: quiero que se ejecuten todos, pero que se tomen su tiempo. El nowait se usa cuando un usuario quiere modificar un objeto que está actualmente bloqueado para la actualización; simplemente le diré que está siendo modificado en este momento.

skip_locked es útil para otro tipo de actualización, cuando los usuarios pueden desencadenar un nuevo análisis de un objeto, y no me importa quién lo skip_locked , siempre que se skip_locked , así que skip_locked me permite omitir silenciosamente los disparadores duplicados.

Para referencia futura, consulte https://github.com/RobCombs/django-locking . Bloquea de una manera que no deja lockings eternos, mediante una combinación de javascript que se desbloquea cuando el usuario abandona la página y bloquea los tiempos de espera (por ejemplo, en caso de que el navegador del usuario falle). La documentación es bastante completa.

Probablemente deberías usar el middleware de transacción django al menos, incluso sin importar este problema.

En cuanto a su problema real de tener múltiples usuarios editando los mismos datos … sí, use el locking. O:

Compruebe qué versión está actualizando un usuario (¡haga esto 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 página nueva con la versión original que estaba editando, su versión enviada y la (s) nueva (s) versión (es) escrita (s) por otros. Pídales que combinen los cambios en una versión completamente actualizada. Puede tratar de fusionarlos automáticamente usando un conjunto de herramientas como diff + patch, pero tendrá que tener el método de fusión manual funcionando para los casos de falla de todos modos, así que empiece con eso. Además, deberá conservar el historial de versiones y permitir que los administradores reviertan los cambios, en caso de que alguien involuntariamente o intencionalmente arruine la fusión. Pero probablemente deberías tener eso de todos modos.

Es muy probable que una aplicación / biblioteca django haga la mayor parte de esto por usted.

Otra cosa que debes buscar es la palabra “atómico”. Una operación atómica significa que su cambio en la base de datos ocurrirá con éxito o fallará obviamente. Una búsqueda rápida muestra esta pregunta sobre las operaciones atómicas en Django.

La idea anterior

 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 boost el comportamiento .save () de deafult 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 que es llamado por 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 debe hacerse en mi humilde opinión es algo así 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 en la eliminación. Sin embargo, eliminar es un poco más difícil ya que Django está implementando bastante vudú en esta área a través de django.db.models.deletion.Collector.

Es extraño que la herramienta Modren como Django carezca de orientación para el Control de Contención Optimictic.

Actualizaré esta publicación cuando resuelva el enigma. Afortunadamente, la solución será de una manera pythonica agradable que no implique toneladas de encoding, vistas raras, omitiendo piezas esenciales de Django, etc.

Para estar seguro, la base de datos debe admitir transacciones .

Si los campos son de “forma 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 de los datos), puede almacenar los datos originales en un variable. Cuando el usuario se compromete, compruebe si los datos de entrada han cambiado desde los datos originales (de lo contrario, no necesita molestar al DB reescribiendo los datos antiguos), si los datos originales comparados con los datos actuales en el db 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, cantidad de artículos en una tienda, etc., puede manejarlo más automáticamente si calcula la diferencia entre el valor original (almacenado cuando el usuario comenzó a completar el formulario) y el nuevo valor que puede comience una transacción, lea el valor actual y agregue la diferencia, luego finalice la transacción. Si no puede tener valores negativos, debe cancelar la transacción si el resultado es negativo y avisarle al usuario.

No sé django, entonces no puedo darte los cod3s …;)

De 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á tratando de 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() 
    Intereting Posts