286 lines
12 KiB
Markdown
286 lines
12 KiB
Markdown
# ``GRDB/TransactionObserver``
|
|
|
|
A type that tracks database changes and transactions performed in a database.
|
|
|
|
## Overview
|
|
|
|
`TransactionObserver` is the low-level protocol that supports all <doc:DatabaseObservation> 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 ``DatabaseWriter/add(transactionObserver:extent:)`` `DatabaseWriter` method, or the ``Database/add(transactionObserver:extent:)`` `Database` method:
|
|
|
|
```swift
|
|
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 <doc:TransactionObserver#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](https://www.sqlite.org/foreignkeys.html#fk_actions), and [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
|
|
|
|
Transaction completions are notified to the ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and ``databaseDidRollback(_:)`` callbacks.
|
|
|
|
> Important: Some changes and transactions are not automatically notified. See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes> below.
|
|
|
|
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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
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](https://www.sqlite.org/lang_savepoint.html) 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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
do {
|
|
try dbQueue.inTransaction { db in
|
|
...
|
|
return .commit // 1. willCommit (throws), 2. didRollback
|
|
}
|
|
} catch {
|
|
// 3. The error thrown by the transaction observer.
|
|
}
|
|
```
|
|
|
|
- Note: All callbacks are called in the writer dispatch queue, and serialized with all database updates.
|
|
|
|
- Note: The `databaseDidChange` and `databaseWillCommit` callbacks must not access the observed writer database connection in any way. This limitation does not apply to `databaseDidCommit` and `databaseDidRollback` which can use their database argument.
|
|
|
|
## 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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 its `databaseDidCommit` or `databaseDidRollback` 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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
1. Use the GRDB.swift CocoaPod with a custom compilation option, as below.
|
|
|
|
It uses the system SQLite, which is compiled with `SQLITE_ENABLE_PREUPDATE_HOOK` support, but only on iOS 11.0+ (we don't know the minimum version of macOS, tvOS, watchOS):
|
|
|
|
```ruby
|
|
pod 'GRDB.swift'
|
|
platform :ios, '11.0' # or above
|
|
|
|
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 in `GCC_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 this `GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1` option.
|
|
|
|
2. Use a [custom SQLite build](http://github.com/groue/GRDB.swift/blob/master/Documentation/CustomSQLiteBuilds.md) 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`](https://www.sqlite.org/withoutrowid.html) tables.
|
|
- The deletion of duplicate rows triggered by [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) 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 ``Database/notifyChanges(in:)`` `Database` method. The ``databaseDidChange()-7olv7`` callback will be called accordingly. For example:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
try dbQueue.write { db in
|
|
// Notify all observers of the sqlite_master table
|
|
try db.notifyChanges(in: Table("sqlite_master"))
|
|
}
|
|
```
|
|
|
|
## Topics
|
|
|
|
### Filtering Database Changes
|
|
|
|
- ``observes(eventsOfKind:)``
|
|
- ``DatabaseEventKind``
|
|
|
|
### Handling Database Changes
|
|
|
|
- ``databaseDidChange()-7olv7``
|
|
- ``databaseDidChange(with:)``
|
|
- ``stopObservingDatabaseChangesUntilNextTransaction()``
|
|
- ``DatabaseEvent``
|
|
|
|
### Handling Transactions
|
|
|
|
- ``databaseWillCommit()-7mksu``
|
|
- ``databaseDidCommit(_:)``
|
|
- ``databaseDidRollback(_:)``
|