Transactions & Savepoints
Transactions are a fundamental feature of any relational database, allowing you to group a series of SQL statements into a single atomic operation. This ensures data integrity by guaranteeing that either all statements in the group complete successfully, or none of them do.
Using Transactions
SQLite.swift provides a simple transaction
method on the Connection
object that takes a closure. All statements executed within the closure are part of the transaction.
- If the closure completes without throwing an error, the transaction is committed.
- If the closure throws an error, the transaction is rolled back, and all changes made within it are undone.
let alice = users.filter(id == 1)
let betty = users.filter(id == 2)
let balance = Expression<Double>("balance")
let amount = 100.0
try db.transaction {
// Withdraw from Alice's account
try db.run(alice.update(balance -= amount))
// Deposit into Betty's account
try db.run(betty.update(balance += amount))
}
// SQL Executed:
// BEGIN DEFERRED TRANSACTION
// UPDATE "users" SET "balance" = "balance" - 100.0 WHERE ("id" = 1)
// UPDATE "users" SET "balance" = "balance" + 100.0 WHERE ("id" = 2)
// COMMIT TRANSACTION
If either of the update
statements were to fail (e.g., if a CHECK
constraint was violated), an error would be thrown, and a ROLLBACK TRANSACTION
would be executed automatically.
Transaction Modes
You can specify a TransactionMode
to control the locking behavior of the transaction:
.deferred
(Default): Locks are not acquired until the first read or write operation..immediate
: A reserved lock is acquired on the database immediately..exclusive
: An exclusive lock is acquired on all databases immediately.
try db.transaction(.immediate) {
// ...
}
Using Savepoints for Nested Transactions
SQLite does not support nesting BEGIN...COMMIT
transactions. To achieve a similar effect, you can use savepoints.
Savepoints allow you to roll back to a specific point within a larger transaction without rolling back the entire transaction.
try db.transaction {
// Perform some initial updates
try db.run(users.insert(email <- "betty@icloud.com"))
do {
try db.savepoint {
// This nested block can fail independently
try db.run(users.insert(email <- "cathy@icloud.com")) // valid
try db.run(users.insert(email <- "cathy@icloud.com")) // constraint violation, will throw
}
} catch {
// The savepoint is rolled back, but the outer transaction continues.
print("Inner operation failed, but we can continue.")
}
// The insert of "betty@icloud.com" will still be committed.
}
In this example, Cathy's insert is rolled back, but Betty's insert, which is part of the outer transaction, is still committed.