Django: Wie kann ich mich vor gleichzeitigen Änderungen von Datenbankeinträgen schützen?

80

Gibt es eine Möglichkeit, sich vor gleichzeitigen Änderungen desselben Datenbankeintrags durch zwei oder mehr Benutzer zu schützen?

Es wäre akzeptabel, dem Benutzer, der den zweiten Commit / Save-Vorgang ausführt, eine Fehlermeldung anzuzeigen, aber die Daten sollten nicht stillschweigend überschrieben werden.

Ich denke, das Sperren des Eintrags ist keine Option, da ein Benutzer möglicherweise die Schaltfläche "Zurück" verwendet oder einfach seinen Browser schließt und die Sperre für immer verlässt.

Ber
quelle
4
Wenn ein Objekt von mehreren gleichzeitigen Benutzern aktualisiert werden kann, liegt möglicherweise ein größeres Entwurfsproblem vor. Es kann sinnvoll sein, über benutzerspezifische Ressourcen nachzudenken oder Verarbeitungsschritte in separate Tabellen zu unterteilen, um zu verhindern, dass dies ein Problem darstellt.
S.Lott

Antworten:

48

So mache ich in Django optimistisches Sperren:

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

Der oben aufgeführte Code kann als Methode in Custom Manager implementiert werden .

Ich mache folgende Annahmen:

  • filter (). update () führt zu einer einzelnen Datenbankabfrage, da der Filter faul ist
  • Eine Datenbankabfrage ist atomar

Diese Annahmen reichen aus, um sicherzustellen, dass niemand zuvor den Eintrag aktualisiert hat. Wenn mehrere Zeilen auf diese Weise aktualisiert werden, sollten Sie Transaktionen verwenden.

WARNUNG Django Doc :

Beachten Sie, dass die update () -Methode direkt in eine SQL-Anweisung konvertiert wird. Es ist eine Massenoperation für direkte Updates. Auf Ihren Modellen werden keine save () -Methoden ausgeführt oder die Signale pre_save oder post_save ausgegeben

Andrei Savu
quelle
12
Nett! Sollte das nicht '&' statt '&&' sein?
Giles Thomas
1
Könnten Sie das Problem umgehen, dass 'update' keine save () -Methoden ausführt, indem Sie den Aufruf von 'update' in Ihre eigene überschriebene save () -Methode einfügen?
Jonathan Hartley
1
Was passiert, wenn zwei Threads gleichzeitig aufrufen filter, beide eine identische Liste mit unverändert erhalten eund dann beide gleichzeitig aufrufen update? Ich sehe kein Semaphor, das Filter und Update gleichzeitig blockiert. EDIT: Oh, ich verstehe jetzt Lazy Filter. Aber wie gültig ist die Annahme, dass update () atomar ist?
Sicherlich
1
@totowtwo Das I in ACID garantiert die Bestellung ( en.wikipedia.org/wiki/ACID ). Wenn ein UPDATE für Daten ausgeführt wird, die sich auf ein gleichzeitiges (aber später gestartetes) SELECT beziehen, wird es blockiert, bis das UPDATE abgeschlossen ist. Es können jedoch mehrere SELECT gleichzeitig ausgeführt werden.
Kit Sunde
1
Sieht so aus, als würde dies nur im Autocommit-Modus (der Standard ist) ordnungsgemäß funktionieren. Andernfalls wird final COMMIT von dieser aktualisierenden SQL-Anweisung getrennt, sodass gleichzeitig Code zwischen ihnen ausgeführt werden kann. Und wir haben die ReadCommited-Isolationsstufe in Django, sodass die alte Version gelesen wird. (Warum ich hier eine manuelle Transaktion möchte - weil ich zusammen mit diesem Update eine Zeile in einer anderen Tabelle erstellen möchte.) Tolle Idee.
Alex Lokk
39

Diese Frage ist etwas alt und meine Antwort etwas spät, aber nach dem, was ich verstehe, wurde dies in Django 1.4 behoben, indem:

select_for_update(nowait=True)

Siehe die Dokumente

Gibt ein Abfrageset zurück, das Zeilen bis zum Ende der Transaktion sperrt und eine SQL-Anweisung SELECT ... FOR UPDATE für unterstützte Datenbanken generiert.

