Konfigurieren eines Versionszählers

Die Mapper unterstützt die Verwaltung einer Versions-ID-Spalte, einer einzelnen Tabellenspalte, die ihren Wert bei jeder UPDATE der zugeordneten Tabelle inkrementiert oder anderweitig aktualisiert. Dieser Wert wird jedes Mal überprüft, wenn die ORM eine UPDATE oder DELETE für die Zeile ausgibt, um sicherzustellen, dass der im Speicher gehaltene Wert mit dem Datenbankwert übereinstimmt.

Warnung

Da die Versionierungsfunktion auf dem Vergleich des **im Speicher** befindlichen Datensatzes eines Objekts beruht, gilt die Funktion nur für den Session.flush() Prozess, bei dem die ORM einzelne Zeilen im Speicher in die Datenbank ausgibt. Sie wird **nicht** wirksam, wenn ein Mehrzeilen-UPDATE oder -DELETE mit den Methoden Query.update() oder Query.delete() durchgeführt wird, da diese Methoden nur eine UPDATE- oder DELETE-Anweisung ausgeben, aber ansonsten keinen direkten Zugriff auf den Inhalt der betroffenen Zeilen haben.

Der Zweck dieser Funktion besteht darin, zu erkennen, wann zwei gleichzeitige Transaktionen dieselbe Zeile ungefähr zur gleichen Zeit ändern, oder alternativ als Schutz gegen die Verwendung einer "veralteten" Zeile in einem System, das Daten aus einer früheren Transaktion wiederverwenden könnte, ohne sie zu aktualisieren (z.B. wenn man expire_on_commit=False mit einer Session setzt, ist es möglich, Daten aus einer früheren Transaktion wiederzuverwenden).

Einfache Versionszählung

Der einfachste Weg, Versionen zu verfolgen, ist das Hinzufügen einer Ganzzahlspalte zur zugeordneten Tabelle und deren Einrichtung als version_id_col in den Mapper-Optionen.

class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    version_id = mapped_column(Integer, nullable=False)
    name = mapped_column(String(50), nullable=False)

    __mapper_args__ = {"version_id_col": version_id}

Hinweis

Es wird **dringend empfohlen**, die Spalte version_id als NICHT NULL festzulegen. Die Versionsfunktion **unterstützt nicht** einen NULL-Wert in der Versionsspalte.

Oben verfolgt die User-Zuordnung Ganzzahlversionen anhand der Spalte version_id. Wenn ein Objekt vom Typ User zum ersten Mal ausgegeben wird, erhält die Spalte version_id den Wert "1". Anschließend wird ein UPDATE der Tabelle später immer in einer Weise ausgegeben, die der folgenden ähnelt:

UPDATE user SET version_id=:version_id, name=:name
WHERE user.id = :user_id AND user.version_id = :user_version_id
-- {"name": "new name", "version_id": 2, "user_id": 1, "user_version_id": 1}

Die obige UPDATE-Anweisung aktualisiert die Zeile, die nicht nur mit user.id = 1 übereinstimmt, sondern auch verlangt, dass user.version_id = 1 ist, wobei "1" die letzte Versionskennung ist, die wir für dieses Objekt verwendet haben. Wenn eine andere Transaktion die Zeile unabhängig geändert hat, stimmt diese Versions-ID nicht mehr überein, und die UPDATE-Anweisung meldet, dass keine Zeilen übereinstimmten; dies ist die Bedingung, die SQLAlchemy testet, nämlich dass genau eine Zeile mit unserer UPDATE- (oder DELETE-)Anweisung übereinstimmte. Wenn Null Zeilen übereinstimmen, bedeutet dies, dass unsere Daten veraltet sind, und ein StaleDataError wird ausgelöst.

Benutzerdefinierte Versionszähler / Typen

Andere Arten von Werten oder Zählern können zur Versionierung verwendet werden. Gängige Typen sind Datumsangaben und GUIDs. Bei Verwendung eines alternativen Typs oder Zählerschemas bietet SQLAlchemy einen Hook für dieses Schema über das Argument version_id_generator, das einen aufrufbaren Versionsgenerierer akzeptiert. Dieser Aufrufbare erhält den Wert der aktuellen bekannten Version und soll die nachfolgende Version zurückgeben.

Wenn wir beispielsweise die Versionierung unserer User-Klasse mithilfe einer zufällig generierten GUID verfolgen wollten, könnten wir dies tun (beachten Sie, dass einige Backends einen nativen GUID-Typ unterstützen, wir hier jedoch eine einfache Zeichenfolge verwenden):

import uuid


class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    version_uuid = mapped_column(String(32), nullable=False)
    name = mapped_column(String(50), nullable=False)

    __mapper_args__ = {
        "version_id_col": version_uuid,
        "version_id_generator": lambda version: uuid.uuid4().hex,
    }

Die Persistenz-Engine wird uuid.uuid4() jedes Mal aufrufen, wenn ein User-Objekt einer INSERT- oder UPDATE-Operation unterzogen wird. In diesem Fall kann unsere Versionsgenerierungsfunktion den eingehenden Wert von version ignorieren, da die Funktion uuid4() Bezeichner ohne einen vorgeschriebenen Wert generiert. Wenn wir ein sequenzielles Versionierungsschema wie numerisch oder ein Sonderzeichensystem verwenden würden, könnten wir den angegebenen version nutzen, um den nachfolgenden Wert zu bestimmen.

