Learn how to use transactions and savepoints for atomic database operations
Transactions ensure that a group of database operations either all succeed together or all fail together, maintaining database consistency and integrity.
The inTransaction closure returns a TransactionCompletion:
enum TransactionCompletion { case commit case rollback}
Commit
Rollback
Error Rollback
try dbQueue.inTransaction { db in try Player(name: "Arthur", score: 100).insert(db) // All changes are saved return .commit}
try dbQueue.inTransaction { db in try Player(name: "Arthur", score: 100).insert(db) // Changes are discarded return .rollback}
do { try dbQueue.inTransaction { db in try Player(name: "Arthur", score: 100).insert(db) throw CustomError() // Transaction automatically rolls back }} catch { print("Transaction rolled back due to error")}
try dbQueue.write { db in try Player(name: "Arthur", score: 100).insert(db) try db.inSavepoint { try Player(name: "Barbara", score: 1000).insert(db) try Player(name: "Charlie", score: 500).insert(db) return .commit } // Arthur is inserted, Barbara and Charlie are inserted}
try dbQueue.write { db in try Player(name: "Arthur", score: 100).insert(db) try db.inSavepoint { try Player(name: "Barbara", score: 1000).insert(db) try Player(name: "Charlie", score: 500).insert(db) return .rollback // Rollback this savepoint only } // Arthur is inserted, Barbara and Charlie are NOT inserted}
try dbQueue.write { db in try Player(name: "Arthur", score: 100).insert(db) try db.inSavepoint { try Player(name: "Barbara", score: 1000).insert(db) try db.inSavepoint { try Player(name: "Charlie", score: 500).insert(db) return .rollback // Rollback only Charlie } return .commit // Commit Barbara } // Arthur and Barbara are inserted, Charlie is NOT inserted}
func insertPlayers(_ players: [Player]) throws { try dbQueue.write { db in for player in players { try player.insert(db) } // All or nothing - if any insert fails, all are rolled back }}
try dbQueue.write { db in var successfulUpdates = 0 for player in players { try db.inSavepoint { do { try player.insert(db) successfulUpdates += 1 return .commit } catch { print("Failed to insert \(player.name)") return .rollback // Skip this player, continue with others } } } print("Inserted \(successfulUpdates) players")}
// ✅ GOOD: Short transactiontry dbQueue.write { db in try player.insert(db)}// ❌ BAD: Long transactiontry dbQueue.write { db in let data = downloadLargeFile() // Don't do slow operations in transactions try processData(data, db: db)}
// ✅ GOOD: Deferred for read-heavytry dbQueue.inTransaction(.deferred) { db in let players = try Player.fetchAll(db) try updateScore(for: players.first!, db: db) return .commit}// ✅ GOOD: Immediate for write-heavytry dbQueue.inTransaction(.immediate) { db in try insertManyPlayers(db: db) return .commit}
// ✅ GOOD: Use savepoints for nestingtry dbQueue.write { db in try player1.insert(db) try db.inSavepoint { try player2.insert(db) return .commit }}// ❌ BAD: Don't nest transactionstry dbQueue.inTransaction { db in try dbQueue.inTransaction { db in // ERROR! return .commit } return .commit}
// Slow: Individual transactions (implicit)for player in players { try dbQueue.write { db in try player.insert(db) }}// Fast: Single transactiontry dbQueue.write { db in for player in players { try player.insert(db) }}
// Process in batcheslet batchSize = 1000for batch in players.chunked(into: batchSize) { try dbQueue.write { db in for player in batch { try player.insert(db) } }}