Udostępnij za pośrednictwem


Transakcji

Transakcje umożliwiają grupowanie wielu instrukcji SQL w jedną jednostkę pracy, która jest zatwierdzana w bazie danych jako jedna jednostka niepodzielna. Jeśli jakiekolwiek polecenie w transakcji zakończy się niepowodzeniem, zmiany wprowadzone przez poprzednie polecenia mogą zostać cofnięte. Początkowy stan bazy danych podczas uruchamiania transakcji jest zachowywany. Użycie transakcji może również poprawić wydajność bazy danych SQLite podczas wprowadzania licznych zmian w bazie danych jednocześnie.

Współbieżność

W programie SQLite tylko jedna transakcja może mieć oczekujące zmiany w bazie danych naraz. W związku z tym, wywołania do BeginTransaction i metody Execute na SqliteCommand mogą przekroczyć limit czasu, jeśli inna transakcja trwa zbyt długo.

Aby uzyskać więcej informacji na temat blokowania, ponawiania prób i przekroczenia limitu czasu, zobacz Błędy bazy danych.

Poziomy izolacji

Transakcje są domyślnie serializowalne w sqlite. Ten poziom izolacji gwarantuje, że wszelkie zmiany wprowadzone w ramach transakcji są całkowicie odizolowane. Inne instrukcje wykonywane poza transakcją nie mają wpływu na zmiany transakcji.

SQLite obsługuje również niezatwierdzony odczyt podczas korzystania z udostępnionej pamięci podręcznej. Ten poziom umożliwia brudne odczyty, niepowtarzalne odczyty i fantomy:

  • Zanieczyszczony odczyt występuje, gdy zmiany oczekujące na jedną transakcję są zwracane przez zapytanie poza transakcją, ale zmiany w transakcji są cofane. Wyniki zawierają dane, które nigdy nie zostały zatwierdzone w bazie danych.

  • Odczyt bez zmian występuje, gdy transakcja wykonuje zapytanie w tym samym wierszu dwa razy, ale wyniki są różne, ponieważ zostały zmienione między dwoma zapytaniami przez inną transakcję.

  • Phantoms to wiersze, które są zmieniane lub dodawane, aby spełniać klauzulę where w zapytaniu w trakcie transakcji. Jeśli jest to możliwe, to samo zapytanie może zwrócić różne wiersze po wykonaniu dwukrotnie w tej samej transakcji.

Microsoft.Data.Sqlite traktuje IsolationLevel przekazany do BeginTransaction jako minimalny poziom. Rzeczywisty poziom izolacji zostanie podwyższony do poziomu "read uncommitted" lub "serializable".

Poniższy kod symuluje brudny odczyt. Należy pamiętać, że parametry połączenia muszą zawierać wartość Cache=Shared.

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

Transakcje odroczone

Począwszy od wersji Microsoft.Data.Sqlite w wersji 5.0, transakcje można odroczyć. Spowoduje to odwrócenie tworzenia rzeczywistej transakcji w bazie danych do momentu wykonania pierwszego polecenia. Powoduje również stopniowe przekształcanie transakcji z transakcji odczytu do transakcji zapisu w miarę zmieniających się potrzeb poleceń. Może to być przydatne w przypadku włączania współbieżnego dostępu do bazy danych podczas transakcji.

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

Ostrzeżenie

Polecenia wewnątrz transakcji odroczonej mogą zakończyć się niepowodzeniem, jeśli spowodują uaktualnienie transakcji z transakcji odczytu do transakcji zapisu, gdy baza danych jest zablokowana. W takim przypadku aplikacja będzie musiała ponowić próbę wykonania całej transakcji.

Punkty zapisywania

Wersja 6.0 biblioteki Microsoft.Data.Sqlite obsługuje punkty zapisywania. Punkty zapisywania mogą służyć do tworzenia zagnieżdżonych transakcji. Punkty zapisywania można wycofać bez wpływu na inne części transakcji, a nawet jeśli punkt zapisywania zostanie zatwierdzony (zwolniony), jego zmiany mogą być później wycofane w ramach transakcji nadrzędnej.

Poniższy kod ilustruje użycie optymistycznego wzorca blokady offline do wykrywania współbieżnych aktualizacji i rozwiązywania konfliktów w punkcie zapisywania w ramach większej transakcji.

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