Partager via


Opérations

Les transactions vous permettent de regrouper plusieurs instructions SQL en une seule unité de travail validée dans la base de données en tant qu’unité atomique. Si une instruction dans la transaction échoue, les modifications apportées par les instructions précédentes peuvent être annulées. L’état initial de la base de données lors du démarrage de la transaction est conservé. L’utilisation d’une transaction peut également améliorer les performances sur SQLite lors de nombreuses modifications apportées à la base de données à la fois.

Concurrence

Dans SQLite, une seule transaction est autorisée à avoir des modifications en attente dans la base de données à la fois. En raison de cela, les appels à BeginTransaction et les méthodes Execute sur SqliteCommand peuvent expirer si une autre transaction prend trop de temps.

Pour plus d’informations sur le verrouillage, les nouvelles tentatives et les délais d’expiration, consultez les erreurs de base de données.

Niveaux d’isolation

Les transactions sont sérialisables par défaut dans SQLite. Ce niveau d’isolation garantit que toutes les modifications apportées au sein d’une transaction sont complètement isolées. Les autres instructions exécutées en dehors de la transaction ne sont pas affectées par les modifications de la transaction.

SQLite prend également en charge la lecture non validée lors de l’utilisation d’un cache partagé. Ce niveau permet les lectures erronées, les lectures non reproductibles et les fantômes :

  • Une lecture erronée se produit lorsque les modifications en attente dans une transaction sont retournées par une requête en dehors de la transaction, mais que les modifications apportées à la transaction sont annulées. Les résultats contiennent des données qui n’ont jamais été réellement validées dans la base de données.

  • Une lecture non répécable se produit lorsqu’une transaction interroge la même ligne deux fois, mais les résultats sont différents, car il a été modifié entre les deux requêtes par une autre transaction.

  • Les fantômes sont des lignes qui sont modifiées ou ajoutées pour satisfaire à la clause WHERE d’une requête pendant une transaction. Si elle est autorisée, la même requête peut retourner différentes lignes lorsqu’elle est exécutée deux fois dans la même transaction.

Microsoft.Data.Sqlite traite IsolationLevel passé à BeginTransaction comme un niveau minimal. Le niveau d’isolement réel sera promu au niveau de lecture non validée ou au niveau de sérialisation.

Le code suivant simule une lecture sale. Notez que la chaîne de connexion doit inclure 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();
}

Transactions différées

À compter de Microsoft.Data.Sqlite version 5.0, les transactions peuvent être différées. Cela reporte la création de la transaction réelle dans la base de données jusqu’à ce que la première commande soit exécutée. Elle entraîne également la mise à niveau progressive de la transaction de lecture vers une transaction d’écriture en fonction des besoins de ses commandes. Cela peut être utile pour activer l’accès simultané à la base de données pendant la transaction.

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

Avertissement

Les commandes à l’intérieur d’une transaction différée peuvent échouer s’ils provoquent la mise à niveau de la transaction de lecture vers une transaction d’écriture pendant que la base de données est verrouillée. Lorsque cela se produit, l’application doit réessayer toute la transaction.

Points de sauvegarde

La version 6.0 de Microsoft.Data.Sqlite prend en charge les points d’enregistrement. Les points de sauvegarde peuvent être utilisés pour créer des transactions imbriquées. Les points d’enregistrement peuvent être restaurer sans affecter d’autres parties de la transaction, et même si un point d’enregistrement est confirmé (publié), ses modifications peuvent être annulées ultérieurement dans le cadre de sa transaction parent.

Le code suivant illustre l’utilisation du patron de verrouillage optimiste hors ligne pour détecter les mises à jour simultanées et résoudre les conflits au sein d’un point de sauvegarde dans le cadre d’une transaction plus étendue.

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