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




template django (8)

Pour être sûr, la base de données doit prendre en charge les transactions .

Si les champs sont "libres", par exemple du texte, etc. et que vous devez autoriser plusieurs utilisateurs à modifier les mêmes champs (vous ne pouvez pas avoir de propriétaire unique pour les données), vous pouvez stocker les données d'origine dans un variable. Lorsque l'utilisateur valide, vérifiez si les données d'entrée ont changé par rapport aux données d'origine (sinon, vous n'avez pas besoin de déranger le DB en réécrivant les anciennes données), si les données d'origine comparées aux données actuelles dans la db sont les mêmes vous pouvez enregistrer, si elle a changé, vous pouvez montrer à l'utilisateur la différence et demander à l'utilisateur quoi faire.

Si les champs sont des numéros, par exemple le solde du compte, le nombre d'articles dans un magasin, etc., vous pouvez le gérer plus automatiquement si vous calculez la différence entre la valeur d'origine (stockée lorsque l'utilisateur a commencé à remplir le formulaire) et la nouvelle valeur démarrer une transaction lire la valeur actuelle et ajouter la différence, puis terminer la transaction. Si vous ne pouvez pas avoir de valeurs négatives, vous devez interrompre la transaction si le résultat est négatif et en informer l'utilisateur.

Je ne sais pas django, donc je ne peux pas vous donner les cod3s ..;)

S'il existe un moyen de 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 silencieusement.

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.


Voici comment je fais le 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 répertorié ci-dessus peut être implémenté en tant que méthode dans Custom Manager .

Je fais les hypothèses suivantes:

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

Ces hypothèses sont suffisantes pour s'assurer que personne d'autre n'a mis à jour l'entrée avant. Si plusieurs lignes sont mises à jour de cette manière, vous devez utiliser des transactions.

AVERTISSEMENT Django Doc :

Sachez que la méthode update () est directement convertie en une instruction SQL. C'est une opération en bloc 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


D'ici:
Comment éviter d'écraser un objet que quelqu'un d'autre a modifié

Je suppose que l'horodatage sera tenu comme un champ caché sous la forme que vous essayez d'enregistrer les détails de.

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

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

semble très bien et devrait fonctionner correctement, même sans transactions sérialisables.

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

J'ai regardé l'idée de Custom Manager.

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

Ceci est le 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, supprimer est un peu plus difficile car Django implémente un peu de vaudou dans cette zone via django.db.models.deletion.Collector.

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

Je vais mettre à jour ce post quand je résous l'énigme. J'espère que la solution sera d'une belle manière pythonique qui n'implique pas des tonnes de codage, des vues bizarres, en sautant des morceaux essentiels de Django etc.


Django 1.11 dispose de trois options pratiques pour gérer cette situation en fonction des besoins de votre logique métier:

  • Something.objects.select_for_update() bloquera jusqu'à ce que le modèle devienne libre
  • Something.objects.select_for_update(nowait=True) et capture 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 a des workflows interactifs et batch sur différents modèles, j'ai trouvé ces trois options pour résoudre la plupart de mes scénarios de traitement simultanés.

L '"attente" select_for_update est très pratique dans les processus batch séquentiels - je veux qu'ils s'exécutent tous, mais laissez-les prendre leur temps. Le nowait est utilisé quand un utilisateur veut modifier un objet qui est actuellement verrouillé pour la mise à jour - je vais juste leur dire qu'il est en train d'être modifié 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 skip_locked qui le déclenche, tant qu'il est déclenché, donc skip_locked me permet de passer silencieusement les triggers dupliqués.


Pour référence ultérieure, 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 verrouille les délais d'attente (par exemple, dans le cas où le navigateur de l'utilisateur se bloque). La documentation est assez complète.


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

select_for_update(nowait=True)

voir les docs

Renvoie un jeu de requêtes qui verrouille les lignes jusqu'à la fin de la transaction, en 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 bloque jusqu'à ce que le verrou soit libéré. Si ce n'est pas le comportement que vous voulez, 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é lors de l'évaluation du jeu de requêtes.

Bien sûr, cela ne fonctionnera que si le back-end supporte la fonctionnalité "select for update", ce qui n'est pas le cas de sqlite par exemple. Malheureusement: nowait=True n'est pas supporté par MySql, il faut utiliser: nowait=False , qui ne bloquera que jusqu'à la libération du verrou.


Vous devriez probablement utiliser au moins le middleware de transaction django, même si ce n'est pas le cas.

Quant à votre problème actuel 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é, de sorte que les utilisateurs ne puissent pas simplement pirater le système pour dire qu'ils mettent à jour la dernière copie!), Et mettez à jour seulement si cette version est actuelle. Sinon, renvoyez à l'utilisateur une nouvelle page avec la version originale qu'il modifiait, sa version soumise et la ou les nouvelle (s) version (s) écrite (s) par d'autres. Demandez-leur de fusionner les changements en une version complètement mise à jour. Vous pouvez essayer de les fusionner automatiquement à l'aide d'un jeu d'outils tel que diff + patch, mais vous aurez besoin de la méthode de fusion manuelle pour les cas de défaillance, alors commencez par cela. En outre, vous devez conserver l'historique des versions et autoriser les administrateurs à annuler les modifications, au cas où quelqu'un aurait involontairement ou intentionnellement bousillé la fusion. Mais vous devriez probablement avoir ça de toute façon.

Il y a très probablement une application / bibliothèque django qui fait le plus pour vous.





atomic