web-dev-qa-db-fra.com

Django: Comment puis-je me protéger contre la modification simultanée des entrées de la base de données

S'il existe un moyen de se protéger contre les modifications simultanées de la même entrée de base de données par deux utilisateurs ou plus?

Il serait acceptable d'afficher un message d'erreur à l'utilisateur effectuant la deuxième opération de validation/sauvegarde, mais les données ne devraient pas être écrasées en silence.

Je pense que le verrouillage de l'entrée n'est pas une option, car un utilisateur peut utiliser le bouton "Retour" ou simplement fermer son navigateur, laissant le verrou pour toujours.

76
Ber

Voici comment je fais un verrouillage optimiste dans 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()

Le code ci-dessus peut être implémenté comme méthode dans Custom Manager .

Je fais les hypothèses suivantes:

  • filter (). update () entraînera une seule requête de base de données car le filtre est paresseux
  • une requête de base de données est atomique

Ces hypothèses suffisent pour garantir que personne d'autre n'a mis à jour l'entrée auparavant. Si plusieurs lignes sont mises à jour de cette façon, vous devez utiliser des transactions.

[~ # ~] avertissement [~ # ~] Django Doc :

N'oubliez pas que la méthode update () est convertie directement en une instruction SQL. Il s'agit d'une opération groupée pour les mises à jour directes. Il n'exécute aucune méthode save () sur vos modèles, ni n'émet les signaux pre_save ou post_save

46
Andrei Savu

Cette question est un peu ancienne et ma réponse un peu tardive, mais après ce que je comprends, cela a été corrigé dans Django 1.4 en utilisant:

select_for_update(nowait=True)

voir les docs

Renvoie un ensemble de requêtes qui verrouillera les lignes jusqu'à la fin de la transaction, générant une instruction SQL SELECT ... FOR UPDATE sur les bases de données prises en charge.

Habituellement, si une autre transaction a déjà acquis un verrou sur l'une des lignes sélectionnées, la requête se bloquera jusqu'à ce que le verrou soit libéré. Si ce n'est pas le comportement que vous souhaitez, appelez select_for_update (nowait = True). Cela rendra l'appel non bloquant. Si un verrou conflictuel est déjà acquis par une autre transaction, DatabaseError sera levée lorsque le jeu de requêtes est évalué.

Bien sûr, cela ne fonctionnera que si le back-end prend en charge la fonctionnalité "sélectionner pour la mise à jour", ce qui n'est pas le cas, par exemple, de sqlite. Malheureusement: nowait=True n'est pas supporté par MySql, là vous devez utiliser: nowait=False, qui ne bloquera que lorsque le verrou sera libéré.

35
giZm0

En fait, les transactions ne vous aident pas beaucoup ici ... sauf si vous voulez que les transactions s'exécutent sur plusieurs requêtes HTTP (ce que vous ne voulez probablement pas).

Ce que nous utilisons habituellement dans ces cas est le "verrouillage optimiste". L'ORM Django ne le supporte pas pour autant que je sache. Mais il y a eu des discussions sur l'ajout de cette fonctionnalité.

Vous êtes donc seul. Fondamentalement, vous devez ajouter un champ "version" à votre modèle et le transmettre à l'utilisateur en tant que champ masqué. Le cycle normal d'une mise à jour est:

  1. lire les données et les montrer à l'utilisateur
  2. modifier les données de l'utilisateur
  3. l'utilisateur publie les données
  4. l'application l'enregistre de nouveau dans la base de données.

Pour implémenter un verrouillage optimiste, lorsque vous enregistrez les données, vous vérifiez si la version que vous avez récupérée de l'utilisateur est la même que celle de la base de données, puis mettez à jour la base de données et incrémentez la version. Si ce n'est pas le cas, cela signifie qu'il y a eu un changement depuis le chargement des données.

Vous pouvez le faire avec un seul appel SQL avec quelque chose comme:

UPDATE ... WHERE version = 'version_from_user';

Cet appel mettra à jour la base de données uniquement si la version est toujours la même.

28
Guillaume

Django 1.11 a trois options pratiques pour gérer cette situation en fonction de vos exigences de logique métier:

  • Something.objects.select_for_update() se bloquera jusqu'à ce que le modèle devienne libre
  • Something.objects.select_for_update(nowait=True) et catch DatabaseError si le modèle est actuellement verrouillé pour la mise à jour
  • Something.objects.select_for_update(skip_locked=True) ne retournera pas les objets actuellement verrouillés

Dans mon application, qui propose à la fois des workflows interactifs et par lots sur différents modèles, j'ai trouvé ces trois options pour résoudre la plupart de mes scénarios de traitement simultané.

L'attente" select_for_update est très pratique dans les processus séquentiels par lots - je veux qu'ils s'exécutent tous, mais laissez-les prendre leur temps. Le nowait est utilisé lorsqu'un utilisateur souhaite modifier un objet qui est actuellement verrouillé pour la mise à jour - je vais juste lui dire qu'il est en cours de modification en ce moment.

Le skip_locked est utile pour un autre type de mise à jour, lorsque les utilisateurs peuvent déclencher une nouvelle analyse d'un objet - et peu m'importe qui le déclenche, tant qu'il est déclenché, donc skip_locked me permet d'ignorer silencieusement les déclencheurs dupliqués.

13
kravietz

Pour référence future, consultez https://github.com/RobCombs/Django-locking . Il verrouille d'une manière qui ne laisse pas de verrous éternels, par un mélange de déverrouillage javascript lorsque l'utilisateur quitte la page et de délais d'expiration de verrouillage (par exemple en cas de panne du navigateur de l'utilisateur). La documentation est assez complète.

3
Stijn Debrouwere

Vous devriez probablement utiliser le middleware de transaction Django au moins, même indépendamment de ce problème.

Quant à votre problème réel d'avoir plusieurs utilisateurs éditant les mêmes données ... oui, utilisez le verrouillage. OU:

Vérifiez la version contre laquelle un utilisateur met à jour (faites-le en toute sécurité, afin que les utilisateurs ne puissent pas simplement pirater le système pour dire qu'ils mettaient à jour la dernière copie!), Et ne mettez à jour que si cette version est à jour. Sinon, renvoyez à l'utilisateur une nouvelle page avec la version d'origine qu'il modifiait, sa version soumise et la ou les nouvelles versions écrites par d'autres. Demandez-leur de fusionner les modifications en une seule version complètement à jour. Vous pouvez essayer de les fusionner automatiquement à l'aide d'un ensemble d'outils comme le correctif diff +, mais vous devrez de toute façon avoir la méthode de fusion manuelle pour les cas d'échec, alors commencez par cela. En outre, vous devrez conserver l'historique des versions et permettre aux administrateurs d'annuler les modifications, au cas où quelqu'un gâcherait involontairement ou intentionnellement la fusion. Mais vous devriez probablement l'avoir de toute façon.

Il y a très probablement une Django application/bibliothèque qui fait la plupart de cela pour vous.

1
Lee B

L'idée ci-dessus

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()

a fière allure et devrait fonctionner correctement même sans transactions sérialisables.

Le problème est de savoir comment augmenter le comportement de sourd .save () afin de ne pas avoir à effectuer de plomberie manuelle pour appeler la méthode .update ().

J'ai regardé l'idée du gestionnaire personnalisé.

Mon plan consiste à remplacer la méthode Manager _update qui est appelée par Model.save_base () pour effectuer la mise à jour.

Il s'agit du code actuel dans Django 1.3

def _update(self, values, **kwargs):
   return self.get_query_set()._update(values, **kwargs)

Ce qui doit être fait à mon humble avis est quelque chose comme:

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)

Une chose similaire doit se produire lors de la suppression. Cependant, la suppression est un peu plus difficile car Django implémente pas mal de vaudou dans ce domaine via Django.db.models.deletion.Collector.

Il est étrange que l'outil modren comme Django manque de conseils pour Optimictic Concurency Control.

Je mettrai à jour ce message lorsque je résoudrai l'énigme. Espérons que la solution sera d'une manière Pythonique agréable qui n'implique pas des tonnes de codage, des vues étranges, en sautant des morceaux essentiels de Django etc.

0
Kiril

Une autre chose à rechercher est le mot "atomique". Une opération atomique signifie que le changement de votre base de données se fera avec succès ou échouera évidemment. Une recherche rapide montre cette question poser des questions sur les opérations atomiques dans Django.

0
Harley Holcombe