GRDB.swift is an SQLite toolkit for Swift 2.2, from the author of GRMustache.
It ships with a low-level database API, plus application-level tools.
December 3, 2015: GRDB.swift 0.32.2 is out - Release notes. Follow @groue on Twitter for release announcements and usage tips.
Requirements: iOS 7.0+ / OSX 10.9+, Xcode 7.2+
The last release that supports Swift 2.1 is version 0.32.2 on the Swift2.1 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. You won't lose a single feature or convenience by crafting custom SQL queries, on the contrary. Without losing type safety and all niceties you expect from a real Swift library.
GRDB is fast. As fast, when not faster, than FMDB and SQLite.swift.
- 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, and provides basic CRUD operations.
- 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 Changes Observation: A robust way to perform post-commit and post-rollback actions.
- RowConvertible Protocol: Turn database rows into handy types, without sacrificing performance.
- Records: CRUD operations and changes tracking.
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.32.2'
Carthage is another dependency manager for Xcode projects.
To use GRDB.swift with Carthage, specify in your Cartfile:
github "groue/GRDB.swift" == 0.32.2
The Swift Package Manager is the open source tool for managing the distribution of Swift code.
To use GRDB.swift with the Swift Package Manager, add https://github.com/groue/GRDB.swift to the list of your package dependencies:
import PackageDescription
let package = Package(
name: "MyPackage",
targets: [],
dependencies: [
.Package(url: "https://github.com/groue/GRDB.swift", majorVersion: 1, minor: 0),
]
)
- Download a copy of GRDB.swift.
- Embed the
Xcode/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:
let bookCount: Int = row.value(named: "bookCount")
let bookCount64: Int64 = row.value(named: "bookCount")
let hasBooks: Bool = row.value(named: "bookCount") // false when 0
Did I say the type you ask for? See Values for more information on supported value types:
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!
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.
row.value(named: "foo") as String // "bar" row.value(named: "foo") as NSDate? // nil: can't convert "bar" 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.
-
The convenience conversions of SQLite, such as Blob to String or String to Integer 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?]
}
For example:
dbQueue.inDatabase { db in
// The number of persons with an email ending in @domain.com:
let count = Int.fetchOne(db,
"SELECT COUNT(*) FROM persons WHERE email LIKE ?",
arguments: ["%@domain.com"])!
// The emails of people who own at least two pets:
let emails = String.fetch(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: Bool, Double, Int, Int32, Int64, String, Swift enums.
-
Foundation: NSCoding, 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 as Record properties:
class Link : Record {
var url: NSURL?
var verified: Bool
...
}
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.
NSCoding is indirectly supported, through the DatabaseCoder helper type.
DatabaseCoder reads and writes objects that adopt NSCoding into Blob columns.
For example, store NSArray into the database:
let ints = [1, 2, 3]
try db.execute(
"INSERT INTO ... (ints, ...) VALUES (?, ...)",
arguments: [DatabaseCoder(ints), ...])
Extract NSArray from the database:
let row = Row.fetchOne(db, "SELECT ints ...")!
let coder = row.value(named: "ints") as DatabaseCoder // DatabaseCoder
let array = coder.object as! NSArray // NSArray
let ints = array.map { $0 as! Int } // [Int]
DatabaseCoder.fetch(db, "SELECT ...") // DatabaseSequence<DatabaseCoder>
DatabaseCoder.fetchAll(db, "SELECT ...") // [DatabaseCoder]
DatabaseCoder.fetchOne(db, "SELECT ...") // DatabaseCoder?
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 an instance 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.
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 {
return DatabaseValue(double: date.timeIntervalSince1970)
}
/// Returns an instance 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 let 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.
Added functions can be removed:
dbQueue.inDatabase { db in
let fn = DatabaseFunction(...)
db.addFunction(fn)
...
db.removeFunction(fn)
}
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 Changes Observation: A robust way to perform post-commit and post-rollback actions.
- RowConvertible Protocol: Turn database rows into handy types, without sacrificing performance.
- Records: CRUD operations and changes tracking.
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, and is supported by a specific method of DatabaseMigrator:
// Add a NOT NULL constraint on persons.name:
migrator.registerMigrationWithoutForeignKeyChecks("AddNotNullCheckOnName") { 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.
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:
self.changedTableNames = []
}
}
The RowConvertible
protocol grants fetching methods to any type that can be initialized from a database row:
public protocol RowConvertible {
/// Create an instance initialized with `row`.
init(row: Row)
/// Optional method which gives adopting types an opportunity to complete
/// their initialization. Do not call it directly.
mutating func awakeFromFetch(row: Row)
}
Adopting types can be fetched just like rows:
struct PointOfInterest : RowConvertible {
var coordinate: CLLocationCoordinate2D
var title: String?
init(row: Row) {
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?
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
init(row:)
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:self.row = row.copy()
.
You also get a dictionary initializer for free:
PointOfInterest(dictionary: [
"latitude": 41.8919300,
"longitude": 12.5113300,
"title": "Rome"])
See also the Record class, which builds on top of RowConvertible and adds a few extra features like CRUD operations, and changes tracking.
- Overview
- Subclassing Record
- Fetching Records
- Insert, Update and Delete
- Record Initializers
- Changes Tracking
- Advice
Record is a class that wraps a table row or the result of any query, provides CRUD operations, 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)
}
}
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 operations. All primary keys are supported (auto-incremented INTEGER PRIMARY KEY, single column, multiple columns).
let person = Person(...) try person.insert(db) // Automatically fills person.id for INTEGER PRIMARY KEY. try person.update(db) try person.save(db) // Inserts or updates try person.reload(db) try person.delete(db)
-
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.databaseChanges.keys // ["age"] if person.databaseEdited { try person.save(db) }
Record subclasses override the three core methods that define their relationship with the database:
class Record {
/// The table name
class func databaseTableName() -> String?
/// The values stored in the database
var storedDatabaseDictionary: [String: DatabaseValueConvertible?]
/// Update the record from a database row
func updateFromRow(row: Row)
}
Given an implementation of those three core methods, you are granted with the full Record toolkit:
class Person {
// Initializers
init()
init(row: Row)
convenience init(dictionary: [String: DatabaseValueConvertible?])
convenience init?(dictionary: NSDictionary)
func copy() -> Self
// Change Tracking
var databaseEdited: Bool
var databaseChanges: [String: (old: DatabaseValue?, new: DatabaseValue)]
// CRUD
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 reload(db: Database) throws
func exists(db: Database) -> Bool
// Fetching
static func fetch(...) -> DatabaseSequence<Self>
static func fetchAll(...) -> [Self]
static func fetchOne(...) -> Self?
// Events
func awakeFromFetch(row: Row)
// Description (from the CustomStringConvertible protocol)
var description: String
}
For example, given the following table:
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
url TEXT,
name TEXT,
email TEXT UNIQUE COLLATE NOCASE
)
The Person class freely defines its properties. Here we have chosen optional types that directly map database columns, but you are free to use non-optional types or compound types like CLLocationCoordinate2D.
class Person : Record {
var id: Int64? // Int64 is the preferred type for auto-incremented IDs.
var url: NSURL?
var name: String?
var email: String?
Person overrides databaseTableName()
to return the name of the table that should be used when fetching persons:
/// The table name
override class func databaseTableName() -> String? {
return "persons"
}
Person overrides storedDatabaseDictionary
to return the dictionary of values that should be stored in the database when a person is saved. DatabaseValueConvertible
is the protocol adopted by all supported values (Bool, Int, String, NSDate, Swift enums, etc.) See Values for more information:
/// The values stored in the database
override var storedDatabaseDictionary: [String: DatabaseValueConvertible?] {
return [
"id": id,
"url": url,
"name": name,
"email": email]
}
Person overrides updateFromRow()
to update its properties from the columns found in a database row. See Rows as Dictionaries for more information about the DatabaseValue
type of the dbv
variable, and Values about the supported value types:
/// Update the record from a database row
override func updateFromRow(row: Row) {
if let dbv = row["id"] { id = dbv.value() }
if let dbv = row["url"] { url = dbv.value() }
if let dbv = row["name"] { name = dbv.value() }
if let dbv = row["email"] { email = dbv.value() }
super.updateFromRow(row) // Subclasses are required to call super.
}
}
☝️ Note: The
updateFromRow
method MUST NOT assume the presence of particular columns. The Record class itself reserves the right to callupdateFromRow
with arbitrary columns. The following implementation is thus discouraged:// BAD: this implementation will eventually crash // with "No such column" errors: override func updateFromRow(row: Row) { id = row.value(named: "id") url = row.value(named: "url") name = row.value(named: "name") email = row.value(named: "email") super.updateFromRow(row) }☝️ Note: For performance reasons, the same row argument to
updateFromRow
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()
.
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 @domain.com:
Person.fetch(db,
"SELECT * FROM persons WHERE email LIKE ?",
arguments: ["%@domain.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@domain.com'
Person.fetchOne(db, key: ["email": "me@domain.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 storedDatabaseDictionary
core property:
class Person : Record {
// The values stored in the database
override var storedDatabaseDictionary: [String: DatabaseValueConvertible?] {
return ["id": id, "url": url, "name": name, "email": email]
}
}
try dbQueue.inDatabase { db in
let person = Person(...)
try person.insert(db) // Automatically fills person.id for INTEGER PRIMARY KEY.
try person.update(db)
try person.save(db) // Inserts or updates
try person.reload(db)
try person.delete(db)
person.exists(db) // Bool
}
-
insert
automatically sets the primary key of record whose primary key is declared as "INTEGER PRIMARY KEY".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
andreload
methods can also throw a RecordError of type RecordNotFound, should the update or reload 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.
Record has four initializers:
class Record {
// Designated initializers:
init()
required init(row: Row)
// Convenience initializers:
convenience init(dictionary: [String: DatabaseValueConvertible?])
convenience init?(dictionary: NSDictionary)
}
Whenever you add your own custom initializer, Swift requires you to call one of the designated initializers of your Record superclass, and to provide an implementation of the required init(row:)
:
class Person : Record {
var id: Int64?
var age: Int?
var name: String?
// Person(name: "Arthur", age: 41)
init(id: Int64? = nil, name: String?, age: Int?) {
self.id = id
self.age = age
self.name = name
// Required by Swift
super.init()
}
// Required by Swift
required init(row: Row) {
super.init(row: row)
}
}
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 databaseEdited
property, which returns whether the record has changes that have not been saved:
let json = try! NSJSONSerialization.JSONObjectWithData(...) as! NSDictionary
// Fetches or create a new person given its ID:
let id = json["id"] as! Int?
let person: Person
if let existingPerson = Person.fetchOne(db, key: id) {
person = existingPerson
} else {
person = Person()
}
// Apply JSON payload (assuming json keys are column names)
let row = Row(dictionary: json)!
person.updateFromRow(row)
// Saves the person if it has changes that have not been saved:
if person.databaseEdited {
try person.save(db) // inserts or updates
}
Note that databaseEdited
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.
For "autoincremented" ids, declare your id column as INTEGER PRIMARY KEY:
CREATE TABLE persons {
id INTEGER PRIMARY KEY,
...
}
class Person : Record {
id: Int64?
/// The table definition.
override class func databaseTableName() -> String? {
return "persons"
}
/// The values that should be stored in the database.
override var storedDatabaseDictionary: [String: DatabaseValueConvertible?] {
return ["id": id, ...]
}
/// Updates `self` with a database value.
override func updateFromRow(row: Row) {
if let dbv = row["id"] { id = dbv.value() }
...
super.updateFromRow(row) // Subclasses are required to call super.
}
}
let person = Person(...)
person.id // nil
try person.insert(db)
person.id // some value
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 {
private class PersonWithBookCount : Person {
var bookCount: Int?
override func updateFromRow(row: Row) {
if dbv = row["bookCount"] { bookCount = dbv.value() }
super.updateFromRow(row) // Let Person superclass finish the job.
}
}
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[self.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 storedDatabaseDictionary: [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 Documentation is full of GRDB snippets.
- GRDBDemoiOS: A sample iOS application.
- GRDBDemoiOS7: A sample iOS7 application.
- Check
Xcode/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.