Wenn eine andere Transaktion bereits eine Sperre für eine der ausgewählten Zeilen erhalten hat, wird die Abfrage normalerweise blockiert, bis die Sperre aufgehoben wird. Wenn dies nicht das gewünschte Verhalten ist, rufen Sie select_for_update auf (nowait = True). Dadurch wird der Anruf nicht blockiert. Wenn eine widersprüchliche Sperre bereits von einer anderen Transaktion erfasst wurde, wird DatabaseError ausgelöst, wenn das Abfrageset ausgewertet wird.

Dies funktioniert natürlich nur, wenn das Back-End die Funktion "Für Update auswählen" unterstützt, was beispielsweise bei SQLite nicht der Fall ist. Leider: nowait=Truewird von MySql nicht unterstützt, da muss man: verwenden nowait=False, das nur blockiert, bis die Sperre aufgehoben wird.

giZm0
quelle
2
Dies ist keine gute Antwort - die Frage wollte ausdrücklich kein (pessimistisches) Sperren, und die beiden Antworten mit den höheren Stimmen konzentrieren sich aus diesem Grund derzeit auf eine optimistische Parallelitätskontrolle ("optimistisches Sperren"). Die Auswahl für die Aktualisierung ist jedoch in anderen Situationen in Ordnung.
RichVel
@ giZm0 Das macht es immer noch pessimistisch sperren. Der erste Thread, der das Schloss erhält, kann es unbegrenzt halten.
Knaperek
6
Ich mag diese Antwort, weil sie aus der Dokumentation von Django stammt und keine schöne Erfindung eines Dritten ist.
Anizzomc
28

Tatsächlich helfen Ihnen Transaktionen hier nicht viel ... es sei denn, Sie möchten, dass Transaktionen über mehrere HTTP-Anforderungen ausgeführt werden (was Sie höchstwahrscheinlich nicht möchten).

Was wir normalerweise in diesen Fällen verwenden, ist "Optimistic Locking". Das Django ORM unterstützt das meines Wissens nicht. Es gab jedoch einige Diskussionen über das Hinzufügen dieser Funktion.

Sie sind also alleine. Grundsätzlich sollten Sie Ihrem Modell ein "Versions" -Feld hinzufügen und es als verstecktes Feld an den Benutzer übergeben. Der normale Zyklus für ein Update ist:

  1. Lesen Sie die Daten und zeigen Sie sie dem Benutzer
  2. Benutzer ändern Daten
  3. Benutzer posten die Daten
  4. Die App speichert es wieder in der Datenbank.

Um eine optimistische Sperrung zu implementieren, überprüfen Sie beim Speichern der Daten, ob die vom Benutzer zurückgegebene Version mit der in der Datenbank übereinstimmt, aktualisieren Sie die Datenbank und erhöhen Sie die Version. Wenn dies nicht der Fall ist, bedeutet dies, dass seit dem Laden der Daten eine Änderung vorgenommen wurde.

Sie können dies mit einem einzelnen SQL-Aufruf tun, mit:

UPDATE ... WHERE version = 'version_from_user';

Dieser Aufruf aktualisiert die Datenbank nur, wenn die Version noch identisch ist.

Guillaume
quelle
1
Dieselbe Frage tauchte auch bei Slashdot auf. Das von Ihnen vorgeschlagene optimistische Sperren wurde ebenfalls dort vorgeschlagen, aber imho etwas besser erklärt: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367
hopla
5
Beachten Sie auch, dass Sie darüber hinaus Transaktionen verwenden möchten, um diese Situation zu vermeiden: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django bietet Middleware, mit der jede Aktion in der Datenbank automatisch in eine Transaktion eingeschlossen wird Von der ersten Anforderung an und erst nach einer erfolgreichen Antwort festschreiben : docs.djangoproject.com/de/dev/topics/db/transactions ( wohlgemerkt : Die Transaktions-Middleware hilft nur, das oben genannte Problem mit optimistischem Sperren zu vermeiden, bietet jedoch kein Sperren von selbst)
hopla
Ich suche auch nach Details, wie das geht. Bisher kein Glück.
Seanyboy
1
Sie können dies tun, indem Sie Django-Bulk-Updates verwenden. Überprüfen Sie meine Antwort.
Andrei Savu
13

Django 1.11 bietet drei praktische Optionen , um diese Situation abhängig von Ihren Anforderungen an die Geschäftslogik zu bewältigen:

  • Something.objects.select_for_update() wird blockiert, bis das Modell frei wird
  • Something.objects.select_for_update(nowait=True)und abfangen, DatabaseErrorob das Modell derzeit für die Aktualisierung gesperrt ist
  • Something.objects.select_for_update(skip_locked=True) gibt die aktuell gesperrten Objekte nicht zurück

