GRDB.swift is an SQLite toolkit for Swift 2, from the author of GRMustache.
It ships with a low-level database API, plus application-level tools.
December 22, 2015: GRDB.swift 0.35.0 is out - Release notes. Follow @groue on Twitter for release announcements and usage tips.
Requirements: iOS 7.0+ / OSX 10.9+, Xcode 7+
Swift Package Manager: Use the Swift2.2 branch.
Why GRDB, when we already have the excellent ccgus/fmdb, and the very popular stephencelis/SQLite.swift?
GRDB owes a lot to FMDB. You will use the familiar and safe database queues you are used to. Yet you may appreciate that database errors are handled in the Swift way, and that fetching data is somewhat easier.
Your SQL skills are rewarded here. Complex queries are never treated differently from the simple ones. You won't lose a single feature or convenience by crafting custom SQL queries, on the contrary. And you are granted with type safety and all the niceties you expect from a real Swift library.
GRDB provides protocols and a Record class that help isolating database management code into database layer types, and avoid cluterring the rest of your application.
GRDB is fast. As fast, when not faster, than FMDB and SQLite.swift.
You can query your database right from the debugger.
- A low-level SQLite API that leverages the Swift 2 standard library.
- A Record class that wraps result sets, eats your custom SQL queries for breakfast, provides persistence operations, and changes tracking.
- Swift type freedom: pick the right Swift type that fits your data. Use Int64 when needed, or stick with the convenient Int. Store and read NSDate or NSDateComponents. Declare Swift enums for discrete data types. Define your own database-convertible types.
- Database migrations
- Database changes observation hooks
-
GRDB Reference (on cocoadocs.org)
-
Migrations: Transform your database as your application evolves.
-
Database Protocols, and Record
- RowConvertible Protocol: Don't fetch rows, fetch your custom types instead.
- DatabasePersistable Protocol: Grant any type with persistence methods.
- Record: The class that wraps a table row or the result of any query, provides persistence methods, and changes tracking.
-
Database Changes Observation: A robust way to perform post-commit and post-rollback actions.
You can use GRDB.swift in a project targetting iOS7. See GRDBDemoiOS7 for more information.
CocoaPods is a dependency manager for Xcode projects.
To use GRDB.swift with Cocoapods, specify in your Podfile:
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!
pod 'GRDB.swift', '0.35.0'
Carthage is another dependency manager for Xcode projects.
To use GRDB.swift with Carthage, specify in your Cartfile:
github "groue/GRDB.swift" == 0.35.0
- Download a copy of GRDB.swift.
- Embed the
GRDB.xcodeproj
project in your own project. - Add the
GRDBOSX
orGRDBiOS
target in the Target Dependencies section of the Build Phases tab of your application target. - Add
GRDB.framework
to the Embedded Binaries section of the General tab of your target.
See GRDBDemoiOS for an example of such integration.
import GRDB
// Open connection to database
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
try dbQueue.inDatabase { db in
// Create tables
try db.execute("CREATE TABLE wines (...)")
// Insert
let changes = try db.execute("INSERT INTO wines (color, name) VALUES (?, ?)",
arguments: [Color.Red, "Pomerol"])
let wineId = changes.insertedRowID
print("Inserted wine id: \(wineId)")
// Fetch rows
for row in Row.fetch(db, "SELECT * FROM wines") {
let name: String = row.value(named: "name")
let color: Color = row.value(named: "color")
print(name, color)
}
// Fetch values
let redWineCount = Int.fetchOne(db,
"SELECT COUNT(*) FROM wines WHERE color = ?",
arguments: [Color.Red])!
}
You access SQLite databases through thread-safe database queues (inspired by ccgus/fmdb):
import GRDB
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let inMemoryDBQueue = DatabaseQueue()
SQLite creates the database file if it does not already exist.
The connection is closed when the database queue gets deallocated.
Configure databases:
var config = Configuration()
config.trace = { print($0) } // Prints all SQL statements
let dbQueue = try DatabaseQueue(
path: "/path/to/database.sqlite",
configuration: config)
See Configuration and Concurrency for more details.
Once connected, the inDatabase
and inTransaction
methods perform your database statements in a dedicated, serial, queue:
// Execute database statements:
dbQueue.inDatabase { db in
for row in Row.fetch(db, "SELECT * FROM wines") {
let name: String = row.value(named: "name")
let color: Color = row.value(named: "color")
print(name, color)
}
}
// Extract values from the database:
let wineCount = dbQueue.inDatabase { db in
Int.fetchOne(db, "SELECT COUNT(*) FROM wines")!
}
// Wrap database statements in a transaction:
try dbQueue.inTransaction { db in
try db.execute("INSERT ...")
try db.execute("DELETE FROM ...")
return .Commit
}
See Transactions for more information about GRDB transaction handling.
The Database.execute
method executes the SQL statements that do not return any database row, such as CREATE TABLE
, INSERT
, DELETE
, ALTER
, etc.
For example:
try dbQueue.inDatabase { db in
try db.execute(
"CREATE TABLE persons (" +
"id INTEGER PRIMARY KEY," +
"name TEXT NOT NULL," +
"age INT" +
")")
try db.execute(
"INSERT INTO persons (name, age) VALUES (?, ?)",
arguments: ["Arthur", 36])
try db.execute(
"INSERT INTO persons (name, age) VALUES (:name, :age)",
arguments: ["name": "Barbara", "age": 39])
}
The ?
and colon-prefixed keys like :name
in the SQL query are the statements arguments. You pass arguments in with arrays or dictionaries, as in the example above (arguments are actually of type StatementArguments, which happens to adopt the ArrayLiteralConvertible and DictionaryLiteralConvertible protocols).
See Values for more information on supported arguments types (Bool, Int, String, NSDate, Swift enums, etc.).
After an INSERT statement, you extract the inserted Row ID from the result of the execute
method:
let insertedRowID = try db.execute(
"INSERT INTO persons (name, age) VALUES (?, ?)",
arguments: ["Arthur", 36]).insertedRowID
GRDB lets you fetch rows, values, and custom models.
Rows are the results of SQL queries (see row queries):
dbQueue.inDatabase { db in
for row in Row.fetch(db, "SELECT * FROM wines") {
let name: String = row.value(named: "name")
let color: Color = row.value(named: "color")
print(name, color)
}
}
Values are the Bool, Int, String, NSDate, Swift enums, etc that feed your application (see value queries):
dbQueue.inDatabase { db in
let redWineCount = Int.fetchOne(db,
"SELECT COUNT(*) FROM wines WHERE color = ?",
arguments: [Color.Red])!
}
Custom models are your application objects that can initialize themselves from rows (see the RowConvertible protocol and the Record class):
dbQueue.inDatabase { db in
let wines = Wine.fetchAll(db, "SELECT * FROM wines ORDER BY name")
let favoriteWine = Wine.fetchOne(db, key: user.favoriteWineId)
}
Rows, values, and custom models can all be fetched in three fashions. Pick one, depending on the number of values you expect, and the way you use them:
- The
fetch()
method returns a memory-efficient sequence that goes in the database as it is iterated. - The
fetchAll()
method returns an array which is less memory-efficient, but can be used from any thread. - The
fetchOne()
method returns a single optional value.
Most of those methods take an SQL query as an argument. If SQL is not your cup of tea, then maybe you are looking for a query builder. stephencelis/SQLite.swift is a pretty popular one.
Fetch sequences of rows, arrays, or a single row:
dbQueue.inDatabase { db in
Row.fetch(db, "SELECT ...", arguments: ...) // DatabaseSequence<Row>
Row.fetchAll(db, "SELECT ...", arguments: ...) // [Row]
Row.fetchOne(db, "SELECT ...", arguments: ...) // Row?
for row in Row.fetch(db, "SELECT * FROM wines") {
let name: String = row.value(named: "name")
let color: Color = row.value(named: "color")
print(name, color)
}
}
Arguments are optional arrays or dictionaries that fill the positional ?
and colon-prefixed keys like :name
in the query:
let rows = Row.fetch(db,
"SELECT * FROM persons WHERE name = ?",
arguments: ["Arthur"])
let rows = Row.fetch(db,
"SELECT * FROM persons WHERE name = :name",
arguments: ["name": "Arthur"])
See Values for more information on supported arguments types (Bool, Int, String, NSDate, Swift enums, etc.).
Both fetch
and fetchAll
let you iterate the full list of fetched rows. The differences are:
- The array returned by
fetchAll
can take a lot of memory. Yet it can be iterated on any thread. - The sequence returned by
fetch
only goes to the database as you iterate it, and is thus more memory efficient. The price for this efficiency is that the sequence must be iterated in the database queue (you'll get a fatal error if you do otherwise). - The sequence returned by
fetch
will return a different set of results if the database has been modified between two sequence iterations.
Row sequences also grant the fastest access to the database. This performance advantage comes with extra precautions:
☝️ Don't turn a row sequence into an array with
Array(rowSequence)
orrowSequence.filter { ... }
: you would not get the distinct rows you expect. To get an array, useRow.fetchAll(...)
.☝️ Make sure you copy a row whenever you extract it from a sequence for later use:
row.copy()
. This does not apply to row arrays, which already contain independent copies of the database rows.
Read column values by index or column name:
let name: String = row.value(atIndex: 0) // 0 is the leftmost column
let name: String = row.value(named: "name")
Make sure to ask for an optional when the value may be NULL:
let name: String? = row.value(named: "name")
The value
function returns the type you ask for. See Values for more information on supported value types:
let bookCount: Int = row.value(named: "bookCount")
let bookCount64: Int64 = row.value(named: "bookCount")
let hasBooks: Bool = row.value(named: "bookCount") // false when 0
let dateString: String = row.value(named: "date") // "2015-09-11 18:14:15.123"
let date: NSDate = row.value(named: "date") // NSDate
self.date = row.value(named: "date") // Depends on the type of the property.
You can also use the as
type casting operator:
row.value(...) as Int
row.value(...) as Int?
row.value(...) as Int!
⚠️ Warning: avoid theas!
andas?
operators (see rdar://21676393):row.value(...) as! Int // NO NO NO DON'T DO THAT! row.value(...) as? Int // NO NO NO DON'T DO THAT!
When you ask for a missing column, you will get nil, or a fatal error:
let row = Row.fetchOne(db, "SELECT 'foo' AS foo")!
row.value(named: "missing") as String? // nil
row.value(named: "missing") as String // fatal error: No such column: "missing"
row.value(atIndex: 1) // fatal error: Row index out of range
You can explicitly check for a column presence with the hasColumn
method.
Generally speaking, you can extract the type you need, provided it can be converted from the underlying SQLite value:
-
Successful conversions include:
- Numeric (integer and real) SQLite values to Swift Int, Int32, Int64, Double and Bool (zero is the only false boolean).
- Text SQLite values to Swift String.
- Blob SQLite values to NSData.
See Values for more information on supported types (NSDate, Swift enums, etc.).
-
Invalid conversions return nil.
let row = Row.fetchOne(db, "SELECT 'foo'")! row.value(atIndex: 0) as String // "foo" row.value(atIndex: 0) as NSDate? // nil row.value(atIndex: 0) as NSDate // fatal error: Could not convert "foo" to NSDate.
-
GRDB crashes when you try to convert NULL to a non-optional value.
This behavior is notably different from SQLite C API, or from ccgus/fmdb, that both turn NULL to 0 when extracting an integer, for example:
let row = Row.fetchOne(db, "SELECT NULL")! row.value(atIndex: 0) as Int? // nil row.value(atIndex: 0) as Int // fatal error: Could not convert NULL to Int.
-
The convenience conversions of SQLite, such as Blob to String, String to Int, or huge Double values to Int, are not guaranteed to apply. You must not rely on them.
You may prefer thinking of rows as dictionaries of DatabaseValue
, an intermediate type between SQLite and your values:
// Test if the column `date` is present:
if let databaseValue = row["date"] {
// Pick the type you need:
let dateString: String = databaseValue.value() // "2015-09-11 18:14:15.123"
let date: NSDate = databaseValue.value() // NSDate
self.date = databaseValue.value() // Depends on the type of the property.
// Check for NULL:
if databaseValue.isNull {
print("NULL")
}
// The five SQLite storage classes:
switch databaseValue.storage {
case .Null:
print("NULL")
case .Int64(let int64):
print("Int64: \(int64)")
case .Double(let double):
print("Double: \(double)")
case .String(let string):
print("String: \(string)")
case .Blob(let data):
print("NSData: \(data)")
}
}
Iterate all the tuples (columnName, databaseValue) in a row, from left to right:
for (columnName, databaseValue) in row {
...
}
Rows are not real dictionaries, though. They may contain duplicate keys:
let row = Row.fetchOne(db, "SELECT 1 AS foo, 2 AS foo")!
row.columnNames // ["foo", "foo"]
row.databaseValues // [1, 2]
row["foo"] // 1 (the value for the leftmost column "foo")
for (columnName, databaseValue) in row { ... } // ("foo", 1), ("foo", 2)
Rows is a fundamental type in GRDB, used by many other APIs.
From time to time, you'll want to build a custom one from scratch. Use the dictionary initializer:
Row(dictionary: ["name": "foo", "date": nil])
See Values for more information on supported types.
Instead of rows, you can directly fetch values. Like rows, fetch them as sequences, arrays, or single values. Values are extracted from the leftmost column of the SQL queries:
dbQueue.inDatabase { db in
Int.fetch(db, "SELECT ...", arguments: ...) // DatabaseSequence<Int>
Int.fetchAll(db, "SELECT ...", arguments: ...) // [Int]
Int.fetchOne(db, "SELECT ...", arguments: ...) // Int?
// When database may contain NULL:
Optional<Int>.fetch(db, "SELECT ...", arguments: ...) // DatabaseSequence<Int?>
Optional<Int>.fetchAll(db, "SELECT ...", arguments: ...) // [Int?]
}
There are many supported value types (Bool, Int, String, NSDate, Swift enums, etc.). See Values for more information:
dbQueue.inDatabase { db in
// The number of persons with an email ending in @example.com:
let count = Int.fetchOne(db,
"SELECT COUNT(*) FROM persons WHERE email LIKE ?",
arguments: ["%@example.com"])!
// All URLs:
let urls = NSURL.fetchAll(db, "SELECT url FROM links")
// The emails of people who own at least two pets:
let emails = Optional<String>.fetchAll(db,
"SELECT persons.email " +
"FROM persons " +
"JOIN pets ON pets.masterId = persons.id " +
"GROUP BY persons.id " +
"HAVING COUNT(pets.id) >= 2")
}
Both fetch
and fetchAll
let you iterate the full list of fetched values. The differences are:
- The array returned by
fetchAll
can take a lot of memory. Yet it can be iterated on any thread. - The sequence returned by
fetch
only goes to the database as you iterate it, and is thus more memory efficient. The price for this efficiency is that the sequence must be iterated in the database queue (you'll get a fatal error if you do otherwise). - The sequence returned by
fetch
will return a different set of results if the database has been modified between two sequence iterations.
fetchOne
returns an optional value which is nil in two cases: either the SELECT statement yielded no row, or one row with a NULL value.
GRDB ships with built-in support for the following value types:
-
Swift Standard Library: Bool, Float, Double, Int, Int32, Int64, String, Swift enums.
-
Foundation: NSData, NSDate, NSDateComponents, NSNull, NSNumber, NSString, NSURL.
-
CoreGraphics: CGFloat.
All those types can be used as statement arguments:
let url: NSURL = ...
let verified: Bool = ...
try db.execute(
"INSERT INTO links (url, verified) VALUES (?, ?)",
arguments: [url, verified])
They can be extracted from rows:
for row in Row.fetch(db, "SELECT * FROM links") {
let url: NSURL = row.value(named: "url")
let verified: Bool = row.value(named: "verified")
}
They can be directly fetched from the database:
let urls = NSURL.fetchAll(db, "SELECT url FROM links") // [NSURL]
Use them in the persistentDictionary
property of DatabasePersistable protocol and Record subclasses:
class Link : Record {
var url: NSURL?
var verified: Bool
override var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["url": url, "verified": verified]
}
}
Your custom value types are supported as well, through the DatabaseValueConvertible protocol.
NSData suits the BLOB SQLite columns. It can be stored and fetched from the database just like other value types:
let row = Row.fetchOne(db, "SELECT data, ...")!
let data: NSData = row.value(named: "data")
NSData.fetch(db, "SELECT ...") // DatabaseSequence<NSData>
NSData.fetchAll(db, "SELECT ...") // [NSData]
NSData.fetchOne(db, "SELECT ...") // NSData?
Yet, when extracting NSData from a row, you have the opportunity to save memory by not copying the data fetched by SQLite, using the dataNoCopy()
method:
for row in Row.fetch(db, "SELECT data, ...") {
let data = row.dataNoCopy(named: "data") // NSData?
// When the column `data` may not be there:
if row.hasColumn("data") {
let data = row.dataNoCopy(named: "data") // NSData?
}
}
☝️ Note: The non-copied data does not live longer than the iteration step: make sure that you do not use it past this point.
Compare with the anti-patterns below:
for row in Row.fetch(db, "SELECT data, ...") {
// Data is copied, row after row:
let data: NSData = row.value(named: "data")
// Data is copied, row after row:
if let databaseValue = row["data"] {
let data: NSData = databaseValue.value()
}
}
// All rows have been copied in memory when the loop begins:
for row in Row.fetchAll(db, "SELECT data, ...") {
// Too late to do the right thing:
let data = row.dataNoCopy(named: "data")
}
NSDate and NSDateComponents can be stored and fetched from the database.
Here is the support provided by GRDB.swift for the various date formats supported by SQLite:
SQLite format | NSDate | NSDateComponents |
---|---|---|
YYYY-MM-DD | Read ¹ | Read/Write |
YYYY-MM-DD HH:MM | Read ¹ | Read/Write |
YYYY-MM-DD HH:MM:SS | Read ¹ | Read/Write |
YYYY-MM-DD HH:MM:SS.SSS | Read/Write ¹ | Read/Write |
YYYY-MM-DDTHH:MM | Read ¹ | Read |
YYYY-MM-DDTHH:MM:SS | Read ¹ | Read |
YYYY-MM-DDTHH:MM:SS.SSS | Read ¹ | Read |
HH:MM | Read/Write | |
HH:MM:SS | Read/Write | |
HH:MM:SS.SSS | Read/Write | |
Julian Day Number | Read ² | |
now |
¹ NSDates are stored and read in the UTC time zone. Missing components are assumed to be zero.
² See https://en.wikipedia.org/wiki/Julian_day
GRDB stores NSDate using the format "yyyy-MM-dd HH:mm:ss.SSS" in the UTC time zone.
☝️ Note: This format is lexically comparable with SQLite's CURRENT_TIMESTAMP, which means that your ORDER BY clauses will behave as expected.
Yet, this format may not fit your needs. We provide below some sample code for storing dates as timestamps. You can adapt it for your application.
Declare DATETIME columns in your tables:
try db.execute(
"CREATE TABLE persons (" +
"creationDate DATETIME, " +
"...)")
Store NSDate into the database:
let creationDate = NSDate()
try db.execute("INSERT INTO persons (creationDate, ...) " +
"VALUES (?, ...)",
arguments: [creationDate, ...])
Extract NSDate from the database:
let row = Row.fetchOne(db, "SELECT creationDate, ...")!
let date: NSDate = row.value(named: "creationDate")
NSDate.fetch(db, "SELECT ...") // DatabaseSequence<NSDate>
NSDate.fetchAll(db, "SELECT ...") // [NSDate]
NSDate.fetchOne(db, "SELECT ...") // NSDate?
See Column Values and Value Queries for more information.
NSDateComponents is indirectly supported, through the DatabaseDateComponents helper type.
DatabaseDateComponents reads date components from all date formats supported by SQLite, and stores them in the format of your choice, from HH:MM to YYYY-MM-DD HH:MM:SS.SSS.
Declare DATETIME columns in your tables:
try db.execute(
"CREATE TABLE persons (" +
"birthDate DATETIME, " +
"...)")
Store NSDateComponents into the database:
let components = NSDateComponents()
components.year = 1973
components.month = 9
components.day = 18
// The .YMD format stores "1973-09-18" in the database.
let dbComponents = DatabaseDateComponents(components, format: .YMD)
try db.execute("INSERT INTO persons (birthDate, ...) " +
"VALUES (?, ...)",
arguments: [dbComponents, ...])
Extract NSDateComponents from the database:
let row = Row.fetchOne(db, "SELECT birthDate ...")!
let dbComponents: DatabaseDateComponents = row.value(named: "birthDate")
dbComponents.format // .YMD (the actual format found in the database)
dbComponents.dateComponents // NSDateComponents
DatabaseDateComponents.fetch(db, "SELECT ...") // DatabaseSequence<DatabaseDateComponents>
DatabaseDateComponents.fetchAll(db, "SELECT ...") // [DatabaseDateComponents]
DatabaseDateComponents.fetchOne(db, "SELECT ...") // DatabaseDateComponents?
See Column Values and Value Queries for more information.
Swift enums get full support from GRDB.swift as long as their raw values are Int, Int32, Int64 or String.
Given those two enums:
enum Color : Int {
case Red
case White
case Rose
}
enum Grape : String {
case Chardonnay
case Merlot
case Riesling
}
Simply add those two lines:
extension Color : DatabaseIntRepresentable { } // DatabaseInt32Representable for Int32, DatabaseInt64Representable for Int64
extension Grape : DatabaseStringRepresentable { }
And both types gain database powers:
// Store:
try db.execute("INSERT INTO wines (grape, color) VALUES (?, ?)",
arguments: [Grape.Merlot, Color.Red])
// Extract from row:
for rows in Row.fetch(db, "SELECT * FROM wines") {
let grape: Grape = row.value(named: "grape")
let color: Color = row.value(named: "color")
}
// Direct fetch:
Color.fetch(db, "SELECT ...", arguments: ...) // DatabaseSequence<Color>
Color.fetchAll(db, "SELECT ...", arguments: ...) // [Color]
Color.fetchOne(db, "SELECT ...", arguments: ...) // Color?
See Column Values and Value Queries for more information.
Conversion to and from the database is based on the DatabaseValueConvertible
protocol:
public protocol DatabaseValueConvertible {
/// Returns a value that can be stored in the database.
var databaseValue: DatabaseValue { get }
/// Returns a value initialized from databaseValue, if possible.
static func fromDatabaseValue(databaseValue: DatabaseValue) -> Self?
}
All types that adopt this protocol can be used wherever the built-in types Int
, String
, etc. are used. without any limitation or caveat. Those built-in types actually adopt it.
The databaseValue
property returns DatabaseValue, a type that wraps the five types supported by SQLite: NULL, Int64, Double, String and NSData. DatabaseValue has no public initializer: to create one, use DatabaseValue.Null
, or the fact that Int, String, etc. adopt the protocol: 1.databaseValue
, "foo".databaseValue
.
The fromDatabaseValue()
factory method returns an instance of your custom type, if the databaseValue contains a suitable value.
As an example, let's write an alternative to the built-in NSDate behavior, and store dates as timestamps. Our sample DatabaseTimestamp type applies all the best practices for a great GRDB.swift integration:
struct DatabaseTimestamp: DatabaseValueConvertible {
// NSDate conversion
//
// Value types should consistently use the Swift nil to represent the
// database NULL: the date property is a non-optional NSDate.
let date: NSDate
// As a convenience, the NSDate initializer accepts an optional NSDate, and
// is failable: the result is nil if and only if *date* is nil.
init?(_ date: NSDate?) {
guard let date = date else {
return nil
}
self.date = date
}
// DatabaseValueConvertible adoption
/// Returns a value that can be stored in the database.
var databaseValue: DatabaseValue {
// Double itself adopts DatabaseValueConvertible:
return date.timeIntervalSince1970.databaseValue
}
/// Returns a value initialized from *databaseValue*, if possible.
static func fromDatabaseValue(databaseValue: DatabaseValue) -> DatabaseTimestamp? {
// Double itself adopts DatabaseValueConvertible:
guard let timeInterval = Double.fromDatabaseValue(databaseValue) else {
// No Double, no NSDate!
return nil
}
return DatabaseTimestamp(NSDate(timeIntervalSince1970: timeInterval))
}
}
As a DatabaseValueConvertible adopter, DatabaseTimestamp can be stored and fetched from the database just like simple types Int and String:
// Store NSDate
let date = NSDate()
try db.execute("INSERT INTO persons (date, ...) " +
"VALUES (?, ...)",
arguments: [DatabaseTimestamp(date), ...])
// Extract NSDate from row:
for rows in Row.fetch(db, "SELECT ...") {
let date = (row.value(named: "date") as DatabaseTimestamp).date
}
// Direct fetch:
DatabaseTimestamp.fetch(db, "SELECT ...") // DatabaseSequence<DatabaseTimestamp>
DatabaseTimestamp.fetchAll(db, "SELECT ...") // [DatabaseTimestamp]
DatabaseTimestamp.fetchOne(db, "SELECT ...") // DatabaseTimestamp?
See Column Values and Value Queries for more information.
The DatabaseQueue.inTransaction()
method opens an SQLite transaction:
try dbQueue.inTransaction { db in
let wine = Wine(color: .Red, name: "Pomerol")
try wine.insert(db)
return .Commit
}
A ROLLBACK statement is issued if an error is thrown within the transaction block.
Otherwise, transactions are guaranteed to succeed, provided there is a single DatabaseQueue connected to the database file. See Concurrency for more information about concurrent database access.
If you want to insert a transaction between other database statements, and group those in a single block of code protected by the the database queue, you can use the Database.inTransaction() function:
try dbQueue.inDatabase { db in
...
try db.inTransaction {
...
return .Commit
}
...
}
SQLite supports three kinds of transactions: DEFERRED, IMMEDIATE, and EXCLUSIVE. GRDB defaults to IMMEDIATE.
The transaction kind can be changed in the database configuration, or for each transaction:
var config = Configuration()
config.defaultTransactionKind = .Deferred
let dbQueue = try DatabaseQueue(path: "...", configuration: config)
// Opens a DEFERRED transaction:
dbQueue.inTransaction { db in ... }
// Opens an EXCLUSIVE transaction:
dbQueue.inTransaction(.Exclusive) { db in ... }
No SQLite error goes unnoticed. Yet when such an error happens, some GRDB.swift functions throw a DatabaseError error, and some crash with a fatal error.
The rule is:
- All methods that write to the database throw.
- All other methods crash without notice (but with a detailed error message).
// fatal error:
// SQLite error 1 with statement `SELECT foo FROM bar`: no such table: bar
Row.fetchAll(db, "SELECT foo FROM bar")
do {
try db.execute(
"INSERT INTO pets (masterId, name) VALUES (?, ?)",
arguments: [1, "Bobby"])
} catch let error as DatabaseError {
// SQLite error 19 with statement `INSERT INTO pets (masterId, name)
// VALUES (?, ?)` arguments [1, "Bobby"]: FOREIGN KEY constraint failed
error.description
// The SQLite result code: 19 (SQLITE_CONSTRAINT)
error.code
// The eventual SQLite message
// "FOREIGN KEY constraint failed"
error.message
// The eventual erroneous SQL query
// "INSERT INTO pets (masterId, name) VALUES (?, ?)"
error.sql
}
See SQLite Result Codes.
Prepared Statements let you prepare an SQL query and execute it later, several times if you need, with different arguments.
There are two kinds of prepared statements: select statements, and update statements:
try dbQueue.inTransaction { db in
let updateSQL = "INSERT INTO persons (name, age) VALUES (:name, :age)"
let updateStatement = db.updateStatement(updateSQL)
let selectSQL = "SELECT * FROM persons WHERE name = ?"
let selectStatement = db.selectStatement(selectSQL)
}
The ?
and colon-prefixed keys like :name
in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type StatementArguments, which happens to adopt the ArrayLiteralConvertible and DictionaryLiteralConvertible protocols).
// INSERT INTO persons (name, age) VALUES (:name, :age)
updateStatement.arguments = ["name": "Arthur", "age": 41]
// SELECT * FROM persons WHERE name = ?
selectStatement.arguments = ["Arthur"]
After arguments are set, you can execute the prepared statement:
let changes = try updateStatement.execute()
changes.changedRowCount // The number of rows changed by the statement.
changes.insertedRowID // For INSERT statements, the inserted Row ID.
for row in Row.fetch(selectStatement) { ... }
for person in Person.fetch(selectStatement) { ... }
It is possible to set the arguments at the moment of the statement execution:
// INSERT INTO persons (name, age) VALUES (:name, :age)
try statement.execute(arguments: ["name": "Arthur", "age": 41])
// SELECT * FROM persons WHERE name = ?
let person = Person.fetchOne(selectStatement, arguments: ["Arthur"])
Select statements can be used wherever a raw SQL query would fit:
Row.fetch(statement, arguments: ...) // DatabaseSequence<Row>
Row.fetchAll(statement, arguments: ...) // [Row]
Row.fetchOne(statement, arguments: ...) // Row?
String.fetch(statement, arguments: ...) // DatabaseSequence<String>
String.fetchAll(statement, arguments: ...) // [String]
String.fetchOne(statement, arguments: ...) // String?
Person.fetch(statement, arguments: ...) // DatabaseSequence<Person>
Person.fetchAll(statement, arguments: ...) // [Person]
Person.fetchOne(statement, arguments: ...) // Person?
See Row Queries, Value Queries, RowConvertible, and Records for more information.
When your application has a single DatabaseQueue connected to the database file, it has no concurrency issue. That is because all your database statements are executed in a single serial dispatch queue that is connected alone to the database.
Things turn more complex as soon as there are several connections to a database file.
SQLite concurrency management is fragmented. Documents of interest include:
- General discussion about isolation in SQLite: https://www.sqlite.org/isolation.html
- Types of locks and transactions: https://www.sqlite.org/lang_transaction.html
- WAL journal mode: https://www.sqlite.org/wal.html
- Busy handlers: https://www.sqlite.org/c3ref/busy_handler.html
By default, GRDB opens database in the default journal mode, uses IMMEDIATE transactions, and registers no busy handler of any kind.
See Configuration type and DatabaseQueue.inTransaction() method for more precise handling of transactions and eventual SQLITE_BUSY errors.
SQLite lets you define SQL functions.
You can for example use the Unicode support of Swift strings, and go beyond the ASCII limitations of the built-in SQLite upper()
function:
dbQueue.inDatabase { db in
let fn = DatabaseFunction("unicodeUpper", argumentCount: 1, pure: true) { (databaseValues: [DatabaseValue]) in
// databaseValues is guaranteed to have `argumentCount` elements:
let dbv = databaseValues[0]
guard let string: String = dbv.value() else {
return nil
}
return string.uppercaseString
}
db.addFunction(fn)
// "É"
String.fetchOne(db, "SELECT unicodeUpper(?)", arguments: ["é"])!
// "é"
String.fetchOne(db, "SELECT upper(?)", arguments: ["é"])!
}
See Rows as Dictionaries for more information about the DatabaseValue
type.
The result of a pure function only depends on its arguments (unlike the built-in random()
SQL function, for example). SQLite has the opportunity to perform additional optimizations when functions are pure.
See Values for more information on supported arguments and return types (Bool, Int, String, NSDate, Swift enums, etc.).
Functions can take a variable number of arguments:
dbQueue.inDatabase { db in
let fn = DatabaseFunction("sumOf", pure: true) { (databaseValues: [DatabaseValue]) in
let ints: [Int] = databaseValues.flatMap { $0.value() }
return ints.reduce(0, combine: +)
}
db.addFunction(fn)
// 6
Int.fetchOne(db, "SELECT sumOf(1, 2, 3)")!
}
Functions can throw:
dbQueue.inDatabase { db in
let fn = DatabaseFunction("sqrt", argumentCount: 1, pure: true) { (databaseValues: [DatabaseValue]) in
let dbv = databaseValues[0]
guard let double: Double = dbv.value() else {
return nil
}
guard double >= 0.0 else {
throw DatabaseError(message: "Invalid negative value in function sqrt()")
}
return sqrt(double)
}
db.addFunction(fn)
// fatal error: SQLite error 1 with statement `SELECT sqrt(-1)`:
// Invalid negative value in function sqrt()
Double.fetchOne(db, "SELECT sqrt(-1)")
}
See Error Handling for more information on database errors.
When SQLite compares two strings, it uses a collating function to determine which string is greater or if the two strings are equal.
SQLite lets you define your own collating functions. This is how you can bring support for unicode or localization when comparing strings:
dbQueue.inDatabase { db in
// Define the localized_case_insensitive collation:
let collation = DatabaseCollation("localized_case_insensitive") { (lhs, rhs) in
return (lhs as NSString).localizedCaseInsensitiveCompare(rhs)
}
db.addCollation(collation)
// Put it to some use:
try db.execute("CREATE TABLE persons (lastName TEXT COLLATE LOCALIZED_CASE_INSENSITIVE)")
// Persons are sorted as expected:
Person.fetchAll(db, "SELECT * FROM persons ORDER BY lastName")
}
Check https://www.sqlite.org/datatype3.html#collation for more information.
Not all SQLite APIs are exposed in GRDB.
The Database.sqliteConnection
and Statement.sqliteStatement
properties provide the raw pointers that are suitable for SQLite C API:
dbQueue.inDatabase { db in
let sqliteConnection = db.sqliteConnection
sqlite3_db_config(sqliteConnection, ...)
let statement = db.selectStatement("SELECT ...")
let sqliteStatement = statement.sqliteStatement
sqlite3_step(sqliteStatement)
}
☝️ Notes
- Those pointers are owned by GRDB: don't close connections or finalize statements created by GRDB.
- SQLite connections are opened in the Multi-thread mode, which means that they are not thread-safe. Make sure you touch raw databases and statements inside the database queues.
Before jumping in the low-level wagon, here is a reminder of SQLite APIs supported by GRDB:
- Connections & statements, obviously.
- Errors (pervasive)
- Inserted Row IDs (as the result of Database.execute()).
- Changes count (as the result of Database.execute()).
- Custom SQL functions (see Custom SQL Functions)
- Custom collations (see Custom Collations)
- Busy mode (see Concurrency).
- Update, commit and rollback hooks (see Database Changes Observation):
On top of the SQLite API described above, GRDB provides a toolkit for applications. While none of those are mandatory, all of them help dealing with the database:
- Migrations: Transform your database as your application evolves.
- Database Protocols, and Record
- RowConvertible Protocol: Don't fetch rows, fetch your custom types instead.
- DatabasePersistable Protocol: Grant any type with persistence methods.
- Record: The class that wraps a table row or the result of any query, provides persistence methods, and changes tracking.
- Database Changes Observation: A robust way to perform post-commit and post-rollback actions.
Migrations are a convenient way to alter your database schema over time in a consistent and easy way.
Migrations run in order, once and only once. When a user upgrades your application, only non-applied migrations are run.
var migrator = DatabaseMigrator()
// v1.0 database
migrator.registerMigration("createTables") { db in
try db.execute("CREATE TABLE persons (...)")
try db.execute("CREATE TABLE books (...)")
}
// v2.0 database
migrator.registerMigration("AddAgeToPersons") { db in
try db.execute("ALTER TABLE persons ADD COLUMN age INT")
}
migrator.registerMigration("AddYearToBooks") { db in
try db.execute("ALTER TABLE books ADD COLUMN year INT")
}
// (Insert migrations for future versions here)
try migrator.migrate(dbQueue)
Each migration runs in a separate transaction. Should one throw an error, its transaction is rollbacked, subsequent migrations do not run, and the error is eventually thrown by migrator.migrate(dbQueue)
.
The memory of applied migrations is stored in the database itself (in a reserved table). When you are tuning your migrations, you may need to execute one several times. All you need then is to feed your application with a database file from a previous state.
You might use Database.executeMultiStatement(): this method takes an SQL string containing multiple statements separated by semi-colons:
migrator.registerMigration("createTables") { db in
try db.executeMultiStatement(
"CREATE TABLE persons (...);" +
"CREATE TABLE books (...);" +
"...")
}
You might even store your migrations as bundle resources:
// Execute migration01.sql, migration02.sql, etc.
NSBundle.mainBundle()
.pathsForResourcesOfType("sql", inDirectory: "databaseMigrations")
.sort()
.forEach { path in
let migrationName = (path as NSString).lastPathComponent
migrator.registerMigration(migrationName) { db in
let sql = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
try db.executeMultiStatement(sql)
}
}
SQLite does not support many schema changes, and won't let you drop a table column with "ALTER TABLE ... DROP COLUMN ...", for example.
Yet any kind of schema change is still possible. The SQLite documentation explains in detail how to do so: https://www.sqlite.org/lang_altertable.html#otheralter. This technique requires the temporary disabling of foreign key checks:
// Add a NOT NULL constraint on persons.name:
migrator.registerMigration("AddNotNullCheckOnName", withDisabledForeignKeyChecks: true) { db in
try db.executeMultiStatement(
"CREATE TABLE new_persons (id INTEGER PRIMARY KEY, name TEXT NOT NULL);" +
"INSERT INTO new_persons SELECT * FROM persons;" +
"DROP TABLE persons;" +
"ALTER TABLE new_persons RENAME TO persons;")
}
While your migration code runs with disabled foreign key checks, those are re-enabled and checked at the end of the migration, regardless of eventual errors.
GRDB provides protocols and a Record class that help isolating database management code into database layer types, and avoid cluterring the rest of your application.
The Record class grants its subclasses with fetching methods, persistence methods, and changes tracking:
class Person : Record { ... }
try dbQueue.inDatabase { db in
// Store
let person = Person(name: "Arthur")
try person.save(db)
// Fetch
let person = Person.fetch(db, key: personId)
for person in Person.fetch(db, "SELECT * FROM persons") {
print(person.name)
}
// Changes tracking
person.name = "Barbara"
person.persistentChangedValues.keys // ["name"]
if person.hasPersistentChangedValues { // Avoid useless UPDATE statements
try person.save(db)
}
}
Whenever your application types can't inherit from Record, they can still adopt RowConvertible, which grants fetching methods, and DatabasePersistable, which grants persistence methods.
The RowConvertible
protocol grants fetching methods to any type that can be built from a database row:
public protocol RowConvertible {
/// Returns a value initialized from `row`.
static func fromRow(row: Row) -> Self
/// Optional method which gives adopting types an opportunity to complete
/// their initialization. Do not call it directly.
mutating func awakeFromFetch(row row: Row, database: Database)
}
Adopting types can be fetched just like rows:
struct PointOfInterest : RowConvertible {
var coordinate: CLLocationCoordinate2D
var title: String?
static func fromRow(row: Row) -> PointOfInterest {
return PointOfInterest(
coordinate: CLLocationCoordinate2DMake(
row.value(named: "latitude"),
row.value(named: "longitude")),
title: row.value(named: "title"))
}
}
PointOfInterest.fetch(db, "SELECT ...") // DatabaseSequence<PointOfInterest>
PointOfInterest.fetchAll(db, "SELECT ...") // [PointOfInterest]
PointOfInterest.fetchOne(db, "SELECT ...") // PointOfInterest?
See Column Values for more information about the row.value()
method.
Both fetch
and fetchAll
let you iterate the full list of fetched objects. The differences are:
- The array returned by
fetchAll
can take a lot of memory. Yet it can be iterated on any thread. - The sequence returned by
fetch
only goes to the database as you iterate it, and is thus more memory efficient. The price for this efficiency is that the sequence must be iterated in the database queue (you'll get a fatal error if you do otherwise). - The sequence returned by
fetch
will return a different set of results if the database has been modified between two sequence iterations.
☝️ Note: For performance reasons, the same row argument to
fromRow(:)
is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy:result.row = row.copy()
.
See also the Record class, which builds on top of RowConvertible and adds a few extra features like persistence methods, and changes tracking.
Adopt the DatabaseTableMapping
protocol on top of RowConvertible
:
public protocol DatabaseTableMapping {
/// The name of the database table
static func databaseTableName() -> String
}
For example:
// CREATE TABLE persons (
// id INTEGER PRIMARY KEY,
// name TEXT,
// email TEXT UNIQUE COLLATE NOCASE
// )
struct Person : RowConvertible, DatabaseTableMapping {
var id: Int64?
var name: String?
var email: String?
static func databaseTableName() -> String {
return "persons"
}
static func fromRow(row: Row) -> Person {
return Person(
id: row.value(named: "id"),
name: row.value(named: "name"),
email: row.value(named: "email"))
}
}
You are then granted with fetching methods based on database keys:
Person.fetch(db, keys: ...) // DatabaseSequence<Person>
Person.fetchAll(db, keys: ...) // [Person]
Person.fetchOne(db, key: ...) // Person?
Both fetch
and fetchAll
let you iterate the full list of fetched objects. The differences are:
- The array returned by
fetchAll
can take a lot of memory. Yet it can be iterated on any thread. - The sequence returned by
fetch
only goes to the database as you iterate it, and is thus more memory efficient. The price for this efficiency is that the sequence must be iterated in the database queue (you'll get a fatal error if you do otherwise). - The sequence returned by
fetch
will return a different set of results if the database has been modified between two sequence iterations.
The order of sequences and arrays returned by the key-based methods is undefined. To specify the order of returned elements, use a raw SQL query.
When the database table has a single column primary key, you can fetch given key values:
// SELECT * FROM persons WHERE id = 1
Person.fetchOne(db, key: 1)
// SELECT * FROM persons WHERE id IN (1,2,3)
Person.fetch(db, keys: [1,2,3])
// SELECT * FROM countries WHERE isoCode = 'FR'
Country.fetchOne(db, key: "FR")
// SELECT * FROM countries WHERE isoCode IN ('FR', 'ES', 'US')
Country.fetchAll(db, keys: ["FR", "ES", "US"])
For multi-column primary keys and secondary keys, use a key dictionary:
// SELECT * FROM persons WHERE email = 'me@example.com'
Person.fetchOne(db, key: ["email": "me@example.com"])
// SELECT * FROM citizenships WHERE personId = 1 AND countryIsoCode = 'FR'
Citizenship.fetchOne(db, key: ["personId": 1, "countryIsoCode": "FR"])
Your RowConvertible type can accept rows that do not always contain the same columns.
This allows you to load only a subset of a table columns, or to load extra columns joined from other tables. For example:
struct Person : RowConvertible {
let id: Int64?
let name: String
let bookCount: Int? // Only set when person is fetched from fetchAllWithBookCount()
static func fromRow(row: Row) -> Person {
return Person(
id: row.value(named: "id"),
name: row.value(named: "name"),
bookCount: row.value(named: "bookCount")) // nil if column is missing
}
// The returned persons have a value in their bookCount property:
static func fetchAllWithBookCount(db: Database) -> [Person] {
return fetchAll(db,
"SELECT persons.*, COUNT(books.id) AS bookCount " +
"FROM persons " +
"LEFT JOIN books ON books.ownerId = persons.id " +
"GROUP BY persons.id")
}
}
See Column Values for more information about the row.value()
method.
GRDB provides two protocols that let adopting types store themselves in the database:
public protocol MutableDatabasePersistable : DatabaseTableMapping {
/// The name of the database table (from DatabaseTableMapping)
static func databaseTableName() -> String
/// Returns the values that should be persisted in the database.
var persistentDictionary: [String: DatabaseValueConvertible?] { get }
/// Optional method that lets your adopting type store its rowID upon
/// successful insertion. Don't call it directly: it is called for you.
mutating func didInsertWithRowID(rowID: Int64, forColumn column: String?)
}
public protocol DatabasePersistable : MutableDatabasePersistable {
/// Non-mutating version of the optional didInsertWithRowID(:forColumn:)
func didInsertWithRowID(rowID: Int64, forColumn column: String?)
}
That's one more protocol that one could expect, and MutableDatabasePersistable
may sound intimidating.
Yet, here is the rule:
- If your type is a struct that mutates on insertion, choose
MutableDatabasePersistable
. For example, if your table has an INTEGER PRIMARY KEY, you want to store the inserted id on successful insertion. - Otherwise, stick with
DatabasePersistable
.
For example, the Country struct below has no INTEGER PRIMARY KEY, and is not mutated on insertion. On the other side, the Person struct is interested in its rowID, and mutates itself on insertion:
// CREATE TABLE countries (
// isoCode TEXT NOT NULL PRIMARY KEY,
// name TEXT NOT NULL
// )
struct Country : DatabasePersistable {
let isoCode: String
let name: String
static func databaseTableName() -> String {
return "countries"
}
var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["isoCode": isoCode, "name": name]
}
}
// Declare the country as `let`, since its insertion does not mutate it:
let country = Country(isoCode: "FR", name: "France")
try country.insert(db)
// CREATE TABLE persons (
// id INTEGER PRIMARY KEY,
// name TEXT
// )
struct Person : MutableDatabasePersistable {
let id: Int64?
let name: String?
static func databaseTableName() -> String {
return "persons"
}
var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["id": id, "name": name]
}
// Update person ID upon successful insertion:
mutating func didInsertWithRowID(rowID: Int64, forColumn column: String?) {
id = rowID
}
}
// Declare the person as `var`, since its insertion mutates it:
var person = Person(id: nil, name: "Arthur")
person.id // nil
try person.insert(db)
person.id // some value
The persistentDictionary
property returns a dictionary whose keys are column names, and values any DatabaseValueConvertible value (Bool, Int, String, NSDate, Swift enums, etc.) See Values for more information.
☝️ Note: Classes should always prefer adopting
DatabasePersistable
overMutableDatabasePersistable
, even if they mutate on insertion. This will prevent strange compiler errors when they insert an instance stored in alet
variable (see SR-142).
Types that adopt DatabasePersistable or MutableDatabasePersistable are given default implementations for methods that insert, update, and delete:
try object.insert(db) // INSERT
try object.update(db) // UPDATE
try object.save(db) // Inserts or updates
try object.delete(db) // DELETE
object.exists(db) // Bool
-
insert
,update
,save
anddelete
can throw a DatabaseError whenever an SQLite integrity check fails. -
update
can also throw a PersistenceError of type NotFound, should the update fail because there is no matching row in the database.When saving an object that may or may not already exist in the database, prefer the
save
method: it performs the UPDATE or INSERT statement that makes sure your values are saved in the database. -
delete
returns whether a database row was deleted or not.
The differences between DatabasePersistable
and MutableDatabasePersistable
only lie in insertion-related methods:
protocol MutableDatabasePersistable {
// Insertion can mutate:
mutating func didInsertWithRowID(rowID: Int64, forColumn column: String?)
mutating func insert(db: Database) throws
mutating func save(db: Database) throws
}
protocol DatabasePersistable : MutableDatabasePersistable {
// Insertion can not mutate:
func didInsertWithRowID(rowID: Int64, forColumn column: String?)
func insert(db: Database) throws
func save(db: Database) throws
}
Your custom type may want to perform extra work when the persistence methods are invoked.
For example, it may want to have its UUID automatically set before inserting. Or it may want to validate its values before saving.
The protocol exposes special methods for this exact purpose: performInsert
, performUpdate
, performSave
, performDelete
, and performExists
.
struct Person : MutableDatabasePersistable {
var uuid: String?
mutating func insert(db: Database) throws {
if uuid == nil {
uuid = NSUUID().UUIDString
}
try performInsert(db)
}
}
struct Link : DatabasePersistable {
var url: NSURL
func insert(db: Database) throws {
try validate()
try performInsert(db)
}
func update(db: Database) throws {
try validate()
try performUpdate(db)
}
func validate() throws {
if url.host == nil {
throw ValidationError("url must be absolute.")
}
}
}
☝️ Note: The special methods
performInsert
,performUpdate
, etc. are reserved for your custom implementations. Do not use them elsewhere. Do not provide another implementation for those methods.☝️ Note: It is recommended that you do not implement your own version of the
save
method. Its default implementation forwards the job toupdate
orinsert
: these are the methods that may need customization, notsave
.
Record is a class that wraps a table row or the result of any query, provides persistence methods, and changes tracking. It is designed to be subclassed.
// Define Record subclass
class Person : Record { ... }
try dbQueue.inDatabase { db in
// Store
let person = Person(name: "Arthur")
try person.save(db)
// Fetch
for person in Person.fetch(db, "SELECT * FROM persons") {
print(person.name)
}
}
It builds on top of the RowConvertible and DatabasePersistable protocols.
Record is not a smart class. It is no replacement for Core Data’s NSManagedObject, Realm’s Object, or for an Active Record pattern. It does not provide any uniquing, automatic refresh, or synthesized properties. It has no knowledge of external references and table relationships, and will not generate JOIN queries for you.
Yet, it does a few things well:
-
Records can be used from any thread. Not being a replacement for NSManagedObject comes with advantages.
-
It eats any SQL query. A Record subclass is often tied to a database table, but this is not a requirement at all.
let persons = Person.fetchAll(db, "SELECT persons.*, COUNT(citizenships.isoCode) AS citizenshipsCount " + "FROM persons " + "LEFT JOIN citizenships ON citizenships.personId = persons.id " + "GROUP BY persons.id") let person = persons.first! (person.name, person.citizenshipsCount)
-
It provides the classic CRUD persistence methods. All primary keys are supported (auto-incremented INTEGER PRIMARY KEY, single column, multiple columns).
let person = Person(...) try person.insert(db) // INSERT try person.update(db) // UPDATE try person.save(db) // Inserts or updates try person.delete(db) // DELETE
-
It tracks changes. Real changes: setting a column to the same value does not constitute a change.
person = Person.fetch... person.name = "Barbara" person.age = 41 person.persistentChangedValues.keys // ["age"] if person.hasPersistentChangedValues { try person.save(db) }
Record subclasses override the four core methods that define their relationship with the database:
class Record {
/// The table name
class func databaseTableName() -> String
/// Initialize a record from a database row
required init(_ row: Row)
/// The values persisted in the database
var persistentDictionary: [String: DatabaseValueConvertible?]
/// Optionally update record ID after a successful insertion
func didInsertWithRowID(rowID: Int64, forColumn column: String?)
}
Given an implementation of those core methods, you are granted with the full Record toolkit:
class Person {
// Copy
func copy() -> Self
// Change Tracking
var hasPersistentChangedValues: Bool
var persistentChangedValues: [String: DatabaseValue?]
// Persistence
func insert(db: Database) throws
func update(db: Database) throws
func save(db: Database) throws // inserts or updates
func delete(db: Database) throws -> Bool
func exists(db: Database) -> Bool
// Fetching
static func fetch(...) -> DatabaseSequence<Self>
static func fetchAll(...) -> [Self]
static func fetchOne(...) -> Self?
// Events
func awakeFromFetch(row row: Row, database: Database)
func didInsertWithRowID(rowID: Int64, forColumn column: String?)
// Description (from the CustomStringConvertible protocol)
var description: String
}
For example, given the following table:
CREATE TABLE countries (
isoCode TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL
)
The Country class freely defines its properties and initializers. Here we have chosen non optional properties that directly map database columns:
struct Country : Record {
let isoCode: String
let name: String
init(isoCode: String, name: String) {
self.isoCode = isoCode
self.name = name
super.init()
}
Country overrides databaseTableName()
to return the name of the table that should be used when fetching countries:
/// The table name
override class func databaseTableName() -> String {
return "countries"
}
Country overrides persistentDictionary
and returns a dictionary whose keys are column names, and values any DatabaseValueConvertible
value (Bool, Int, String, NSDate, Swift enums, etc.) See Values for more information:
/// The values persisted in the database
override var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["isoCode": isoCode, "name": name]
}
Country overrides init(row:)
so that it can be fetched:
/// Initialize a Country from a row
required init(_ row: Row) {
isoCode = row.value(named: "isoCode")
name = row.value(named: "name")
super.init(row)
}
}
See Column Values for more information about the row.value()
method.
☝️ Note: For performance reasons, the same row argument to
init(row:)
is reused for all records during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy:self.row = row.copy()
.
The Country class is now ready:
dbQueue.inDatabase { db in
let unitedStates = Country.fetchOne(db, key: "US")!
let country = Country(isoCode: "FR", name: "France")
country.insert(db)
}
When the database table has an INTEGER PRIMARY KEY, make sure to override the didInsertWithRowID
method so that your record stores its id upon successful insertion:
// CREATE TABLE persons (
// id INTEGER PRIMARY KEY,
// name TEXT
// )
class Person : Record {
let id: Int64?
let name: String?
init(name: String) {
self.id = nil
self.name = name
super.init()
}
// Record overrides
/// The table name
override class func databaseTableName() -> String {
return "persons"
}
/// The values persisted in the database
override var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["id": id, "name": name]
}
/// Initialize a Person from a row
required init(_ row: Row) {
id = row.value(named: "id")
name = row.value(named: "name")
super.init(row)
}
/// Update person ID after a successful insertion
func didInsertWithRowID(rowID: Int64, forColumn column: String?) {
id = rowID
}
}
let person = Person(...)
person.id // nil
try person.insert(db)
person.id // some value
You can fetch sequences, arrays, or single records with raw SQL queries, or by key:
dbQueue.inDatabase { db in
// SQL
Person.fetch(db, "SELECT ...", arguments:...) // DatabaseSequence<Person>
Person.fetchAll(db, "SELECT ...", arguments:...) // [Person]
Person.fetchOne(db, "SELECT ...", arguments:...) // Person?
// When database table has a single column primary key
Person.fetch(db, keys: [1,2,3]) // DatabaseSequence<Person>
Person.fetchAll(db, keys: [1,2,3]) // [Person]
Person.fetchOne(db, key: 1) // Person?
// For multi-column primary keys and secondary keys
Person.fetch(db, keys: [["name": "Joe"], ...]) // DatabaseSequence<Person>
Person.fetchAll(db, keys: [["name": "Joe"], ...]) // [Person]
Person.fetchOne(db, key: ["name": "Joe"]) // Person?
}
Both fetch
and fetchAll
let you iterate the full list of fetched records. The differences are:
- The array returned by
fetchAll
can take a lot of memory. Yet it can be iterated on any thread. - The sequence returned by
fetch
only goes to the database as you iterate it, and is thus more memory efficient. The price for this efficiency is that the sequence must be iterated in the database queue (you'll get a fatal error if you do otherwise). - The sequence returned by
fetch
will return a different set of results if the database has been modified between two sequence iterations.
For example:
dbQueue.inDatabase { db in
// All persons with an email ending in @example.com:
Person.fetch(db,
"SELECT * FROM persons WHERE email LIKE ?",
arguments: ["%@example.com"])
// All persons who have a single citizenship:
Person.fetch(db,
"SELECT persons.* " +
"FROM persons " +
"JOIN citizenships ON citizenships.personId = persons.id " +
"GROUP BY persons.id " +
"HAVING COUNT(citizenships.id) = 1")
// SELECT * FROM persons WHERE id = 1
Person.fetchOne(db, key: 1)
// SELECT * FROM persons WHERE id IN (1,2,3)
Person.fetch(db, keys: [1,2,3])
// SELECT * FROM countries WHERE isoCode = 'FR'
Country.fetchOne(db, key: "FR")
// SELECT * FROM countries WHERE isoCode IN ('FR', 'ES', 'US')
Country.fetchAll(db, keys: ["FR", "ES", "US"])
// SELECT * FROM persons WHERE email = 'me@example.com'
Person.fetchOne(db, key: ["email": "me@example.com"])
// SELECT * FROM citizenships WHERE personId = 1 AND countryIsoCode = 'FR'
Citizenship.fetchOne(db, key: ["personId": 1, "countryIsoCode": "FR"])
}
The order of sequences and arrays returned by the key-based methods is undefined. To specify the order of returned elements, use a raw SQL query.
Records can store themselves in the database through the persistentDictionary
core property:
class Person : Record {
/// The values stored in the database
override var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["id": id, "url": url, "name": name, "email": email]
}
}
try dbQueue.inDatabase { db in
let person = Person(...)
try person.insert(db) // INSERT
try person.update(db) // UPDATE
try person.save(db) // Inserts or updates
try person.delete(db) // DELETE
person.exists(db) // Bool
}
-
insert
automatically sets the primary key of record whose primary key is declared as "INTEGER PRIMARY KEY", if you override thedidInsertWithRowID
method:let person = Person() person.id // nil try person.insert(db) person.id // some value
Other primary keys (single or multiple columns) are not managed by GRDB: you have to manage them yourself. For example, you can override the
insert
primitive method, and generate an UUID before callingsuper.insert
. -
insert
,update
,save
anddelete
can throw a DatabaseError whenever an SQLite integrity check fails. -
update
can also throw a PersistenceError of type NotFound, should the update fail because the record does not exist in the database.When saving a record that may or may not already exist in the database, prefer the
save
method: it performs the UPDATE or INSERT statement that makes sure your values are saved in the database. -
delete
returns whether a database row was deleted or not.
The update()
method always executes an UPDATE statement. When the record has not been edited, this database access is generally useless.
Avoid it with the hasPersistentChangedValues
property, which returns whether the record has changes that have not been saved:
// Saves the person if it has changes that have not been saved:
if person.hasPersistentChangedValues {
try person.save(db)
}
Note that hasPersistentChangedValues
is based on value comparison: setting a property to the same value does not set the edited flag.
For an efficient algorithm which synchronizes the content of a database table with a JSON payload, check this sample code.
Don't hesitate deriving subclasses from your base records when you have a need for a specific query.
For example, if a view controller needs to display a list of persons along with the number of books they own, it would be unreasonable to fetch the list of persons, and then, for each of them, fetch the number of books they own. That would perform N+1 requests, and this is a well known issue.
Instead, subclass Person:
class PersonsViewController: UITableViewController {
// An ad-hoc Person subclass that fits the need of this view controller:
private class PersonWithBookCount : Person {
var bookCount: Int?
required init(_ row: Row) {
bookCount = row.value(named: "bookCount")
super.init(row)
}
}
Perform a single request:
var persons: [PersonWithBookCount]!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
persons = dbQueue.inDatabase { db in
PersonWithBookCount.fetchAll(db,
"SELECT persons.*, COUNT(books.id) AS bookCount " +
"FROM persons " +
"LEFT JOIN books ON books.ownerId = persons.id " +
"GROUP BY persons.id")
}
tableView.reloadData()
}
Other application objects that expect a Person will gently accept the private subclass:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showPerson" {
let personVC: PersonViewController = segue...
personVC.person = persons[tableView.indexPathForSelectedRow!.row]
}
}
}
Record does not provide any built-in validation.
You can use some external library such as GRValidation in the update() and insert() methods:
class Person : Record, Validable {
var name: String?
override func update(db: Database) throws {
// Validate before update
try validate()
try super.update(db)
}
override func insert(db: Database) throws {
// Validate before insert
try validate()
try super.insert(db)
}
func validate() throws {
// Name should not be nil
try validate(property: "name", with: name >>> ValidationNotNil())
}
}
// fatal error: 'try!' expression unexpectedly raised an error:
// Invalid <Person name:nil>: name should not be nil.
try! Person(name: nil).save(db)
Avoid default values in table declarations. Record doesn't know about them, and those default values won't be present in a record after it has been inserted.
For example, avoid the table below:
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
creationDate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Avoid
...
)
Instead, override insert()
and provide the default value there:
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
creationDate DATETIME NOT NULL, -- OK
...
)
class Person : Record {
var creationDate: NSDate?
override var persistentDictionary: [String: DatabaseValueConvertible?] {
return ["creationDate": creationDate, ...]
}
override func insert(db: Database) throws {
if creationDate == nil {
creationDate = NSDate()
}
try super.insert(db)
}
}
Record does not provide any API which executes a INSERT OR REPLACE query. Instead, consider adding an ON CONFLICT clause to your table definition, and let the simple insert() method perform the eventual replacement:
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE ON CONFLICT REPLACE,
...
)
let person = Person(name: "Arthur")
person.insert(db) // Replace any existing person named "Arthur"
The TransactionObserverType
protocol lets you observe database changes:
public protocol TransactionObserverType : class {
// Notifies a database change:
// - event.kind (insert, update, or delete)
// - event.tableName
// - event.rowID
func databaseDidChangeWithEvent(event: DatabaseEvent)
// An opportunity to rollback pending changes by throwing an error.
func databaseWillCommit() throws
// Database changes have been committed.
func databaseDidCommit(db: Database)
// Database changes have been rollbacked.
func databaseDidRollback(db: Database)
}
There is one transaction observer per database:
var config = Configuration()
config.transactionObserver = MyObserver()
let dbQueue = try DatabaseQueue(path: databasePath, configuration: config)
Protocol callbacks are all invoked on the database queue.
All database changes are notified to databaseDidChangeWithEvent, inserts, updates and deletes, including indirect ones triggered by ON DELETE and ON UPDATE actions associated to foreign keys.
Those changes are not actually applied until databaseDidCommit is called. On the other side, databaseDidRollback confirms their invalidation:
try dbQueue.inTransaction { db in
try db.execute("INSERT ...") // didChange
return .Commit // willCommit, didCommit
}
try dbQueue.inTransaction { db in
try db.execute("INSERT ...") // didChange
return .Rollback // didRollback
}
Database statements that are executed outside of an explicit transaction do not drop off the radar:
try dbQueue.inDatabase { db in
try db.execute("INSERT ...") // didChange, willCommit, didCommit
try db.execute("UPDATE ...") // didChange, willCommit, didCommit
}
Eventual errors thrown from databaseWillCommit are exposed to the application code:
do {
try dbQueue.inTransaction { db in
...
return .Commit // willCommit (throws), didRollback
}
} catch {
// The error thrown by the transaction observer.
}
☝️ Note: The databaseDidChangeWithEvent and databaseWillCommit callbacks must not touch the SQLite database. This limitation does not apply to databaseDidCommit and databaseDidRollback which can use their database argument.
Let's write an object that notifies, on the main thread, of modified database tables. Your view controllers can listen to those notifications and update their views accordingly.
/// The notification posted when database tables have changed:
let DatabaseTablesDidChangeNotification = "DatabaseTablesDidChangeNotification"
let ChangedTableNamesKey = "ChangedTableNames"
/// TableChangeObserver posts a DatabaseTablesDidChangeNotification on the main
/// thread after database tables have changed.
class TableChangeObserver : NSObject, TransactionObserverType {
private var changedTableNames: Set<String> = []
func databaseDidChangeWithEvent(event: DatabaseEvent) {
// Remember the name of the changed table:
changedTableNames.insert(event.tableName)
}
func databaseWillCommit() throws {
// Let go
}
func databaseDidCommit(db: Database) {
// Extract the names of changed tables, and reset until next
// database event:
let changedTableNames = self.changedTableNames
self.changedTableNames = []
// Notify
dispatch_async(dispatch_get_main_queue()) {
NSNotificationCenter.defaultCenter().postNotificationName(
DatabaseTablesDidChangeNotification,
object: self,
userInfo: [ChangedTableNamesKey: changedTableNames])
}
}
func databaseDidRollback(db: Database) {
// Reset until next database event:
changedTableNames = []
}
}
- The Documentation is full of GRDB snippets.
- GRDBDemoiOS: A sample iOS application.
- GRDBDemoiOS7: A sample iOS7 application.
- Check
GRDB.xcworkspace
: it contains GRDB-enabled playgrounds to play with. - How to synchronize a database table with a JSON payload: https://gist.github.com/groue/dcdd3784461747874f41
Thanks
- Pierlis, where we write great software.
- @pierlo for his feedback on GRDB.
- @aymerick and @kali because SQL.
- ccgus/fmdb for its excellency.