GRDB 3 comes with new features, but also a few breaking changes, and a set of updated good practices. This guide aims at helping you upgrading your applications.
For all users
- Swift 4.1 Required
- iOS 8 Sunsetting
- Database Schema Recommendations
- Record Protocols Renaming
- Columns Definition
By topic
- If You Use Database Queues
- If You Use Database Pools
- If You Use Database Snapshots
- If You Use Record Types
- If You Use Custom Requests
- If You Use RxGRDB
- Notable Documentation Updates
GRDB 3 uses conditional conformances, introduced in Swift 4.1. It can only be built on Xcode 9.3+.
GRDB 3 is only tested on iOS 9+, due to a limitation in Xcode 9.3. Code that targets older versions of SQLite and iOS is still there, but is not supported.
GRDB 2 was totally schema-agnostic, and would gladly accept any database.
GRDB 3 still accepts any database, but brings two schema recommendations:
-
💡 Integer primary keys should be auto-incremented, in order to avoid any row id to be reused.
When ids can be reused, your app and database observation tools may think that a row was updated, when it was actually deleted, then replaced. Depending on your application needs, this may be OK. Or not.
GRDB 3 thus comes with a new good practice: use the
autoIncrementedPrimaryKey
method when you create a database table with an integer primary key:try db.create(table: "author") { t in - t.column("id", .integer).primaryKey() // GRDB 2 + t.autoIncrementedPrimaryKey("id") // GRDB 3 recommendation t.column("name", .text).notNull() }
-
💡 Database table names should be singular, and camel-cased. Make them look like Swift identifiers:
place
,country
,postalAddress
, 'httpRequest'.This will help you using the new Associations feature when you need it. Database table names that follow another naming convention are totally OK, but you will need to perform extra configuration.
This convention is applied by the default implementation of the
TableRecord.databaseTableName
: see If You Use Record Types below.
Since you are reading this guide, your application has already defined its database schema. You can migrate it in order to apply the new recommendations, if needed. Below is a sample code that uses DatabaseMigrator, the recommended tool for managing your database schema:
var migrator = DatabaseMigrator()
// Existing GRDB 2 migration:
migrator.registerMigration("initial") { db in
try db.create(table: "authors") { t in
t.column("id", .integer).primaryKey()
t.column("name", .text).notNull()
}
try db.create(table: "books") { t in
t.column("id", .integer).primaryKey()
t.column("authorId", .integer).notNull().references("authors")
t.column("title", .text).notNull()
}
}
// New GRDB 3 migration:
// - Rename tables so that they look like Swift identifiers (singular and camelCased)
// - Make integer primary keys auto-incremented
//
// Since several tables are recreated from scratch, referential integrity
// constraints may break during the process. The
// registerMigrationWithDeferredForeignKeyCheck method makes sure that foreign
// key checks are temporarily disabled, and checked at the end:
migrator.registerMigrationWithDeferredForeignKeyCheck("GRDB3") { db in
try db.create(table: "author") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
}
try db.create(table: "book") { t in
t.autoIncrementedPrimaryKey("id")
t.column("authorId", .integer).notNull().references("author")
t.column("title", .text).notNull()
}
try db.execute("""
INSERT INTO author SELECT * FROM authors;
INSERT INTO book SELECT * FROM books;
""")
try db.drop(table: "authors")
try db.drop(table: "books")
}
GRDB 3 has renamed the record protocols:
RowConvertible
->FetchableRecord
TableMapping
->TableRecord
Persistable
->PersistableRecord
MutablePersistable
->MutablePersistableRecord
After upgrading, build your project: the compiler will guide you through the renaming by the way of fixits.
GRDB 2 has you define columns of the query interface with the Column type:
// GRDB 2
let nameColumn = Column("name")
let arthur = try Player.filter(nameColumn == "Arthur").fetchOne(db)
A recommended practice was to define enum namespaces in record types:
// GRDB 2
struct Player: RowConvertible, TableMapping {
enum Columns {
static let id = Column("id")
static let name = Column("name")
static let score = Column("score")
}
init(row: Row) {
id = row[Columns.id]
name = row[Columns.name]
score = row[Columns.score]
}
}
In GRDB 3, Column
is still there, but a new ColumnExpression
protocol has been introduced in order to streamline column enums:
// GRDB 3
struct Player: FetchableRecord, TableRecord {
enum Columns: String, ColumnExpression {
case id, name, score
}
init(row: Row) {
id = row[Columns.id]
name = row[Columns.name]
score = row[Columns.score]
}
}
extension Player {
static func filter(name: String) -> QueryInterfaceRequest<Player> {
return filter(Columns.name == name)
}
static var maximumScore: QueryInterfaceRequest<Int> {
return select(max(Columns.score), as: Int.self)
}
}
When your record adopts the Codable protocol, you can use its coding keys to safely define database columns:
// GRDB 3
struct Player: Codable {
var id: Int64
var name: String
var score: Int
}
extension Player: FetchableRecord, PersistableRecord {
enum Columns {
static let id = Column(CodingKeys.id.stringValue)
static let name = Column(CodingKeys.name.stringValue)
static let score = Column(CodingKeys.score.stringValue)
}
static func filter(name: String) -> QueryInterfaceRequest<Player> {
return filter(Columns.name == name)
}
static var maximumScore: QueryInterfaceRequest<Int> {
return select(max(Columns.score), as: Int.self)
}
}
With GRDB 2, you used to access the database through the inDatabase
or inTransaction
DatabaseQueue methods:
// GRDB 2
let players = try dbQueue.inDatabase { db in
try Player.fetchAll(db)
}
try dbQueue.inDatabase { db in
try player.updateChanges(db)
}
var balance: Amount! = nil
try dbQueue.inTransaction { db in
try Credit(destinationAccout, amount).insert(db)
try Debit(sourceAccount, amount).insert(db)
balance = try sourceAccount.fetchBalance(db)
return .commit
}
The code above still runs, unchanged, in GRDB 3.
Yet it is now recommended that you use the read
and write
methods instead:
// GRDB 3
let players = try dbQueue.read { db in
try Player.fetchAll(db)
}
try dbQueue.write { db in
try player.updateChanges(db)
}
let balance = try dbQueue.write { db in
try Credit(destinationAccout, amount).insert(db)
try Debit(sourceAccount, amount).insert(db)
return try sourceAccount.fetchBalance(db)
}
The purpose of the new read
and write
methods is to soothe the "transaction mental load" of previous versions of GRDB, a legacy of the FMDB heritage. All developers can forget to open transactions, with the unfortunate consequence that the database may end up containing inconsistent values. Experienced developers may wonder whether they should open transactions or not, even when this doesn't matter a lot.
With GRDB 3, use read
when you need to read values. It's impossible to write within a read
block, which means that you can be sure that no unwanted side effect can happen.
When you need to write, use write
: your database changes are automatically wrapped in a transaction, with the guarantee that all changes are written to disk, or, should any error happen, none at all.
Of course, precise transaction handling sometimes matter. Check the updated Transactions and Savepoints chapter.
With GRDB 2, you used to access the database through the read
, write
or writeInTransaction
DatabasePool methods:
// GRDB 2
let players = try dbPool.read { db in
try Player.fetchAll(db)
}
try dbPool.write { db in
try player.updateChanges(db)
}
var balance: Amount! = nil
try dbPool.writeInTransaction { db in
try Credit(destinationAccout, amount).insert(db)
try Debit(sourceAccount, amount).insert(db)
balance = try sourceAccount.fetchBalance(db)
return .commit
}
In GRDB 3, the write
method has changed: it now automatically wraps your database changes in a transaction, which means that the last block can be rewritten as below:
// GRDB 3
let balance = try dbPool.write { db in
try Credit(destinationAccout, amount).insert(db)
try Debit(sourceAccount, amount).insert(db)
return try sourceAccount.fetchBalance(db)
}
Side effect: you can no longer open explicit transactions inside a write
block:
// GRDB 2: OK
// GRDB 3: SQLite error 1 with statement `BEGIN DEFERRED TRANSACTION`:
// cannot start a transaction within a transaction
try dbPool.write { db in
try db.inTransaction { ... }
}
When precise transaction handling is needed, check the updated Transactions and Savepoints chapter.
The purpose of this change is to prevent an easy misuse of database pools in previous GRDB versions. Unless writes were wrapped inside an explicit transactions, concurrent reads could see an inconsistent state of the database:
// GRDB 2: unsafe
// GRDB 3: safe
try dbPool.write { db in
try Credit(destinationAccout, amount).insert(db)
try Debit(sourceAccount, amount).insert(db)
}
Since write
now wraps your changes in a transaction, you have the guarantee that concurrent reads can't see them until they are all written to disk.
With GRDB 2, you used to create database snapshots with the makeSnaphot
DatabasePool method. For example:
// GRDB 2
let snapshot: DatabaseSnapshot = try dbPool.write { db in
try Player.deleteAll()
return dbPool.makeSnapshot()
}
// Guaranteed to be zero
let count = try snapshot.read { db in
try Player.fetchCount(db)
}
GRDB 3 will crash the above code on the makeSnapshot()
line, with a fatal error: "makeSnapshot() must not be called from inside a transaction."
To avoid this error, you will need precise transaction handling:
// GRDB 3
let snapshot: DatabaseSnapshot = try dbPool.writeWithoutTransaction { db in
try Player.deleteAll()
return dbPool.makeSnapshot()
}
Record types that adopt the former TableMapping protocol, renamed TableRecord, used to declare their table name:
// GRDB 2
struct Place: TableMapping {
static let databaseTableName = "place"
}
print(Place.databaseTableName) // print "place"
With GRDB 3, the databaseTableName
property gets a default implementation:
// GRDB 3
struct Place: TableRecord { }
print(Place.databaseTableName) // print "place"
That default name follows the Database Schema Recommendations: it is singular, camel-cased, and looks like a Swift identifier:
- Place:
place
- Country:
country
- PostalAddress:
postalAddress
- HTTPRequest:
httpRequest
- TOEFL:
toefl
When you subclass the Record class, the Swift compiler won't let you profit from this default name: you have to keep on providing an explicit table name:
// GRDB 2 and GRDB 3
class Place: Record {
override var databaseTableName: String {
return "place"
}
}
Custom requests let you escape the limitations of the query interface, when it can not generate the requests you need.
You may, for example, use SQLRequest
:
// GRDB 2
extension Player {
static func filter(color: Color) -> AnyTypedRequest<Player> {
let sql = "SELECT * FROM players WHERE color = ?"
let request = SQLRequest(sql, arguments: [color])
return request.asRequest(of: Player.self)
}
}
let players = try Player.filter(color: .red).fetchAll(db)
The AnyTypedRequest
type is no longer available, and SQLRequest
is now a generic type:
// GRDB 3
extension Player {
static func filter(color: Color) -> SQLRequest<Player> {
let sql = "SELECT * FROM players WHERE color = ?"
return SQLRequest(sql, arguments: [color])
}
}
let players = try Player.filter(color: .red).fetchAll(db)
See the updated Custom Requests chapter for more information.
Some RxGRDB APIs have slightly changed. Did you track multiple requests at the same time with "fetch tokens"?
// GRDB 2
dbQueue.rx
.fetchTokens(in: [request, ...])
.mapFetch { db in try fetchResult(db) }
.subscribe(...)
Now you'll write instead:
// GRDB 3
ValueObservation
.tracking([request, ...], fetch: { db in try fetchResult(db) })
.rx
.fetch(in: dbQueue)
.subscribe(...)
It's just a syntactic change, without any impact on the runtime.
GRDB 3.6 also introduces a new protocol, DatabaseRegionConvertible, that allows a better encapsulation of complex requests, and a streamlined observable definition.
For example:
// GRDB 2: Track a team and its players
let teamId = 1
let teamRequest = Team.filter(key: teamId)
let playersRequest = Player.filter(teamId: teamId)
dbQueue.rx
.fetchTokens(in: [teamRequest, playersRequest])
.mapFetch { db -> TeamInfo? in
guard let team = try teamRequest.fetchOne(db) else {
return nil
}
let players = try playersRequest.fetchAll(db)
return TeamInfo(team: team, players: players)
}
.subscribe(onNext: { teamInfo: TeamInfo? in
...
})
// GRDB 3: Track a team and its players
struct TeamInfoRequest: DatabaseRegionConvertible { ... }
let request = TeamInfoRequest(teamId: 1)
ValueObservation.tracking(request, fetch: { try request.fetchOne($0) })
.rx
.fetch(in: dbQueue)
.subscribe(onNext: { teamInfo: TeamInfo? in
...
})
If you have time, you may dig deeper in GRDB 3 with those updated documentation chapter:
- Database Queues: focus on the new
read
andwrite
methods. - Transactions and Savepoints: the chapter has been rewritten in order to introduce transactions as a power-user feature.
- ScopeAdapter: do you use row adapters? If so, have a look.
- Examples of Record Definitions: this new chapter provides a handy reference of the three main ways to define record types (Codable, plain struct, Record subclass).
- SQL Operators: the chapter introduces the new
joined(operator:)
method that lets you join a chain of expressions withAND
orOR
without nesting:[cond1, cond2, ...].joined(operator: .and)
. - Custom Requests: the old
Request
andTypedRequest
protocols have been replaced withFetchRequest
. If you want to know more about custom requests, check this chapter. - Migrations: learn how to check if a migration has been applied (very useful for migration tests).