In meiner Anwendung, die sowohl interaktive als auch Batch-Workflows für verschiedene Modelle enthält, habe ich diese drei Optionen gefunden, um die meisten meiner gleichzeitigen Verarbeitungsszenarien zu lösen.

Das "Warten" select_for_updateist sehr praktisch in sequentiellen Batch-Prozessen - ich möchte, dass sie alle ausgeführt werden, aber lassen Sie sie sich Zeit. Das nowaitwird verwendet, wenn ein Benutzer ein Objekt ändern möchte, das derzeit für die Aktualisierung gesperrt ist. Ich werde ihm nur mitteilen, dass es derzeit geändert wird.

Das skip_lockedist für eine andere Art von Update nützlich, wenn Benutzer eine erneute Prüfung eines Objekts auslösen können - und ich weiß nicht , wer es auslöst, solange es ausgelöst wird , so skip_lockedermöglicht es mir , die duplizierten Auslöser leise zu überspringen.

Kravietz
quelle
1
Muss ich die Auswahl für die Aktualisierung mit transaction.atomic () umschließen? Wenn ich die Ergebnisse tatsächlich für ein Update verwende? Wird nicht die gesamte Tabelle gesperrt, wodurch select_for_update zum Noop wird?
Paul Kenjora
3

Weitere Informationen finden Sie unter https://github.com/RobCombs/django-locking . Das Sperren erfolgt auf eine Weise, die keine ewigen Sperren hinterlässt, indem Javascript beim Verlassen der Seite entsperrt wird und Zeitüberschreitungen gesperrt werden (z. B. falls der Browser des Benutzers abstürzt). Die Dokumentation ist ziemlich vollständig.

Stijn Debrouwere
quelle
3
Ich meine, das ist eine wirklich seltsame Idee.
Julx
1

Sie sollten wahrscheinlich zumindest die Django-Transaktions-Middleware verwenden, auch unabhängig von diesem Problem.

Was Ihr eigentliches Problem betrifft, dass mehrere Benutzer dieselben Daten bearbeiten ... ja, verwenden Sie die Sperrung. ODER:

Überprüfen Sie, gegen welche Version ein Benutzer aktualisiert (tun Sie dies sicher, damit Benutzer das System nicht einfach hacken können, um zu sagen, dass sie die neueste Kopie aktualisiert haben!), Und aktualisieren Sie nur, wenn diese Version aktuell ist. Andernfalls senden Sie dem Benutzer eine neue Seite mit der Originalversion, die er bearbeitet hat, der eingereichten Version und den neuen Versionen, die von anderen geschrieben wurden, zurück. Bitten Sie sie, die Änderungen in einer vollständig aktuellen Version zusammenzuführen. Sie können versuchen, diese mithilfe eines Toolset wie diff + patch automatisch zusammenzuführen, aber die manuelle Zusammenführungsmethode muss ohnehin für Fehlerfälle funktionieren. Beginnen Sie also damit. Außerdem müssen Sie den Versionsverlauf beibehalten und Administratoren erlauben, Änderungen rückgängig zu machen, falls jemand die Zusammenführung unbeabsichtigt oder absichtlich durcheinander bringt. Aber das solltest du wahrscheinlich trotzdem haben.

Es gibt sehr wahrscheinlich eine Django-App / Bibliothek, die das meiste für Sie erledigt.

Lee B.
quelle
Dies ist auch Optimistic Locking, wie Guillaume vorgeschlagen hat. Aber er schien alle Punkte zu bekommen :)
hopla
0

Eine andere Sache zu suchen ist das Wort "atomar". Eine atomare Operation bedeutet, dass Ihre Datenbankänderung entweder erfolgreich durchgeführt wird oder offensichtlich fehlschlägt. Eine schnelle Suche zeigt diese Frage nach atomaren Operationen in Django.

