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 des 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 propagation/sauvegarde, mais les données ne devraient pas être effacées en silence.

je pense que verrouiller l'entrée n'est pas une option, car un utilisateur pourrait utiliser le bouton" Back " ou tout simplement fermer son navigateur, laissant la serrure pour toujours.

74
demandé sur Tony 2008-11-26 12:00:35

10 réponses

c'est comme ça que je fais du verrouillage optimiste à 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 une méthode dans Custom Manager .

je fais les hypothèses suivantes:

  • filter().update () résultera en une seule requête de base de données parce que le filtre est paresseux
  • une requête de base de données est atomique

ces les hypothèses sont assez pour s'assurer que personne d'autre n'a mis à jour l'entrée avant. Si plusieurs lignes sont mises à jour de cette façon, vous devriez utiliser les mouvements.

WARNING Django Doc :

soyez conscient que la méthode update() est converti directement en SQL déclaration. C'est une opération en bloc pour la mise à jour directe. Il ne fonctionne pas tout les méthodes save() sur vos modèles, ou emit le pré_save ou post_save signals

46
répondu Andrei Savu 2014-10-02 22:52:32

en fait, les transactions ne vous aident pas beaucoup ici ... sauf si vous voulez avoir des transactions sur plusieurs requêtes HTTP (ce que vous ne voulez probablement pas).

ce que nous utilisons habituellement dans ces cas-là, c'est le"verrouillage optimiste". L'ORM Django ne soutient pas ça, autant que je sache. Mais il y a eu quelques discussions sur l'ajout de cette fonctionnalité.

donc vous êtes tout seul. Fondamentalement, ce que vous devez faire est d'ajouter un "champ" version de votre modèle et de le transmettre à l'utilisateur un champ caché. 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 affiche les données
  4. l'application Le sauvegarde dans la base de données.

pour mettre en œuvre le verrouillage optimiste, lorsque vous enregistrez les données, vous vérifiez si la version que vous avez récupéré de l'utilisateur est la même que celle de la base de données, puis mettre à jour la base de données et incrémenter la version. Si ce n'est pas le cas, cela signifie qu'il y a eu un changement depuis que les données ont été chargées.

vous pouvez faire cela avec un seul appel SQL avec quelque chose comme:

UPDATE ... WHERE version = 'version_from_user';

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

27
répondu Guillaume 2008-11-26 17:00:37

Django 1.11 a trois options pratiques pour gérer cette situation en fonction de vos besoins de logique d'affaires:

  • Something.objects.select_for_update() 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 renverra pas les objets qui sont actuellement verrouillé

Dans mon application, qui a des flux de travail interactifs et par lots sur divers modèles, j'ai trouvé ces trois options pour résoudre la plupart de mes scénarios de traitement simultané.

le "waiting " 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é quand un utilisateur veut modifier un objet qui est actuellement verrouillé pour mise à jour - je vais juste leur dire qu'il est en train d'être modifié à ce moment.

le skip_locked est utile pour un autre type de mise à jour, lorsque les utilisateurs peuvent déclencher un rescan d'un objet - et je ne me soucie pas qui le déclenche, tant qu'il est déclenché, donc skip_locked me permet de passer silencieusement les déclencheurs dupliqués.

11
répondu kravietz 2017-05-18 10:20:53

pour référence future, cochez https://github.com/RobCombs/django-locking . Il se verrouille d'une manière qui ne laisse pas de serrures éternelles, par un mélange de déverrouillage javascript lorsque l'utilisateur quitte la page, et de temps de verrouillage (par exemple dans le cas où le navigateur de l'utilisateur plante). La documentation est assez complète.

3
répondu Stijn Debrouwere 2012-10-15 16:38:58

vous devriez probablement utiliser l'middleware de transaction de django au moins, même en dépit de ce problème.

à votre problème d'avoir plusieurs utilisateurs modifient les mêmes données... oui, utiliser le verrouillage. Ou:

vérifiez quelle version un utilisateur met à jour (faites cela en toute sécurité, pour que les utilisateurs ne puissent pas simplement pirater le système pour dire qu'ils mettaient à jour la dernière copie!), et ne mettre à jour que si cette version est à jour. Sinon, envoie à l'utilisateur un nouveau page avec la version originale qu'ils éditaient, leur version soumise, et la(Les) nouvelle (s) version (s) écrite (s) par d'autres. Demandez - leur de fusionner les changements en une seule version, complètement à jour. Vous pouvez essayer de les fusionner automatiquement en utilisant un ensemble d'outils comme diff+patch, mais vous aurez besoin de la méthode de fusion manuelle pour les cas d'échec de toute façon, alors commencez par cela. En outre, vous aurez besoin de préserver l'historique de version, et permettre aux administrateurs de revenir sur les changements, dans le cas où quelqu'un involontairement ou intentionnellement mess jusqu'à la fusion. Mais vous devez sans doute avoir cela de toute façon.

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

1
répondu Lee B 2009-09-18 10:42:44

une Autre chose à rechercher est le mot "atomique". Une opération atomique signifie que votre changement de base de données se produira avec succès, ou échouera évidemment. Une recherche rapide montre cette question à propos des opérations atomiques à Django.

0
répondu Harley Holcombe 2017-05-23 11:54:41

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 grand et devrait fonctionner très bien, même sans transactions sérialisables.

le problème est de savoir comment augmenter la surdité .save() le comportement de ne pas avoir à le faire en manuel de la plomberie à l'appel .méthode update ().

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

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

C'est le code courant 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 c'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 sur delete. Cependant, supprimer est un peu plus difficile car Django est en train de mettre en place un certain voodoo dans ce domaine à travers django.DB.modèle.suppression.Collecteur.

il est étrange que l'outil modren comme Django ne soit pas suffisamment guidé pour un contrôle optimal de la concurence.

je mettrai à jour ce post quand 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 bizarres, sauter des pièces essentielles de Django etc.

0
répondu Kiril 2011-07-31 22:40:21

Pour être sûr de la base de données a besoin de soutien transactions .

si les champs sont" en clair", par exemple texte, etc. et vous devez permettre à plusieurs utilisateurs de pouvoir éditer les mêmes champs (vous ne pouvez pas avoir la propriété d'un utilisateur unique sur les données), vous pouvez stocker les données originales dans une variable. Lorsque l'utilisateur s'engage, vérifiez si les données d'entrée ont changé par rapport aux données d'origine (si ce n'est pas le cas, vous n'avez pas besoin de déranger la base de données en réécrivant les anciennes données).), si l' les données originales comparées aux données actuelles dans la base de données est la même que vous pouvez enregistrer, si elle a changé, vous pouvez montrer à l'utilisateur la différence et demander à l'utilisateur ce qu'il doit faire.

si les champs sont des nombres, 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 originale (stockée lorsque l'Utilisateur a commencé à remplir le formulaire) et la nouvelle valeur, vous pouvez commencer une transaction lire la valeur actuelle et ajouter la différence, puis mettre fin à la transaction. Si vous ne pouvez pas avoir de valeur négative, vous devriez annuler la transaction si le résultat est négatif, et en informer l'utilisateur.

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

-1
répondu Stein G. Strindhaug 2008-11-26 09:23:16

D'ici:

comment prévenir l'écrasement d'un objet quelqu'un d'autre a modifié

je suppose que l'horodatage sera tenu comme un champ caché dans la forme que vous essayez de sauver 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()
-6
répondu seanyboy 2017-05-23 10:31:30