GRDB provides two classes for accessing SQLite databases: DatabaseQueue and DatabasePool.
Opening Connections
DatabaseQueue
DatabasePool
import GRDB
// Open a database file
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
// Or create an in-memory database
let dbQueue = try DatabaseQueue()
import GRDB
// Open a database file
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
DatabasePool cannot create in-memory databases. Use DatabaseQueue for in-memory databases.
DatabaseQueue vs DatabasePool
Choose the right connection type for your needs:
| Feature | DatabaseQueue | DatabasePool |
|---|
| Concurrent reads | ❌ No | ✅ Yes |
| WAL mode | Optional | Always (unless read-only) |
| In-memory databases | ✅ Yes | ❌ No |
| Thread safety | ✅ Yes | ✅ Yes |
| Memory usage | Lower | Higher |
If you are not sure, choose DatabaseQueue. You can always switch to DatabasePool later when you need concurrent read access.
DatabaseQueue
DatabaseQueue provides a single serialized database connection:
public final class DatabaseQueue {
public init(path: String, configuration: Configuration = Configuration()) throws
public init(named name: String? = nil, configuration: Configuration = Configuration()) throws
}
Reading from DatabaseQueue
try dbQueue.read { db in
let count = try Player.fetchCount(db)
let players = try Player.fetchAll(db)
return players
}
Writing to DatabaseQueue
try dbQueue.write { db in
try Player(name: "Arthur", score: 100).insert(db)
try Player(name: "Barbara", score: 1000).insert(db)
}
In-Memory Databases
DatabaseQueue supports in-memory databases:
// Each connection gets its own independent database
let dbQueue = try DatabaseQueue()
DatabasePool
DatabasePool manages a pool of database connections for concurrent reads:
public final class DatabasePool {
public init(path: String, configuration: Configuration = Configuration()) throws
}
Key Features
- Concurrent Reads: Multiple threads can read simultaneously
- WAL Mode: Automatically enables Write-Ahead Logging
- Single Writer: Writes are serialized
- Snapshot Isolation: Readers see a consistent snapshot
Reading from DatabasePool
// Concurrent reads are isolated from writes
try dbPool.read { db in
let players = try Player.fetchAll(db)
return players
}
Writing to DatabasePool
Writes are serialized through a single writer connection:
try dbPool.write { db in
try Player(name: "Arthur", score: 100).insert(db)
}
Async Concurrent Reads
DatabasePool provides snapshot isolation for concurrent reads:
try dbPool.writeWithoutTransaction { db in
try Player.deleteAll(db)
// Read concurrently with guaranteed snapshot isolation
dbPool.asyncConcurrentRead { dbResult in
do {
let db = try dbResult.get()
// Guaranteed to see zero players
let count = try Player.fetchCount(db)
} catch {
// Handle error
}
}
try Player(name: "Arthur", score: 100).insert(db)
}
Configuration Options
Both connection types accept a Configuration object:
var config = Configuration()
config.readonly = true
config.foreignKeysEnabled = true
config.label = "MyDatabase"
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
Common Configuration Options
Read-Only
Foreign Keys
Prepare Database
var config = Configuration()
config.readonly = true
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
var config = Configuration()
config.foreignKeysEnabled = true
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
var config = Configuration()
config.prepareDatabase { db in
// Called when each connection opens
try db.execute(sql: "PRAGMA journal_size_limit = 10000000")
}
let dbPool = try DatabasePool(path: dbPath, configuration: config)
Memory Management
Both connection types provide memory management methods:
// Release as much memory as possible (blocks current thread)
dbQueue.releaseMemory()
dbPool.releaseMemory()
// DatabasePool also provides an async version
dbPool.releaseMemoryEventually()
On iOS, GRDB automatically releases memory when the app receives memory warnings or enters the background.
Closing Connections
Connections are automatically closed when deallocated, but you can explicitly close them:
try dbQueue.close()
try dbPool.close()
After closing, the connection cannot be reopened. Any database access will throw an error.
Thread Safety
Both DatabaseQueue and DatabasePool are thread-safe:
- DatabaseQueue: Serializes all database access on a single dispatch queue
- DatabasePool: Allows concurrent reads on multiple connections, writes on a single connection
// Safe to call from any thread
DispatchQueue.global().async {
try? dbQueue.read { db in
// Read from database
}
}
DispatchQueue.global().async {
try? dbQueue.write { db in
// Write to database
}
}
Best Practices
Choose Queue First
Singleton Pattern
Error Handling
Start with DatabaseQueue for simplicity, then upgrade to DatabasePool if you need concurrent reads.
final class AppDatabase {
static let shared = AppDatabase()
let dbQueue: DatabaseQueue
private init() {
let dbPath = try! FileManager.default
.url(for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
.appendingPathComponent("db.sqlite")
.path
dbQueue = try! DatabaseQueue(path: dbPath)
}
}
enum DatabaseError: Error {
case connectionFailed
case queryFailed(Error)
}
func openDatabase() throws -> DatabaseQueue {
do {
return try DatabaseQueue(path: dbPath)
} catch {
throw DatabaseError.connectionFailed
}
}
Next Steps