Transactions

As transações permitem agrupar várias instruções SQL em uma única unidade de trabalho confirmada no banco de dados como uma unidade atômica. Se alguma instrução na transação falhar, as alterações feitas pelas instruções anteriores poderão ser revertidas. O estado inicial do banco de dados quando a transação foi iniciada é preservado. O uso de uma transação também pode melhorar o desempenho no SQLite ao fazer várias alterações no banco de dados de uma só vez.

Simultaneidade

No SQLite, é permitido que apenas uma transação tenha alterações pendentes no banco de dados por vez. Por isso, chamadas para os métodos BeginTransaction e Execute em SqliteCommand podem atingir tempo limite se outra transação demorar muito para ser concluída.

Para obter mais informações sobre bloqueio, novas tentativas e tempos limite, consulte Erros de banco de dados.

Níveis de isolamento

As transações são serializáveis por padrão no SQLite. Esse nível de isolamento garante que todas as alterações feitas em uma transação sejam completamente isoladas. Outras instruções executadas fora da transação não são afetadas pelas alterações da transação.

O SQLite também dá suporte à leitura não confirmada ao usar um cache compartilhado. Este nível permite leituras sujas, leituras não repetíveis e fantasmas:

  • Uma leitura suja ocorre quando as alterações pendentes em uma transação são retornadas por uma consulta fora da transação, mas as alterações na transação são revertidas. Os resultados contêm dados que nunca foram realmente confirmados no banco de dados.

  • Uma leitura não repetível ocorre quando uma transação consulta a mesma linha duas vezes, mas os resultados são diferentes porque ela foi alterada por outra transação entre as duas consultas.

  • Fantasmas são linhas que são alteradas ou adicionadas para atender à cláusula where de uma consulta durante uma transação. Se permitido, a mesma consulta poderá retornar linhas diferentes quando executada duas vezes na mesma transação.

O Microsoft.Data.Sqlite trata o IsolationLevel passado para BeginTransaction como um nível mínimo. O nível de isolamento real será promovido a leitura não confirmada ou serializável.

O código a seguir simula uma leitura suja. Observe que a cadeia de conexão deve incluir 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();
}

Transações adiadas

A partir do Microsoft.Data.Sqlite versão 5.0, as transações podem ser adiadas. Isso adia a criação da transação real no banco de dados até que o primeiro comando seja executado. Também faz com que a transação atualize gradualmente de uma transação de leitura para uma transação de gravação conforme necessário por seus comandos. Isso pode ser útil para habilitar o acesso simultâneo ao banco de dados durante a transação.

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

Aviso

Os comandos dentro de uma transação adiada podem falhar se eles fizerem com que a transação seja atualizada de uma transação de leitura para uma transação de gravação enquanto o banco de dados está bloqueado. Quando isso acontecer, o aplicativo precisará repetir toda a transação.

Pontos de salvamento

A versão 6.0 do Microsoft.Data.Sqlite dá suporte a pontos de salvamento. Os pontos de salvamento podem ser usados para criar transações aninhadas. Os pontos de salvamento podem ser revertidos sem afetar outras partes da transação e, embora um ponto de salvamento possa ser confirmado (liberado), suas alterações poderão ser revertidas posteriormente como parte de sua transação pai.

O código a seguir ilustra o uso do padrão de bloqueio offline otimista para detectar atualizações simultâneas e resolver conflitos em um ponto de salvamento como parte de uma transação maior.

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