Harley Holcombe
quelle
Ich möchte keine Transaktion durchführen oder mehrere Anforderungen sperren, da dies eine beliebige Zeitspanne dauern kann (und möglicherweise überhaupt nicht abgeschlossen wird)
Ber
Wenn eine Transaktion startet, muss sie beendet werden. Sie sollten den Datensatz erst sperren (oder die Transaktion starten oder was auch immer Sie tun möchten), nachdem der Benutzer auf "Senden" geklickt hat, und nicht, wenn er den Datensatz zur Anzeige öffnet.
Harley Holcombe
Ja, aber mein Problem ist insofern unterschiedlich, als zwei Benutzer dasselbe Formular öffnen und dann beide ihre Änderungen festschreiben. Ich denke nicht, dass das Sperren die Lösung dafür ist.
Ber
Sie haben Recht, aber das Problem ist, dass es dafür keine Lösung gibt. Ein Benutzer gewinnt, der andere erhält eine Fehlermeldung. Je später Sie den Datensatz sperren, desto weniger Probleme treten auf.
Harley Holcombe
Genau. Ich akzeptiere die Fehlermeldung für den anderen Benutzer vollständig. Ich suche nach einer guten Möglichkeit, diesen Fall zu erkennen (von dem ich erwarte, dass er sehr selten ist).
Ber
0

Die Idee oben

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

sieht gut aus und sollte auch ohne serialisierbare Transaktionen gut funktionieren.

Das Problem besteht darin, wie das taube Verhalten von .save () erweitert werden kann, damit keine manuelle Installation durchgeführt werden muss, um die Methode .update () aufzurufen.

Ich habe mir die Idee des Custom Managers angesehen.

Mein Plan ist es, die von Model.save_base () aufgerufene Manager _update-Methode zu überschreiben, um das Update durchzuführen.

Dies ist der aktuelle Code in Django 1.3

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

Was IMHO getan werden muss, ist so etwas wie:

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)

Ähnliches muss beim Löschen passieren. Das Löschen ist jedoch etwas schwieriger, da Django über django.db.models.deletion.Collector in diesem Bereich ziemlich viel Voodoo implementiert.

Es ist seltsam, dass Modren-Tools wie Django keine Anleitung für Optimictic Concurency Control haben.

Ich werde diesen Beitrag aktualisieren, wenn ich das Rätsel löse. Hoffentlich wird die Lösung auf eine nette pythonische Art und Weise sein, die nicht jede Menge Codierung, seltsame Ansichten, das Überspringen wesentlicher Teile von Django usw. beinhaltet.

Kiril
quelle
-2

Um sicher zu gehen, muss die Datenbank Transaktionen unterstützen .

Wenn es sich bei den Feldern um "Freiform" handelt, z. B. Text usw., und Sie mehreren Benutzern erlauben müssen, dieselben Felder zu bearbeiten (Sie können nicht die Berechtigung eines einzelnen Benutzers für die Daten haben), können Sie die Originaldaten in einem speichern Variable. Überprüfen Sie beim Festschreiben des Benutzers, ob sich die Eingabedaten gegenüber den Originaldaten geändert haben (wenn nicht, müssen Sie die Datenbank nicht durch Umschreiben alter Daten belästigen), wenn die Originaldaten im Vergleich zu den aktuellen Daten in der Datenbank identisch sind Sie können speichern, wenn es sich geändert hat, können Sie dem Benutzer den Unterschied zeigen und ihn fragen, was zu tun ist.

Wenn es sich bei den Feldern um Zahlen handelt, z. B. Kontostand, Anzahl der Artikel in einem Geschäft usw., können Sie dies automatischer verarbeiten, wenn Sie die Differenz zwischen dem ursprünglichen Wert (gespeichert, als der Benutzer mit dem Ausfüllen des Formulars begann) und dem neuen Wert berechnen, den Sie können Starten Sie eine Transaktion, lesen Sie den aktuellen Wert, addieren Sie die Differenz und beenden Sie die Transaktion. Wenn Sie keine negativen Werte haben können, sollten Sie die Transaktion abbrechen, wenn das Ergebnis negativ ist, und dies dem Benutzer mitteilen.

Ich kenne Django nicht, also kann ich dir die cod3s nicht geben ..;)

Stein G. Strindhaug
quelle
-6

Von hier:
So verhindern Sie das Überschreiben eines Objekts, das von einer anderen Person geändert wurde

Ich gehe davon aus, dass der Zeitstempel als verstecktes Feld in der Form gespeichert wird, in der Sie die Details speichern möchten.

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()
Seanyboy
quelle
1
Der Code ist kaputt. Zwischen der Abfrage if check und save kann weiterhin eine Racebedingung auftreten. Sie müssen objects.filter (id = .. & timestamp check) .update (...) verwenden und eine Ausnahme auslösen, wenn keine Zeile aktualisiert wurde.
Andrei Savu