Transaktionen
Mit Transaktionen können Sie mehrere SQL-Anweisungen in einer einzelnen Arbeitseinheit gruppieren, die als unteilbare Einheit an die Datenbank übergeben 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 vor Beginn der Transaktion wird beibehalten. Bei der gleichzeitigen Ausführung von zahlreichen Änderungen können Sie durch den Einsatz einer Transaktion auch die Leistung von SQLite verbessern.
Parallelität
In SQLite darf jeweils nur eine Transaktion über ausstehende Änderungen in der Datenbank verfügen. Daher kann bei einem Aufruf der Methoden BeginTransaction und Execute
in der SqliteCommand-Klasse ein Timeout auftreten, wenn die Ausführung einer anderen Transaktion zu lange dauert.
Weitere Informationen zu Sperren, erneuten Versuchen und Timeouts finden Sie unter Datenbankfehler.
Isolationsgrade
Standardmäßig sind Transaktionen in SQLite serialisierbar. Mit dieser Isolationsstufe wird sichergestellt, dass alle innerhalb 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. In dieser Stufe sind „Dirty Reads“, nicht wiederholbare Lesevorgänge und Phantomlesevorgänge zulässig:
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, für die kein echter Commit in der Datenbank ausgeführt wurde.
Bei einem nicht wiederholbaren Lesevorgang wird eine Zeile zweimal von einer Transaktion abgefragt. Die Ergebnisse unterscheiden sich jedoch, da die Zeile zwischen den beiden Abfragen von einer anderen Transaktion geändert wurde.
Bei einem Phantomlesevorgang sind Zeilen vorhanden, die zur Erfüllung der WHERE-Klausel einer Abfrage während einer Transaktion geändert oder hinzugefügt wurden. Ist ein solcher Vorgang zulässig, kann eine Abfrage unterschiedliche Zeilen zurückgeben, wenn sie zweimal in derselben Transaktion ausgeführt wird.
Microsoft.Data.Sqlite behandelt den für „IsolationLevel“ an BeginTransaction übergegebenen Wert als Mindeststufe. 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 Cache=Shared
enthalten muss.
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 der Version 5.0 von Microsoft.Data.Sqlite können Transaktionen verzögert werden. Dadurch wird die Erstellung der tatsächlichen Transaktion in der Datenbank so lange verzögert, bis der erste Befehl ausgeführt wird. Außerdem wird für die Transaktion dadurch allmählich ein Upgrade von einer Lesetransaktion zu einer Schreibtransaktion durchgeführt, wie es von den jeweiligen Befehlen erfordert wird. Dies kann hilfreich sein, wenn während der Transaktion gleichzeitiger Zugriff auf die Datenbank ermöglicht werden soll.
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
Bei Befehlen innerhalb einer verzögerten Transaktion können Fehler auftreten, wenn sie dazu führen, dass für die Transaktion ein Upgrade von einer Lesetransaktion zu einer Schreibtransaktion durchgeführt wird, noch während die Datenbank gesperrt ist. Wenn dies geschieht, muss die Anwendung die gesamte Transaktion neu ausführen.
Sicherungspunkte
Version 6.0 von Microsoft.Data.Sqlite unterstützt Sicherungspunkte. Mithilfe von Sicherungspunkten können geschachtelte Transaktionen erstellt werden. Für Sicherungspunkte können ohne Auswirkungen auf andere Teile der Transaktion Rollbacks ausgeführt werden und obwohl ein Sicherungspunkt möglicherweise committet (freigegeben) wird, können für die zugehörigen Änderungen später als Teil der übergeordneten Transaktion Rollbacks ausgeführt werden.
Der folgende Code veranschaulicht die Verwendung des optimistischen Offlinesperrmusters, um gleichzeitige Updates zu erkennen und Konflikte innerhalb eines Sicherungspunkts im Rahmen einer größeren Transaktion zu beheben.
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();
}