GRDB 4 comes with new features, but also a few breaking changes. This guide aims at helping you upgrading your applications.
- New requirements
- Raw SQL
- ValueObservation
- Associations
- Good Practices for Designing Record Types
- SQLCipher
- PersistenceError.recordNotFound
GRDB now requires Swift 4.2+ and Xcode 10+. Swift 4.0 and 4.1 are no longer supported. Xcode 9 is no longer supported.
iOS 8 is no longer supported. The minimum iOS target is now iOS 9.
Whenever your application uses raw SQL queries or snippets, you will now always have to use the sql
argument label:
// GRDB 3
try db.execute("INSERT INTO player (name) VALUES (?)", arguments: ["Arthur"])
let playerCount = try Int.fetchOne(db, "SELECT COUNT(*) FROM player")
// GRDB 4
try db.execute(sql: "INSERT INTO player (name) VALUES (?)", arguments: ["Arthur"])
let playerCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM player")
This change was made necessary by the introduction of SQL Interpolation.
ValueObservation has been refreshed in GRDB 4.
To guarantee asynchronous notifications, and never ever block your main thread, use the .async(onQueue:startImmediately:)
scheduling:
// On main queue
var observation = Player.observationForAll()
observation.scheduling = .async(onQueue: .main, startImmediately: true)
let observer = try observation.start(
in: dbQueue,
onError: { error in ... },
onChange: { (players: [Player]) in
// On main queue
print("fresh players: \(players)")s
})
// <- here "fresh players" is not printed yet.
In GRDB 3, this scheduling used to be named .queue(_: startImmediately:)
.
The second breaking change is ValueObservation.extent
, which was removed in GRDB 4. Now all observations last until the observer returned by the start
method is deallocated.
GRDB 4 brought a few new associations features:
-
Indirect associations HasOneThrough and HasManyThrough let you define associations from a record to another through a third one. For example, they let you easily express many-to-many relations such as "a country has many citizens through its passports":
struct Country: TableRecord, EncodableRecord { static let passports = hasMany(Passport.self) // New! static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen) var citizens: QueryInterfaceRequest<Citizen> { return request(for: Country.citizens) } } struct Passport: TableRecord { static let citizen = belongsTo(Citizen.self) } struct Citizen: TableRecord { } let country: Country = ... let citizens: [Citizen] = try dbQueue.read { db in try country.citizens.fetchAll(db) }
-
Eager loading of HasMany associations: The new
including(all:)
method lets you load arrays or sets of associated records in a single request:// All authors with their respective books let request = Author.including(all: Author.books) // This request can feed the following record: struct AuthorInfo: FetchableRecord, Decodable { var author: Author var books: [Book] // all associated books } let authorInfos: [AuthorInfo] = try AuthorInfo.fetchAll(db, request)
See Joining And Prefetching Associated Records for more information.
-
Automatic pluralization and singularization of association identifiers.
GRDB will automatically pluralize or singularize names in order to help you easily associate records.
For example, the Book and Author records will automatically feed properties named
books
,author
, orbookCount
in your decoded records, without any explicit configuration, as long as the names of the backing database tables are "book" and "author".The GRDB pluralization mechanisms are very powerful, being capable of pluralizing and singularizing both regular and irregular words (it's directly inspired from the battle-tested Ruby on Rails inflections).
However, this change may have introduced some incompatibilities with GRDB 3 associations. Check The Structure of a Joined Request for more information.
The integration of GRDB with SQLCipher has changed.
-
With GRDB 3, it was possible to perform a manual installation, or to use CocoaPods and the GRDBCipher pod.
With GRDB 4, CocoaPods is the only supported installation method. And the GRDBCipher pod is discontinued, replaced with GRDB.swift/SQLCipher:
-pod 'GRDBCipher' +pod 'GRDB.swift/SQLCipher'
In your Swift code, you no longer import the GRDBCipher module, but GRDB:
-import GRDBCipher +import GRDB
-
The default SQLCipher version which comes with GRDB 4 is now SQLCipher 4, which is incompatible with SQLCipher 3. SQLCipher 3 is still supported, though. See Encryption for more details.
-
The
cipherPageSize
andkdfIterations
configuration properties are discontinued. With GRDB 4, run sql pragmas in theprepareDatabase
property of the configuration:var configuration = Configuration() configuration.passphrase = "secret" configuration.prepareDatabase = { db in try db.execute(sql: "PRAGMA cipher_page_size = 4096") try db.execute(sql: "PRAGMA kdf_iter = 128000") } let dbQueue = try DatabaseQueue(path: "...", configuration: configuration)
PersistenceError.recordNotFound is thrown whenever a record update method does not find any database row to update. It was refactored in GRDB 4:
public enum PersistenceError: Error {
case recordNotFound(databaseTableName: String, key: [String: DatabaseValue])
}
do {
try player.updateChanges {
$0.score += 1000
}
} catch let error as PersistenceError.recordNotFound {
// Update failed because player does not exist in the database
print(error)
// prints "Key not found in table player: [id:42]"
}