交易

交易可讓您將多個 SQL 陳述式分組成單一工作單位,以一個不可部分完成的單位認可至資料庫。 如果交易中有任何陳述式失敗,就可以復原先前陳述式所做的變更。 系統會保留啟動交易時的資料庫初始狀態。 使用交易也可以改善一次對資料庫進行多項變更時的 SQLite 效能。

並行

在 SQLite 中,一次只能有一個交易在資料庫中有暫止的變更。 因此,如果另一筆交易花費太長的時間完成,則在 SqliteCommand 上呼叫 BeginTransactionExecute 方法可能會逾時。

如需鎖定、重試和逾時的詳細資訊,請參閱資料庫錯誤

隔離等級

在 SQLite 中,交易預設為可序列化。 此隔離等級保證交易內所做的任何變更都會完全隔離。 在交易外部執行的其他陳述式不會受到交易變更的影響。

SQLite 也支援使用共用快取時未認可的讀取。 此等級允許中途讀取、不可重複讀取和虛設項目:

  • 當一個交易中暫止的變更是由交易外部的查詢所傳回,但交易中的變更會復原時,就會發生「中途讀取」。 結果會包含從未實際認可至資料庫的資料。

  • 當交易查詢相同的資料列兩次,但由於另一個交易在兩次查詢之間變更了資料列而產生不同的結果時,就會發生「不可重複讀取」

  • 「虛設項目」是為了符合交易期間查詢的 where 子句所變更或新增的資料列。 如果允許,相同查詢可以在相同交易中執行兩次時傳回不同的資料列。

Microsoft.Data.Sqlite 會將傳遞至 BeginTransaction 的 IsolationLevel 視為最低等級。 實際的隔離等級會升階至未認可或可序列化的讀取。

下列程式碼會模擬中途讀取。 請注意,連接字串必須包含 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();
}

延遲交易

從 Microsoft.Data.Sqlite 5.0 版開始,交易可以延遲。 這會將資料庫中的實際交易建立,延遲到執行第一個命令之後。 它也會讓交易視其命令需要,從讀取交易逐漸升級至寫入交易。 這有助於在交易期間並行存取資料庫。

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

警告

如果延遲交易內的命令導致交易在資料庫鎖定時從讀取交易升級至寫入交易,則命令可能會失敗。 發生這種情況時,應用程式必須重試整個交易。

儲存點

Microsoft.Data.Sqlite 6.0 版支援儲存點。 儲存點可用來建立巢狀交易。 儲存點可以在不影響交易其他部分的情況下復原,即使儲存點可能被認可 (釋放),其變更稍後仍可作為其父交易的一部分來復原。

下列程式碼說明如何使用開放式離線鎖定模式來偵測並行更新,並解決儲存點內的衝突,作為較大交易的一部分。

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