TransactionObserver
A type that tracks database changes and transactions performed in a database.
protocol TransactionObserver : AnyObject
Browse conforming typesOverview
TransactionObserver
is the low-level protocol that supports all Database Observation features.
A transaction observer is notified of individual changes (inserts, updates and deletes), before they are committed to disk, as well as transaction commits and rollbacks.
Activate a Transaction Observer
An observer starts receiving change notifications after it has been added to a database connection with the add(transactionObserver:extent:)
DatabaseWriter
method, or the add(transactionObserver:extent:)
Database
method:
let observer = MyObserver()
dbQueue.add(transactionObserver: observer)
By default, database holds weak references to its transaction observers: they are not retained, and stop getting notifications after they are deallocated. See Observation Extent for more options.
Database Changes And Transactions
Database changes are notified to the databaseDidChange(with:)
callback. This includes indirect changes triggered by ON DELETE
and ON UPDATE
actions associated to foreign keys, and SQL triggers.
Transaction completions are notified to the databaseWillCommit
, databaseDidCommit(_:)
and databaseDidRollback(_:)
callbacks.
Notified changes are not actually written to disk until the transaction commits, and the databaseDidCommit
callback is called. On the other side, databaseDidRollback
confirms their invalidation:
try dbQueue.write { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "UPDATE ...") // 2. didChange
} // 3. willCommit, 4. didCommit
try dbQueue.inTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "UPDATE ...") // 2. didChange
return .rollback // 3. didRollback
}
try dbQueue.write { db in
try db.execute(sql: "INSERT ...") // 1. didChange
throw SomeError()
} // 2. didRollback
Database statements that are executed outside of any explicit transaction do not drop off the radar:
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit
try db.execute(sql: "UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit
}
Changes that are on hold because of a savepoint are only notified after the savepoint has been released. This makes sure that notified events are only those that have an opportunity to be committed:
try dbQueue.inTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "SAVEPOINT foo")
try db.execute(sql: "UPDATE ...") // delayed
try db.execute(sql: "UPDATE ...") // delayed
try db.execute(sql: "RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange
try db.execute(sql: "SAVEPOINT bar")
try db.execute(sql: "UPDATE ...") // not notified
try db.execute(sql: "ROLLBACK TO SAVEPOINT bar")
try db.execute(sql: "RELEASE SAVEPOINT bar")
return .commit // 4. willCommit, 5. didCommit
}
Eventual errors thrown from databaseWillCommit
are exposed to the application code:
do {
try dbQueue.inTransaction { db in
...
return .commit // 1. willCommit (throws), 2. didRollback
}
} catch {
// 3. The error thrown by the transaction observer.
}
Filtering Database Events
Transaction observers can choose the database changes they are interested in.
The observes(eventsOfKind:)
method filters events that are notified to databaseDidChange(with:)
. It is the most efficient and recommended change filtering technique, because it is only called once before a database query is executed, and can completely disable change tracking:
// Calls `observes(eventsOfKind:)` once.
// Calls `databaseDidChange(with:)` for every updated row, or not at all.
try db.execute(sql: "UPDATE player SET score = score + 1")
The DatabaseEventKind
argument of observes(eventsOfKind:)
can distinguish insertions from deletions and updates, and is also able to tell the columns that are about to be changed.
For example, an observer can focus on the changes that happen on the “player” database table only:
class PlayerObserver: TransactionObserver {
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
// Only observe changes to the "player" table.
eventKind.tableName == "player"
}
func databaseDidChange(with event: DatabaseEvent) {
// This method is only called for changes that happen to
// the "player" table.
}
}
When the observes(eventsOfKind:)
method returns false for all event kinds, the observer is still notified of transactions.
Observation Extent
You can specify how long an observer is notified of database changes and transactions.
The remove(transactionObserver:)
method explicitly stops notifications, at any time:
// From a database queue or pool:
dbQueue.remove(transactionObserver: observer)
// From a database connection:
dbQueue.inDatabase { db in
db.remove(transactionObserver: observer)
}
Alternatively, use the extent
parameter of the add(transactionObserver:extent:)
method:
let observer = MyObserver()
// On a database queue or pool:
dbQueue.add(transactionObserver: observer) // default extent
dbQueue.add(transactionObserver: observer, extent: .observerLifetime)
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
// On a database connection:
dbQueue.inDatabase { db in
db.add(transactionObserver: ...)
}
The default extent is
.observerLifetime
: the database holds a weak reference to the observer, and the observation automatically ends when the observer is deallocated. Meanwhile, the observer is notified of all changes and transactions..nextTransaction
activates the observer until the current or next transaction completes. The database keeps a strong reference to the observer until itsdatabaseDidCommit
ordatabaseDidRollback
callback is called. Hereafter the observer won’t get any further notification..databaseLifetime
has the database retain and notify the observer until the database connection is closed.
Finally, an observer can avoid processing database changes until the end of the current transaction. After stopObservingDatabaseChangesUntilNextTransaction
, the databaseDidChange
callback will not be called until the current transaction completes:
class PlayerObserver: TransactionObserver {
var playerTableWasModified = false
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
eventKind.tableName == "player"
}
func databaseDidChange(with event: DatabaseEvent) {
playerTableWasModified = true
// It is pointless to keep on tracking further changes:
stopObservingDatabaseChangesUntilNextTransaction()
}
}
Support for SQLite Pre-Update Hooks
When SQLite is built with the SQLITE_ENABLE_PREUPDATE_HOOK
option, TransactionObserver
gets an extra callback which lets you observe individual column values in the rows modified by a transaction:
protocol TransactionObserver: AnyObject {
#if SQLITE_ENABLE_PREUPDATE_HOOK
/// Notifies before a database change (insert, update, or delete)
/// with change information (initial / final values for the row's
/// columns).
///
/// The event is only valid for the duration of this method call. If you
/// need to keep it longer, store a copy: event.copy().
func databaseWillChange(with event: DatabasePreUpdateEvent)
#endif
}
This extra API can be activated in two ways:
Use the GRDB.swift CocoaPod with a custom compilation option, as below.
pod 'GRDB.swift' post_install do |installer| installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target| target.build_configurations.each do |config| # Enable extra GRDB APIs config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK" # Enable extra SQLite APIs config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1" end end end
Warning: make sure you use the right platform version! You will get runtime errors on devices with a lower version.
Note: the
GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1
option inGCC_PREPROCESSOR_DEFINITIONS
defines some C function prototypes that are lacking from the system<sqlite3.h>
header. When Xcode eventually ships with an SDK that includes a complete header, you may get a compiler error about duplicate function definitions. When this happens, just remove thisGRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1
option.Use a custom SQLite build and activate the
SQLITE_ENABLE_PREUPDATE_HOOK
compilation option.
Dealing with Undetected Changes
The changes and transactions that are not automatically notified to transaction observers are:
Read-only transactions.
Changes and transactions performed by external database connections.
Changes performed by SQLite statements that are not both compiled and executed through GRDB APIs.
Changes to the database schema, changes to internal system tables such as
sqlite_master
.Changes to
WITHOUT ROWID
tables.The deletion of duplicate rows triggered by
ON CONFLICT REPLACE
clauses (this last exception might change in a future release of SQLite).
To notify undetected changes to transaction observers, perform an explicit call to the notifyChanges(in:)
Database
method. The databaseDidChange
callback will be called accordingly. For example:
try dbQueue.write { db in
// Notify observers that some changes were performed in the database
try db.notifyChanges(in: .fullDatabase)
// Notify observers that some changes were performed in the player table
try db.notifyChanges(in: Player.all())
// Equivalent alternative
try db.notifyChanges(in: Table("player"))
}
To notify a change in the database schema, notify a change to the sqlite_master
table:
try dbQueue.write { db in
// Notify all observers of the sqlite_master table
try db.notifyChanges(in: Table("sqlite_master"))
}
Filtering Database Changes
func observes(eventsOfKind: DatabaseEventKind
) -> Bool Returns whether specific kinds of database changes should be notified to the observer.
enum DatabaseEventKind
A kind of database event.
Handling Database Changes
func databaseDidChange(
) Called when the database was modified in some unspecified way.
func databaseDidChange(with: DatabaseEvent
) Called when the database is changed by an insert, update, or delete event.
func stopObservingDatabaseChangesUntilNextTransaction(
) Prevents the observer from receiving further change notifications until the next transaction.
struct DatabaseEvent
A database event.
Handling Transactions
func databaseWillCommit(
) throws Called when a transaction is about to be committed.
func databaseDidCommit(Database
) Called when a transaction has been committed on disk.
func databaseDidRollback(Database
) Called when a transaction has been rollbacked.