Freigeben über


Transaktionen

Transaktionen ermöglichen es Ihnen, mehrere SQL-Anweisungen in eine einzelne Arbeitseinheit zu gruppieren, die der Datenbank als eine atomige Einheit zugesichert wird. Schlägt eine Anweisung innerhalb der Transaktion fehl, können die durch die vorherigen Anweisungen vorgenommenen Änderungen durch ein Rollback rückgängig gemacht werden. Der Anfangszustand der Datenbank, als die Transaktion gestartet wurde, wird beibehalten. Die Verwendung einer Transaktion kann auch die Leistung von SQLite verbessern, wenn zahlreiche Änderungen an der Datenbank gleichzeitig vorgenommen werden.

Konkurrenz

In SQLite darf gleichzeitig nur eine Transaktion Änderungen in der Datenbank anhängig haben. Aus diesem Grund kann ein Timeout für Aufrufe von BeginTransaction und den Execute-Methoden bei SqliteCommand auftreten, wenn eine andere Transaktion zu lange dauert.

Weitere Informationen zu Sperrmechanismen, Wiederholungen und Timeouts finden Sie unter Datenbankfehler.

Isolationsebenen

Transaktionen können standardmäßig in SQLite serialisiert werden. Diese Isolationsstufe garantiert, dass alle in einer Transaktion vorgenommenen Änderungen vollständig isoliert sind. Außerhalb der Transaktion ausgeführte Anweisungen sind von den Änderungen der Transaktion nicht betroffen.

Bei Verwendung eines freigegebenen Caches unterstützt SQLite auch die Isolationsstufe Lesen ohne Commit. Diese Stufe ermöglicht schmutzige Lesevorgänge, nicht wiederholbare Lesevorgänge und Phantome:

  • Bei einem Dirty Read genannten Lesevorgang werden in einer Transaktion ausstehende Änderungen von einer Abfrage außerhalb der Transaktion zurückgegeben. Für die Änderungen innerhalb der Transaktion wird jedoch ein Rollback ausgeführt. Die Ergebnisse enthalten Daten, die nie tatsächlich an die Datenbank gebunden wurden.

  • Ein nicht wiederholbarer Lesevorgang tritt auf, wenn eine Transaktion die gleiche Zeile zweimal abfragt, aber die Ergebnisse unterscheiden sich, da sie zwischen den beiden Abfragen durch eine andere Transaktion geändert wurde.

  • Phantome sind Zeilen, die geändert oder hinzugefügt werden, um die Where-Klausel einer Abfrage während einer Transaktion zu erfüllen. Wenn dies zulässig ist, kann dieselbe Abfrage unterschiedliche Zeilen zurückgeben, wenn sie zweimal in derselben Transaktion ausgeführt werden.

Microsoft.Data.Sqlite behandelt den übergebenen IsolationLevel als Mindeststufe für BeginTransaction. Die tatsächliche Isolationsstufe wird höhergestuft auf „Lesen ohne Commit“ oder „serialisierbar“.

Mit dem folgenden Code wird ein „Dirty Read“ simuliert. Beachten Sie, dass die Verbindungszeichenfolge enthalten Cache=Sharedmuss.

using (var firstTransaction = firstConnection.BeginTransaction())
{
    var updateCommand = firstConnection.CreateCommand();
    updateCommand.CommandText =
    @"
        UPDATE data
        SET value = 'dirty'
    ";
    updateCommand.ExecuteNonQuery();

    // Without ReadUncommitted, the command will time out since the table is locked
    // while the transaction on the first connection is active
    using (secondConnection.BeginTransaction(IsolationLevel.ReadUncommitted))
    {
        var queryCommand = secondConnection.CreateCommand();
        queryCommand.CommandText =
        @"
            SELECT *
            FROM data
        ";
        var value = (string)queryCommand.ExecuteScalar();
        Console.WriteLine($"Value: {value}");
    }

    firstTransaction.Rollback();
}

Verzögerte Transaktionen

Ab Microsoft.Data.Sqlite, Version 5.0, können Transaktionen zurückgestellt werden. Dadurch wird die Erstellung der tatsächlichen Transaktion in der Datenbank zurückverzögert, bis der erste Befehl ausgeführt wird. Sie bewirkt außerdem, dass die Transaktion nach und nach von einer Lesetransaktion auf eine Schreibtransaktion aktualisiert wird, wenn sie von ihren Befehlen benötigt wird. Dies kann nützlich sein, um gleichzeitigen Zugriff auf die Datenbank während der Transaktion zu ermöglichen.

using (var transaction = connection.BeginTransaction(deferred: true))
{
    // Before the first statement of the transaction is executed, both concurrent
    // reads and writes are allowed

    var readCommand = connection.CreateCommand();
    readCommand.CommandText =
    @"
        SELECT *
        FROM data
    ";
    var value = (long)readCommand.ExecuteScalar();

    // After a the first read statement, concurrent writes are blocked until the
    // transaction completes. Concurrent reads are still allowed

    var writeCommand = connection.CreateCommand();
    writeCommand.CommandText =
    @"
        UPDATE data
        SET value = $newValue
    ";
    writeCommand.Parameters.AddWithValue("$newValue", value + 1L);
    writeCommand.ExecuteNonQuery();

    // After the first write statement, both concurrent reads and writes are blocked
    // until the transaction completes

    transaction.Commit();
}

Warnung

Befehle innerhalb einer verzögerten Transaktion können fehlschlagen, wenn die Transaktion von einer Lesetransaktion auf eine Schreibtransaktion aktualisiert wird, während die Datenbank gesperrt ist. In diesem Fall muss die Anwendung die gesamte Transaktion wiederholen.

Sicherungspunkte

Version 6.0 von Microsoft.Data.Sqlite unterstützt Speicherpunkte. Mithilfe von Sicherungspunkten können geschachtelte Transaktionen erstellt werden. Speicherpunkte können rückgängig gemacht werden, ohne dass sich dies auf andere Teile der Transaktion auswirkt, und obwohl ein Speicherpunkt möglicherweise zugesichert (freigegeben) wird, können die Änderungen später als Teil der übergeordneten Transaktion zurückgesetzt werden.

Der folgende Code veranschaulicht die Verwendung des Musters "Optimistische Offlinesperre" zur Erkennung gleichzeitiger Updates und zur Lösung von Konflikten innerhalb eines Savepoints, der Teil einer größeren Transaktion ist.

using (var transaction = connection.BeginTransaction())
{
    // Transaction may include additional statements before the savepoint

    var updated = false;
    do
    {
        // Begin savepoint
        transaction.Save("optimistic-update");

        var insertCommand = connection.CreateCommand();
        insertCommand.CommandText =
        @"
            INSERT INTO audit
            VALUES (datetime('now'), 'User updates data with id 1')
        ";
        insertCommand.ExecuteScalar();

        var updateCommand = connection.CreateCommand();
        updateCommand.CommandText =
        @"
            UPDATE data
            SET value = 2,
                version = $expectedVersion + 1
            WHERE id = 1
                AND version = $expectedVersion
        ";
        updateCommand.Parameters.AddWithValue("$expectedVersion", expectedVersion);
        var recordsAffected = updateCommand.ExecuteNonQuery();
        if (recordsAffected == 0)
        {
            // Concurrent update detected! Rollback savepoint and retry
            transaction.Rollback("optimistic-update");

            // TODO: Resolve update conflicts
        }
        else
        {
            // Update succeeded. Commit savepoint and continue with the transaction
            transaction.Release("optimistic-update");

            updated = true;
        }
    }
    while (!updated);

    // Additional statements may be included after the savepoint

    transaction.Commit();
}