Tabellen sperren mit beliebigem ORM?

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Ich glaube, ich steh auf dem Schlauch. Ich möchte in einer Datenbank transaktionssicher prüfen, ob ein Datensatz vorhanden ist und ihn dann ändern oder aber einen neuen anlegen (und den dann ändern). In Python würde das so aussehen:

Code: Alles auswählen

activity = person.activities.find_or_create(kind='whatever')
activity.occurence += 1
Für den naiven Ansatz, bräuchte ich diese Sequenz von SQL-Befehle:

Code: Alles auswählen

select * from activity where person_id=... and kind='whatever'
    ; falls es nicht existiert
    insert into activity ('kind', 'occurences') values ('whatever', 0)
    ; bestimme jetzt last_insert_id oder wie das auch immer heißt
    ; ende von falls
    update activity set occurence=occurence+1 where id=...
Oder man nutzt aus, dass ein Update ein beliebiges `where` haben kann und zurückliefert, wie viele Zeilen betroffen waren. Dann schiebt man ein Insert hinterher, falls es 0 waren.

Wie auch immer, es sind mindestens zwei Befehle und dazwischen kann ein anderer Prozess Schaden anrichten, in dem er's auch versucht. Das ganze ist nicht atomar. Ich muss irgendwie die Tabelle in meiner Transaktion sperren.

Bitte nennt mir einen Python-ORM eurer Wahl und eine Lösung, wie und warum die funktioniert. Aus dunkler Vorzeit erinnere ich mich noch "select for update" als eine Lösung für das Problem, aber sowas ist Datenbank spezifisch und wo wäre der Sinn eines ORM, wenn ich nicht von SQL abstrahieren könnte?

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Zu früh gepostet. Bei SQLalchemy gibt es offenbar ein `for_update=True`, was dann für Oracle und Mysql richtig ausgeführt wird. Bei SQLObject kann man ebenfalls `forUpdate=True` sagen. Nur Storm scheint das nicht zu können...

Problem bleibt natürlich, dass je nach Datenbank das ganze dann ignoriert wird und das Programm fehlerhaft wird. SQL ist schon blöd.

Stefan
Benutzeravatar
veers
User
Beiträge: 1219
Registriert: Mittwoch 28. Februar 2007, 20:01
Wohnort: Zürich (CH)
Kontaktdaten:

Wäre es nicht sinnvoll einfach eine Transaktion zu verwenen?
[url=http://29a.ch/]My Website - 29a.ch[/url]
"If privacy is outlawed, only outlaws will have privacy." - Phil Zimmermann
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

veers hat geschrieben:Wäre es nicht sinnvoll einfach eine Transaktion zu verwenen?
Ich glaube nicht, dass das hilft. Nehmen wir den höchsten "transaction isolation level" SERIALIZABLE an. Dort stellt die DB sicher, dass in einer ersten Transaktion die Änderungen in einer zweiten parallelen nicht sichtbar sind.

Spielen wir es einmal durch: Prozess 1 macht ein UPDATE um dabei festzustellen, dass noch kein Datensatz existiert. Prozess 1 beschließt also ein INSERT zu machen. Jetzt kommt Prozess 2 und macht ein UPDATE, um ebenfalls festzustellen, dass ein INSERT notwendig ist. Nach meinem Verständnis können diese beiden in einer Transkation parallel stattfinden. Das sich beide Prozesse für ein INSERT entschieden haben, ist aber ein Fehler.

Hinzu kommt, dass nur wenige DBs SERIALIZABLE unterstützen. Oracle oder PostgreSQL können das nicht, sondern bieten "snapshot isolation". Wikipedia erklärt, wo der Unterschied liegt.

Ein SELECT FOR UPDATE hilft mir aber glaube ich auch nicht. Prozess 1 macht ein SELECT. Wäre der Datensatz da, wäre er jetzt gesperrt und ein zweites SELECT in einem zweiten Prozess bliebe hängen. Ist er aber nicht da, kann der zweite Prozess munter auch wieder entscheiden, ein INSERT zu machen, ist es kommt zu einem Fehler.

Stefan
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Habe das gerade mal mit sqlite ausprobiert: Konkurrierende selects sind möglich, allerdings gelingt nur ein insert, das zweite führt zu einem Fehler. Interessanterweise führt danach auch ein commit im ersten Prozess zu dem selben Fehler. Der zweite Prozess muss erst ein rollback machen, bevor der erste weitermachen kann. Sqlite scheint ein Datenbank-weites lock zu setzen, wenn eine Transaktion etwas ändert. Damit lässt sich natürlich mein Problem lösen, wenn es auch weit über das Ziel hinaus schließt.

Der ORM Storm kommt damit leider nicht klar, beide Transaktionen schlagen fehlt. Wie das bei andere RDBs ist, habe ich nicht ausprobiert, aber ich bin mir fast sicher, dass das hochgradig DB-spezifisch ist. Grummel.

Stefan
Antworten