Serverseitige Versionszähler

Der version_id_generator kann auch so konfiguriert werden, dass er auf einem von der Datenbank generierten Wert beruht. In diesem Fall müsste die Datenbank über ein Mittel zur Generierung neuer Bezeichner verfügen, wenn eine Zeile einer INSERT-Operation unterzogen wird, sowie bei einem UPDATE. Für den UPDATE-Fall wird typischerweise ein Update-Trigger benötigt, es sei denn, die betreffende Datenbank unterstützt eine andere native Versionskennung. Die PostgreSQL-Datenbank unterstützt insbesondere eine Systemspalte namens xmin, die UPDATE-Versionierung bereitstellt. Wir können die PostgreSQL-Spalte xmin verwenden, um unsere User-Klasse wie folgt zu versionieren:

from sqlalchemy import FetchedValue


class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50), nullable=False)
    xmin = mapped_column("xmin", String, system=True, server_default=FetchedValue())

    __mapper_args__ = {"version_id_col": xmin, "version_id_generator": False}

Mit der obigen Zuordnung verlässt sich die ORM auf die Spalte xmin, um automatisch den neuen Wert des Versions-ID-Zählers bereitzustellen.

Die ORM ruft typischerweise nicht aktiv die Werte von datenbankgenerierten Werten ab, wenn sie eine INSERT- oder UPDATE-Anweisung ausgibt, sondern lässt diese Spalten als "abgelaufen" und holt sie ab, wenn sie das nächste Mal darauf zugegriffen wird, es sei denn, die Option eager_defaults des Mapper-Flags ist gesetzt. Wenn jedoch eine serverseitige Versionsspalte verwendet wird, muss die ORM den neu generierten Wert aktiv abrufen. Dies geschieht, damit der Versionszähler eingerichtet wird, bevor eine gleichzeitige Transaktion ihn erneut ändern kann. Dieses Abrufen geschieht am besten gleichzeitig innerhalb der INSERT- oder UPDATE-Anweisung unter Verwendung von RETURNING. Andernfalls, wenn anschließend eine SELECT-Anweisung ausgegeben wird, besteht immer noch eine potenzielle Race Condition, bei der sich der Versionszähler ändern kann, bevor er abgerufen werden kann.

Wenn die Ziel-Datenbank RETURNING unterstützt, sieht eine INSERT-Anweisung für unsere User-Klasse wie folgt aus:

INSERT INTO "user" (name) VALUES (%(name)s) RETURNING "user".id, "user".xmin
-- {'name': 'ed'}

Wo oben die ORM alle neu generierten Primärschlüsselwerte zusammen mit serverseitig generierten Versionsbezeichnern in einer einzigen Anweisung abrufen kann. Wenn das Backend RETURNING nicht unterstützt, muss für **jeden** INSERT und UPDATE eine zusätzliche SELECT-Anweisung ausgegeben werden, was wesentlich ineffizienter ist und auch die Möglichkeit von verpassten Versionszählern birgt.

INSERT INTO "user" (name) VALUES (%(name)s)
-- {'name': 'ed'}

SELECT "user".version_id AS user_version_id FROM "user" where
"user".id = :param_1
-- {"param_1": 1}

Es wird **dringend empfohlen**, serverseitige Versionszähler nur dann zu verwenden, wenn es absolut notwendig ist und nur auf Backends, die RETURNING unterstützen, derzeit PostgreSQL, Oracle Database, MariaDB 10.5, SQLite 3.35 und SQL Server.

Programmatische oder bedingte Versionszähler

Wenn version_id_generator auf False gesetzt ist, können wir auch programmatisch (und bedingt) die Versionskennung auf unserem Objekt setzen, auf dieselbe Weise, wie wir ein beliebiges anderes zugeordnetes Attribut zuweisen. Zum Beispiel, wenn wir unser UUID-Beispiel verwenden, aber version_id_generator auf False setzen, können wir die Versionskennung nach Wahl festlegen:

import uuid


class User(Base):
    __tablename__ = "user"

    id = mapped_column(Integer, primary_key=True)
    version_uuid = mapped_column(String(32), nullable=False)
    name = mapped_column(String(50), nullable=False)

    __mapper_args__ = {"version_id_col": version_uuid, "version_id_generator": False}


u1 = User(name="u1", version_uuid=uuid.uuid4())

session.add(u1)

session.commit()

u1.name = "u2"
u1.version_uuid = uuid.uuid4()

session.commit()

Wir können unser User-Objekt auch aktualisieren, ohne den Versionszähler zu inkrementieren. Der Wert des Zählers bleibt unverändert, und die UPDATE-Anweisung prüft weiterhin gegen den vorherigen Wert. Dies kann für Schemata nützlich sein, bei denen nur bestimmte Arten von UPDATEs anfällig für Nebenläufigkeitsprobleme sind.

# will leave version_uuid unchanged
u1.name = "u3"
session.commit()