一个 SQLite 数据库工具包,专注于应用程序开发
自 2015 年以来,自豪地为社区服务
最新版本:2025 年 2 月 12 日 • 7.2.0 版本 • 更新日志 • 从 GRDB 6 迁移到 GRDB 7
要求:iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ • SQLite 3.20.0+ • Swift 6+ / Xcode 16+
联系方式:
使用此库将应用程序的永久数据保存到 SQLite 数据库中。 它带有内置工具,可满足常见需求
SQL 生成
使用持久性和获取方法增强您的应用程序模型,这样您就不必在不想处理 SQL 和原始数据库行时处理它们。
数据库观察
当数据库值被修改时获取通知。
强大的并发性
多线程应用程序可以有效地使用其数据库,包括支持并发读写的 WAL 数据库。
迁移
随着应用程序新版本的发布,不断发展数据库的 schema。
利用您的 SQLite 技能
并非所有开发人员都需要高级 SQLite 功能。 但是当您需要时,GRDB 会像您希望的那样敏锐。 带着您的 SQL 和 SQLite 技能来,或者随着您的进步学习新的技能!
import GRDB
// 1. Open a database connection
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
// 2. Define the database schema
try dbQueue.write { db in
try db.create(table: "player") { t in
t.primaryKey("id", .text)
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
// 3. Define a record type
struct Player: Codable, FetchableRecord, PersistableRecord {
var id: String
var name: String
var score: Int
}
// 4. Write and read in the database
try dbQueue.write { db in
try Player(id: "1", name: "Arthur", score: 100).insert(db)
try Player(id: "2", name: "Barbara", score: 1000).insert(db)
}
let players: [Player] = try dbQueue.read { db in
try Player.fetchAll(db)
}
try dbQueue.write { db in
try db.execute(sql: """
CREATE TABLE place (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
favorite BOOLEAN NOT NULL DEFAULT 0,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL)
""")
try db.execute(sql: """
INSERT INTO place (title, favorite, latitude, longitude)
VALUES (?, ?, ?, ?)
""", arguments: ["Paris", true, 48.85341, 2.3488])
let parisId = db.lastInsertedRowID
// Avoid SQL injection with SQL interpolation
try db.execute(literal: """
INSERT INTO place (title, favorite, latitude, longitude)
VALUES (\("King's Cross"), \(true), \(51.52151), \(-0.12763))
""")
}
请参阅 执行更新
try dbQueue.read { db in
// Fetch database rows
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM place")
while let row = try rows.next() {
let title: String = row["title"]
let isFavorite: Bool = row["favorite"]
let coordinate = CLLocationCoordinate2D(
latitude: row["latitude"],
longitude: row["longitude"])
}
// Fetch values
let placeCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")! // Int
let placeTitles = try String.fetchAll(db, sql: "SELECT title FROM place") // [String]
}
let placeCount = try dbQueue.read { db in
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")!
}
请参阅 获取查询
struct Place {
var id: Int64?
var title: String
var isFavorite: Bool
var coordinate: CLLocationCoordinate2D
}
// snip: turn Place into a "record" by adopting the protocols that
// provide fetching and persistence methods.
try dbQueue.write { db in
// Create database table
try db.create(table: "place") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
t.column("favorite", .boolean).notNull().defaults(to: false)
t.column("longitude", .double).notNull()
t.column("latitude", .double).notNull()
}
var berlin = Place(
id: nil,
title: "Berlin",
isFavorite: false,
coordinate: CLLocationCoordinate2D(latitude: 52.52437, longitude: 13.41053))
try berlin.insert(db)
berlin.id // some value
berlin.isFavorite = true
try berlin.update(db)
}
请参阅 记录
try dbQueue.read { db in
// Place
let paris = try Place.find(db, id: 1)
// Place?
let berlin = try Place.filter(Column("title") == "Berlin").fetchOne(db)
// [Place]
let favoritePlaces = try Place
.filter(Column("favorite") == true)
.order(Column("title"))
.fetchAll(db)
// Int
let favoriteCount = try Place.filter(Column("favorite")).fetchCount(db)
// SQL is always welcome
let places = try Place.fetchAll(db, sql: "SELECT * FROM place")
}
请参阅 查询接口
// Define the observed value
let observation = ValueObservation.tracking { db in
try Place.fetchAll(db)
}
// Start observation
let cancellable = observation.start(
in: dbQueue,
onError: { error in ... },
onChange: { (places: [Place]) in print("Fresh places: \(places)") })
现成的对 Combine 和 RxSwift 的支持
// Combine
let cancellable = observation.publisher(in: dbQueue).sink(
receiveCompletion: { completion in ... },
receiveValue: { (places: [Place]) in print("Fresh places: \(places)") })
// RxSwift
let disposable = observation.rx.observe(in: dbQueue).subscribe(
onNext: { (places: [Place]) in print("Fresh places: \(places)") },
onError: { error in ... })
请参阅 数据库观察, Combine 支持, RxGRDB。
GRDB 在 SQLite 之上运行:您应该熟悉 SQLite 常见问题解答。 有关一般和详细信息,请跳转到 SQLite 文档。
以下安装过程使 GRDB 使用目标操作系统附带的 SQLite 版本。
有关使用 SQLCipher 的 GRDB 安装过程,请参阅 加密。
有关使用 SQLite 的自定义构建的 GRDB 安装过程,请参阅 自定义 SQLite 构建。
Swift Package Manager 自动执行 Swift 代码的分发。 要将 GRDB 与 SPM 一起使用,请添加对 https://github.com/groue/GRDB.swift.git
的依赖
GRDB 提供两个库,GRDB
和 GRDB-dynamic
。 只选择一个。 如有疑问,请首选 GRDB
。 如果您要将其与应用程序中的多个目标链接,并且只想链接到共享的动态框架一次,则 GRDB-dynamic
库可以显示出有用。 有关更多信息,请参见如何将 Swift Package 链接为动态库 。
注意:目前不支持 Linux。
CocoaPods是 Xcode 项目的依赖项管理器。 要将 GRDB 与 CocoaPods(1.2 或更高版本)一起使用,请在您的 Podfile
中指定
pod 'GRDB.swift'
GRDB 可以作为框架或静态库安装。
CocoaPods 安装的重要提示
由于 CocoaPods 中的一个问题,目前无法将新版本的 GRDB 部署到 CocoaPods。 CocoaPods 上提供的最新版本是 6.24.1。 要使用 CocoaPods 安装更高版本的 GRDB,请使用以下解决方法之一
依赖于 GRDB6
分支。 这或多或少等效于 pod 'GRDB.swift', '~> 6.0'
通常会执行的操作,如果 CocoaPods 接受发布新的 GRDB 版本
# Can't use semantic versioning due to https://github.com/CocoaPods/CocoaPods/issues/11839
pod 'GRDB.swift', git: 'https://github.com/groue/GRDB.swift.git', branch: 'GRDB6'
显式依赖于特定版本(用您要使用的版本替换标签)
# Can't use semantic versioning due to https://github.com/CocoaPods/CocoaPods/issues/11839
# Replace the tag with the tag that you want to use.
pod 'GRDB.swift', git: 'https://github.com/groue/GRDB.swift.git', tag: 'v6.29.0'
Carthage 不受支持。 有关此决定的上下文,请参见 #433。
下载 GRDB 的副本,或者克隆其存储库并确保您检出最新的标记版本。
将 GRDB.xcodeproj
项目嵌入到您自己的项目中。
在应用程序目标(WatchOS 的扩展目标)的“构建阶段”选项卡的“目标依赖项”部分中添加 GRDB
目标。
将 GRDB.framework
添加到应用程序目标(WatchOS 的扩展目标)的“常规”选项卡的“嵌入式二进制文件”部分。
GRDB 提供了两个用于访问 SQLite 数据库的类:DatabaseQueue
和 DatabasePool
import GRDB
// Pick one:
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
区别在于
如果您不确定,请选择 DatabaseQueue
。 您始终可以稍后切换到 DatabasePool
。
有关打开连接的更多信息和提示,请参见 数据库连接。
在本节文档中,我们将讨论 SQL。 如果 SQL 不是您的菜,请跳转到 查询接口。
高级主题
一旦获得数据库连接,execute(sql:arguments:)
方法就会执行不返回任何数据库行的 SQL 语句,例如 CREATE TABLE
、INSERT
、DELETE
、ALTER
等。
例如
try dbQueue.write { db in
try db.execute(sql: """
CREATE TABLE player (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INT)
""")
try db.execute(
sql: "INSERT INTO player (name, score) VALUES (?, ?)",
arguments: ["Barbara", 1000])
try db.execute(
sql: "UPDATE player SET score = :score WHERE id = :id",
arguments: ["score": 1000, "id": 1])
}
}
SQL 查询中的 ?
和冒号前缀的键 (如 :score
) 是**语句参数**。 可以使用数组或字典传递参数,如上例所示。 有关支持的参数类型(Bool、Int、String、Date、Swift 枚举等)的更多信息,请参见值;有关 SQLite 参数的详细文档,请参见 StatementArguments
。
你也可以使用 execute(literal:)
将查询参数直接嵌入到 SQL 查询中,如下例所示。 有关更多详细信息,请参见 SQL 插值。
try dbQueue.write { db in
let name = "O'Brien"
let score = 550
try db.execute(literal: """
INSERT INTO player (name, score) VALUES (\(name), \(score))
""")
}
永远不要将值直接嵌入到原始 SQL 字符串中。 有关更多信息,请参见避免 SQL 注入
// WRONG: don't embed values in raw SQL strings
let id = 123
let name = textField.text
try db.execute(
sql: "UPDATE player SET name = '\(name)' WHERE id = \(id)")
// CORRECT: use arguments dictionary
try db.execute(
sql: "UPDATE player SET name = :name WHERE id = :id",
arguments: ["name": name, "id": id])
// CORRECT: use arguments array
try db.execute(
sql: "UPDATE player SET name = ? WHERE id = ?",
arguments: [name, id])
// CORRECT: use SQL Interpolation
try db.execute(
literal: "UPDATE player SET name = \(name) WHERE id = \(id)")
用分号连接多个语句:
try db.execute(sql: """
INSERT INTO player (name, score) VALUES (?, ?);
INSERT INTO player (name, score) VALUES (?, ?);
""", arguments: ["Arthur", 750, "Barbara", 1000])
try db.execute(literal: """
INSERT INTO player (name, score) VALUES (\("Arthur"), \(750));
INSERT INTO player (name, score) VALUES (\("Barbara"), \(1000));
""")
如果要确保执行单个语句,请使用预处理的 Statement
。
在 INSERT 语句之后,可以使用 lastInsertedRowID
获取插入行的行 ID
try db.execute(
sql: "INSERT INTO player (name, score) VALUES (?, ?)",
arguments: ["Arthur", 1000])
let playerId = db.lastInsertedRowID
不要错过 记录,它提供了经典的 持久化方法
var player = Player(name: "Arthur", score: 1000)
try player.insert(db)
let playerId = player.id
数据库连接 可以让你获取数据库行、纯值和自定义模型(也称为“记录”)。
行是 SQL 查询的原始结果
try dbQueue.read { db in
if let row = try Row.fetchOne(db, sql: "SELECT * FROM wine WHERE id = ?", arguments: [1]) {
let name: String = row["name"]
let color: Color = row["color"]
print(name, color)
}
}
值是存储在行中的列中的 Bool、Int、String、Date、Swift 枚举等
try dbQueue.read { db in
let urls = try URL.fetchCursor(db, sql: "SELECT url FROM wine")
while let url = try urls.next() {
print(url)
}
}
记录是可以从行初始化自身的应用程序对象
let wines = try dbQueue.read { db in
try Wine.fetchAll(db, sql: "SELECT * FROM wine")
}
在整个 GRDB 中,您可以始终获取任何可获取类型(数据库行、简单值或自定义记录)的游标、数组、集合或单个值。
try Row.fetchCursor(...) // A Cursor of Row
try Row.fetchAll(...) // [Row]
try Row.fetchSet(...) // Set<Row>
try Row.fetchOne(...) // Row?
fetchCursor
返回一个在获取的值上的 游标
let rows = try Row.fetchCursor(db, sql: "SELECT ...") // A Cursor of Row
fetchAll
返回一个 数组
let players = try Player.fetchAll(db, sql: "SELECT ...") // [Player]
fetchSet
返回一个 集合
let names = try String.fetchSet(db, sql: "SELECT ...") // Set<String>
fetchOne
返回一个 单个可选值,并消耗单个数据库行(如果有)。
let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) ...") // Int?
所有这些获取方法都需要一个包含单个 SQL 语句的 SQL 字符串。 如果要从用分号连接的多个语句中获取数据,请迭代 SQL 字符串中找到的多个 预处理语句。
📖 Cursor
每当从数据库中使用多个行时,可以获取数组、集合或游标.
fetchAll()
和 fetchSet()
方法返回常规的 Swift 数组和集合,可以像迭代所有其他数组和集合一样迭代它们
try dbQueue.read { db in
// [Player]
let players = try Player.fetchAll(db, sql: "SELECT ...")
for player in players {
// use player
}
}
与数组和集合不同,fetchCursor()
返回的游标会逐步加载其结果
try dbQueue.read { db in
// Cursor of Player
let players = try Player.fetchCursor(db, sql: "SELECT ...")
while let player = try players.next() {
// use player
}
}
游标不能在任何线程上使用:必须在其创建的调度队列中使用游标。 特别是,不要从数据库访问方法中提取游标
// Wrong
let cursor = try dbQueue.read { db in
try Player.fetchCursor(db, ...)
}
while let player = try cursor.next() { ... }
相反,数组和集合可以在任何线程上使用
// OK
let array = try dbQueue.read { db in
try Player.fetchAll(db, ...)
}
for player in array { ... }
游标只能迭代一次。 数组和集合可以迭代多次。
游标以延迟方式迭代数据库结果,并且不占用太多内存。 数组和集合包含数据库值的副本,并且当有很多获取的结果时,可能会占用大量内存。
与数组和集合必须花费时间复制数据库值不同,游标可以直接访问 SQLite。 如果追求更高的性能,则可能更喜欢游标。
游标可以为 Swift 集合提供数据。
大多数时候,你会在想要数组或集合时使用 fetchAll
或 fetchSet
。 对于更具体的需求,您可能更喜欢下面的初始化程序之一。 它们都接受一个额外的可选 minimumCapacity
参数,当您了解游标中的元素数量时,该参数有助于优化你的应用程序(内置的 fetchAll
和 fetchSet
不执行这种优化)。
数组和所有符合 RangeReplaceableCollection
的类型
// [String]
let cursor = try String.fetchCursor(db, ...)
let array = try Array(cursor)
集合:
// Set<Int>
let cursor = try Int.fetchCursor(db, ...)
let set = try Set(cursor)
字典:
// [Int64: [Player]]
let cursor = try Player.fetchCursor(db)
let dictionary = try Dictionary(grouping: cursor, by: { $0.teamID })
// [Int64: Player]
let cursor = try Player.fetchCursor(db).map { ($0.id, $0) }
let dictionary = try Dictionary(uniqueKeysWithValues: cursor)
游标采用 Cursor 协议,该协议非常类似于 Swift 的标准 lazy sequences。 因此,游标附带了许多便捷方法:compactMap
、contains
、dropFirst
、dropLast
、drop(while:)
、enumerated
、filter
、first
、flatMap
、forEach
、joined
、joined(separator:)
、max
、max(by:)
、min
、min(by:)
、map
、prefix
、prefix(while:)
、reduce
、reduce(into:)
、suffix
// Prints all Github links
try URL
.fetchCursor(db, sql: "SELECT url FROM link")
.filter { url in url.host == "github.com" }
.forEach { url in print(url) }
// An efficient cursor of coordinates:
let locations = try Row.
.fetchCursor(db, sql: "SELECT latitude, longitude FROM place")
.map { row in
CLLocationCoordinate2D(latitude: row[0], longitude: row[1])
}
游标不是 Swift 序列。 这是因为 Swift 序列无法处理迭代错误,而读取 SQLite 结果可能随时失败。
游标需要小心使用:
不要在游标迭代期间修改结果
// Undefined behavior
while let player = try players.next() {
try db.execute(sql: "DELETE ...")
}
不要将 Row
游标转换为数组或集合。 你将无法获得你期望的不同的行。 要获取行数组,请使用 Row.fetchAll(...)
。 要获取行集合,请使用 Row.fetchSet(...)
。 一般来说,请确保在每次从游标中提取行以供以后使用时复制行:row.copy()
。
如果你看不到或不关心差异,请使用数组。 如果你关心内存和性能,请在适当的时候使用游标。
获取行的 游标 、 数组 、 集合 或 单个 行(请参见 获取方法)
try dbQueue.read { db in
try Row.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Row
try Row.fetchAll(db, sql: "SELECT ...", arguments: ...) // [Row]
try Row.fetchSet(db, sql: "SELECT ...", arguments: ...) // Set<Row>
try Row.fetchOne(db, sql: "SELECT ...", arguments: ...) // Row?
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
while let row = try rows.next() {
let name: String = row["name"]
let color: Color = row["color"]
print(name, color)
}
}
let rows = try dbQueue.read { db in
try Row.fetchAll(db, sql: "SELECT * FROM player")
}
参数是可选的数组或字典,用于填充查询中的位置 ?
和冒号前缀的键(如 :name
)
let rows = try Row.fetchAll(db,
sql: "SELECT * FROM player WHERE name = ?",
arguments: ["Arthur"])
let rows = try Row.fetchAll(db,
sql: "SELECT * FROM player WHERE name = :name",
arguments: ["name": "Arthur"])
有关支持的参数类型(Bool、Int、String、Date、Swift 枚举等)的更多信息,请参见值;有关 SQLite 参数的详细文档,请参见 StatementArguments
。
与包含数据库行副本的行数组不同,行游标更接近 SQLite 本身,需要小心使用
注意:不要将
Row
游标转换为数组或集合。 你将无法获得你期望的不同的行。 要获取行数组,请使用Row.fetchAll(...)
。 要获取行集合,请使用Row.fetchSet(...)
。 一般来说,请确保在每次从游标中提取行以供以后使用时复制行:row.copy()
。
通过索引或列名读取列值
let name: String = row[0] // 0 is the leftmost column
let name: String = row["name"] // Leftmost matching column - lookup is case-insensitive
let name: String = row[Column("name")] // Using query interface's Column
确保在值可能为 NULL 时请求一个可选值
let name: String? = row["name"]
row[]
下标返回你请求的类型。 有关支持的值类型的更多信息,请参见值
let bookCount: Int = row["bookCount"]
let bookCount64: Int64 = row["bookCount"]
let hasBooks: Bool = row["bookCount"] // false when 0
let string: String = row["date"] // "2015-09-11 18:14:15.123"
let date: Date = row["date"] // Date
self.date = row["date"] // Depends on the type of the property.
你还可以使用 as
类型转换运算符
row[...] as Int
row[...] as Int?
警告:避免使用
as!
和as?
运算符if let int = row[...] as? Int { ... } // BAD - doesn't work if let int = row[...] as Int? { ... } // GOOD
警告:避免 nil 合并行值,而应首选
coalesce
方法let name: String? = row["nickname"] ?? row["name"] // BAD - doesn't work let name: String? = row.coalesce(["nickname", "name"]) // GOOD
一般来说,只要可以从底层 SQLite 值转换,就可以提取所需的类型
成功的转换包括
有关支持的类型(Bool、Int、String、Date、Swift 枚举等)的更多信息,请参见值
NULL 返回 nil。
let row = try Row.fetchOne(db, sql: "SELECT NULL")!
row[0] as Int? // nil
row[0] as Int // fatal error: could not convert NULL to Int.
但是,有一个例外:DatabaseValue 类型
row[0] as DatabaseValue // DatabaseValue.null
缺少的列返回 nil。
let row = try Row.fetchOne(db, sql: "SELECT 'foo' AS foo")!
row["missing"] as String? // nil
row["missing"] as String // fatal error: no such column: missing
你可以使用 hasColumn
方法显式检查列是否存在。
无效的转换会引发致命错误。
let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
row[0] as String // "Mom’s birthday"
row[0] as Date? // fatal error: could not convert "Mom’s birthday" to Date.
row[0] as Date // fatal error: could not convert "Mom’s birthday" to Date.
let row = try Row.fetchOne(db, sql: "SELECT 256")!
row[0] as Int // 256
row[0] as UInt8? // fatal error: could not convert 256 to UInt8.
row[0] as UInt8 // fatal error: could not convert 256 to UInt8.
可以使用 DatabaseValue 类型避免这些转换致命错误
let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
let dbValue: DatabaseValue = row[0]
if dbValue.isNull {
// Handle NULL
} else if let date = Date.fromDatabaseValue(dbValue) {
// Handle valid date
} else {
// Handle invalid date
}
这种额外的冗长是由于必须处理不受信任的数据库所致:你可以考虑改为修复数据库的内容。 有关更多信息,请参见致命错误。
SQLite 具有弱类型系统,并提供便捷转换,可以将 String 转换为 Int,将 Double 转换为 Blob 等。
GRDB 有时会允许这些转换通过
let rows = try Row.fetchCursor(db, sql: "SELECT '20 small cigars'")
while let row = try rows.next() {
row[0] as Int // 20
}
不要惊慌:这些转换并没有阻止 SQLite 成为你想使用的极其成功的数据库引擎。 GRDB 添加了上面描述的安全检查。 你也可以通过使用 DatabaseValue 类型完全阻止这些便捷转换。
DatabaseValue
是 SQLite 和你的值之间的中间类型,它提供有关存储在数据库中的原始值的信息。
你可以像获取其他值类型一样获取 DatabaseValue
let dbValue: DatabaseValue = row[0]
let dbValue: DatabaseValue? = row["name"] // nil if and only if column does not exist
// Check for NULL:
dbValue.isNull // Bool
// The stored value:
dbValue.storage.value // Int64, Double, String, Data, or nil
// All the five storage classes supported by SQLite:
switch dbValue.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("Data: \(data)")
}
你可以使用 fromDatabaseValue() 方法从 DatabaseValue
中提取常规值(Bool、Int、String、Date、Swift 枚举等)
let dbValue: DatabaseValue = row["bookCount"]
let bookCount = Int.fromDatabaseValue(dbValue) // Int?
let bookCount64 = Int64.fromDatabaseValue(dbValue) // Int64?
let hasBooks = Bool.fromDatabaseValue(dbValue) // Bool?, false when 0
let dbValue: DatabaseValue = row["date"]
let string = String.fromDatabaseValue(dbValue) // "2015-09-11 18:14:15.123"
let date = Date.fromDatabaseValue(dbValue) // Date?
fromDatabaseValue
对无效转换返回 nil
let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
let dbValue: DatabaseValue = row[0]
let string = String.fromDatabaseValue(dbValue) // "Mom’s birthday"
let int = Int.fromDatabaseValue(dbValue) // nil
let date = Date.fromDatabaseValue(dbValue) // nil
Row 采用标准 RandomAccessCollection 协议,可以将其视为 DatabaseValue 的字典
// All the (columnName, dbValue) tuples, from left to right:
for (columnName, dbValue) in row {
...
}
你可以从字典构建行(标准的 Swift 字典和 NSDictionary)。 更多支持的类型请参见 值。
let row: Row = ["name": "foo", "date": nil]
let row = Row(["name": "foo", "date": nil])
let row = Row(/* [AnyHashable: Any] */) // nil if invalid dictionary
然而,行并不是真正的字典:它们可能包含重复的列。
let row = try Row.fetchOne(db, sql: "SELECT 1 AS foo, 2 AS foo")!
row.columnNames // ["foo", "foo"]
row.databaseValues // [1, 2]
row["foo"] // 1 (leftmost matching column)
for (columnName, dbValue) in row { ... } // ("foo", 1), ("foo", 2)
当你从行构建字典时,你必须消除重复列的歧义,并选择如何表示数据库值。 例如:
一个 [String: DatabaseValue]
字典,如果存在重复的列名,则保留最左边的值。
let dict = Dictionary(row, uniquingKeysWith: { (left, _) in left })
一个 [String: AnyObject]
字典,如果存在重复的列名,则保留最右边的值。 这个字典与 FMDB 中 FMResultSet 的 resultDictionary 相同。它包含空列的 NSNull 值,并且可以与 Objective-C 共享。
let dict = Dictionary(
row.map { (column, dbValue) in
(column, dbValue.storage.value as AnyObject)
},
uniquingKeysWith: { (_, right) in right })
一个 [String: Any]
字典,例如,可以用来给 JSONSerialization 提供数据。
let dict = Dictionary(
row.map { (column, dbValue) in
(column, dbValue.storage.value)
},
uniquingKeysWith: { (left, _) in left })
有关更多信息,请参见 Dictionary.init(_:uniquingKeysWith:)
的文档。
你可以直接获取值,而不是获取行。 有许多支持的 值类型 (Bool, Int, String, Date, Swift 枚举等)。
与行一样,将值作为游标、数组、集合或单个值获取 (请参见 获取方法)。 值是从 SQL 查询的最左侧列中提取的。
try dbQueue.read { db in
try Int.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int
try Int.fetchAll(db, sql: "SELECT ...", arguments: ...) // [Int]
try Int.fetchSet(db, sql: "SELECT ...", arguments: ...) // Set<Int>
try Int.fetchOne(db, sql: "SELECT ...", arguments: ...) // Int?
let maxScore = try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") // Int?
let names = try String.fetchAll(db, sql: "SELECT name FROM player") // [String]
}
Int.fetchOne
在两种情况下返回 nil:要么 SELECT 语句没有产生任何行,要么产生了一行但包含一个 NULL 值。
// No row:
try Int.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // nil
// One row with a NULL value:
try Int.fetchOne(db, sql: "SELECT NULL") // nil
// One row with a non-NULL value:
try Int.fetchOne(db, sql: "SELECT 42") // 42
对于可能包含 NULL 的请求,获取可选类型。
try dbQueue.read { db in
try Optional<Int>.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int?
try Optional<Int>.fetchAll(db, sql: "SELECT ...", arguments: ...) // [Int?]
try Optional<Int>.fetchSet(db, sql: "SELECT ...", arguments: ...) // Set<Int?>
}
💡 提示:当获取单个值时,一个高级用例是区分语句未产生任何行或产生一行但包含 NULL 值的情况。为此,请使用
Optional<Int>.fetchOne
,它返回一个双重可选类型Int??
。// No row: try Optional<Int>.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // .none // One row with a NULL value: try Optional<Int>.fetchOne(db, sql: "SELECT NULL") // .some(.none) // One row with a non-NULL value: try Optional<Int>.fetchOne(db, sql: "SELECT 42") // .some(.some(42))
有许多支持的值类型 (Bool, Int, String, Date, Swift 枚举等)。 更多信息请参见 值。
GRDB 附带对以下值类型的内置支持:
Swift 标准库:Bool, Double, Float, 所有有符号和无符号整数类型, String, Swift 枚举。
Foundation: Data, Date, DateComponents, Decimal, NSNull, NSNumber, NSString, URL, UUID。
CoreGraphics: CGFloat.
DatabaseValue, 该类型提供有关存储在数据库中的原始值的信息。
全文模式:FTS3Pattern 和 FTS5Pattern。
一般来说,所有采用 DatabaseValueConvertible
协议的类型都支持。
值可以用作 语句参数。
let url: URL = ...
let verified: Bool = ...
try db.execute(
sql: "INSERT INTO link (url, verified) VALUES (?, ?)",
arguments: [url, verified])
值可以从 行中提取。
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM link")
while let row = try rows.next() {
let url: URL = row["url"]
let verified: Bool = row["verified"]
}
值可以被 直接获取。
let urls = try URL.fetchAll(db, sql: "SELECT url FROM link") // [URL]
在 Records 中使用值。
struct Link: FetchableRecord {
var url: URL
var isVerified: Bool
init(row: Row) {
url = row["url"]
isVerified = row["verified"]
}
}
在 查询接口 中使用值。
let url: URL = ...
let link = try Link.filter(Column("url") == url).fetchOne(db)
Data 适用于 BLOB SQLite 列。 它可以像其他 值 一样存储和从数据库中获取。
let rows = try Row.fetchCursor(db, sql: "SELECT data, ...")
while let row = try rows.next() {
let data: Data = row["data"]
}
在请求迭代的每个步骤中,row[]
下标都会创建数据库字节的 *两个副本*:一个由 SQLite 获取,另一个存储在 Swift Data 值中。
你可以通过不复制 SQLite 获取的数据来节省内存。
while let row = try rows.next() {
try row.withUnsafeData(name: "data") { (data: Data?) in
...
}
}
非复制数据的生命周期不会超过迭代步骤:请确保你在此点之后不使用它。
Date 和 DateComponents 可以存储和从数据库中获取。
以下是 GRDB 如何支持 SQLite 支持的各种 日期格式。
SQLite 格式 | Date | DateComponents |
---|---|---|
YYYY-MM-DD | 读取 ¹ | 读取 / 写入 |
YYYY-MM-DD HH:MM | 读取 ¹ ² | 读取 ² / 写入 |
YYYY-MM-DD HH:MM:SS | 读取 ¹ ² | 读取 ² / 写入 |
YYYY-MM-DD HH:MM:SS.SSS | 读取 ¹ ² / 写入 ¹ | 读取 ² / 写入 |
YYYY-MM-DDTHH:MM | 读取 ¹ ² | 读取 ² |
YYYY-MM-DDTHH:MM:SS | 读取 ¹ ² | 读取 ² |
YYYY-MM-DDTHH:MM:SS.SSS | 读取 ¹ ² | 读取 ² |
HH:MM | 读取 ² / 写入 | |
HH:MM:SS | 读取 ² / 写入 | |
HH:MM:SS.SSS | 读取 ² / 写入 | |
自 Unix 纪元以来的时间戳 | 读取 ³ | |
now |
¹ 缺失的组件假定为零。 日期以 UTC 时区存储和读取,除非格式后跟时区指示符 ⁽²⁾。
² 此格式可以选择后跟 [+-]HH:MM
或仅 Z
形式的时区指示符。
³ GRDB 2+ 将数值解释为驱动 Date(timeIntervalSince1970:)
的时间戳。 之前的 GRDB 版本过去将数字解释为 儒略日。 儒略日仍然支持,通过 Date(julianDay:)
初始化器。
警告:SQLite 日期格式中有效年份的范围是 0000-9999。 当你的应用程序需要处理此范围之外的年份时,你需要选择另一种日期格式。 请参见以下章节。
Date 可以像其他 值 一样存储和从数据库中获取。
try db.execute(
sql: "INSERT INTO player (creationDate, ...) VALUES (?, ...)",
arguments: [Date(), ...])
let row = try Row.fetchOne(db, ...)!
let creationDate: Date = row["creationDate"]
日期使用 "YYYY-MM-DD HH:MM:SS.SSS" 格式以 UTC 时区存储。 它精确到毫秒。
注意:选择此格式是因为它是唯一一个:
- 可比较的 (
ORDER BY date
工作)- 与 SQLite 关键字 CURRENT_TIMESTAMP 可比较 (
WHERE date > CURRENT_TIMESTAMP
工作)- 能够给 SQLite 日期和时间函数 提供数据
- 足够精确
警告:SQLite 日期格式中有效年份的范围是 0000-9999。 你会遇到超出此范围的年份的问题,例如解码错误,或使用 SQLite 日期和时间函数 进行的无效日期计算。
某些应用程序可能更喜欢另一种日期格式。
T
分隔符的 ISO-8601。Date
往返。在选择不同的日期格式之前,你应该三思。
Date
往返并不像乍看起来那样明显。 日期通常在离开你的应用程序后就不能精确往返,因为你的应用程序与之通信的其他系统使用它们自己的日期表示形式(你的应用程序的 Android 版本,你的应用程序与之通信的服务器等)。 最重要的是,Date
比较至少与 浮点数比较 一样困难和令人讨厌。日期格式的自定义是明确的。 例如:
let date = Date()
let timeInterval = date.timeIntervalSinceReferenceDate
try db.execute(
sql: "INSERT INTO player (creationDate, ...) VALUES (?, ...)",
arguments: [timeInterval, ...])
if let row = try Row.fetchOne(db, ...) {
let timeInterval: TimeInterval = row["creationDate"]
let creationDate = Date(timeIntervalSinceReferenceDate: timeInterval)
}
另请参见 Codable Records 以获取更多日期自定义选项,如果你想定义一个带有自定义数据库表示的 Date 包装类型,请参见 DatabaseValueConvertible
。
DateComponents 通过 DatabaseDateComponents 辅助类型间接支持。
DatabaseDateComponents 从所有 SQLite 支持的日期格式 中读取日期组件,并将它们存储在你选择的格式中,从 HH:MM 到 YYYY-MM-DD HH:MM:SS.SSS。
警告:有效年份的范围是 0000-9999。 你会遇到超出此范围的年份的问题,例如解码错误,或使用 SQLite 日期和时间函数 进行的无效日期计算。 更多信息请参见 Date。
DatabaseDateComponents 可以像其他 值 一样存储和从数据库中获取。
let components = DateComponents()
components.year = 1973
components.month = 9
components.day = 18
// Store "1973-09-18"
let dbComponents = DatabaseDateComponents(components, format: .YMD)
try db.execute(
sql: "INSERT INTO player (birthDate, ...) VALUES (?, ...)",
arguments: [dbComponents, ...])
// Read "1973-09-18"
let row = try Row.fetchOne(db, sql: "SELECT birthDate ...")!
let dbComponents: DatabaseDateComponents = row["birthDate"]
dbComponents.format // .YMD (the actual format found in the database)
dbComponents.dateComponents // DateComponents
NSNumber 和 Decimal 可以像其他 值 一样存储和从数据库中获取。
以下是 GRDB 如何支持 SQLite 支持的各种数据类型。
Integer | Double | String | |
---|---|---|---|
NSNumber | 读取 / 写入 | 读取 / 写入 | 读取 |
NSDecimalNumber | 读取 / 写入 | 读取 / 写入 | 读取 |
Decimal | 读取 | 读取 | 读取 / 写入 |
所有这三种类型都可以解码数据库整数和双精度浮点数。
let number = try NSNumber.fetchOne(db, sql: "SELECT 10") // NSNumber
let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT 1.23") // NSDecimalNumber
let number = try Decimal.fetchOne(db, sql: "SELECT -100") // Decimal
所有这三种类型都将数据库字符串解码为十进制数字。
let number = try NSNumber.fetchOne(db, sql: "SELECT '10'") // NSDecimalNumber (sic)
let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT '1.23'") // NSDecimalNumber
let number = try Decimal.fetchOne(db, sql: "SELECT '-100'") // Decimal
NSNumber
和 NSDecimalNumber
在数据库中发送 64 位有符号整数和双精度浮点数。
// INSERT INTO transfer VALUES (10)
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10)])
// INSERT INTO transfer VALUES (10.0)
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10.0)])
// INSERT INTO transfer VALUES (10)
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.0")])
// INSERT INTO transfer VALUES (10.5)
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.5")])
警告:由于 SQLite 不支持十进制数字,因此发送非整数
NSDecimalNumber
可能会导致在转换为双精度浮点数时精度损失。与其将非整数
NSDecimalNumber
发送到数据库,你可能更喜欢:
- 发送
Decimal
代替(这些将十进制字符串存储在数据库中)。- 发送整数代替(例如,存储美分金额而不是欧元金额)。
Decimal
在数据库中发送十进制字符串。
// INSERT INTO transfer VALUES ('10')
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(10)])
// INSERT INTO transfer VALUES ('10.5')
try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(string: "10.5")!])
UUID 可以像其他 值 一样存储和从数据库中获取。
GRDB 将 uuid 存储为 16 字节的数据 blob,并从 16 字节的数据 blob 和字符串(例如 "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")中解码它们。
Swift 枚举 以及通常所有采用 RawRepresentable 协议的类型,都可以像它们的原始 值 一样存储和从数据库中获取。
enum Color : Int {
case red, white, rose
}
enum Grape : String {
case chardonnay, merlot, riesling
}
// Declare empty DatabaseValueConvertible adoption
extension Color : DatabaseValueConvertible { }
extension Grape : DatabaseValueConvertible { }
// Store
try db.execute(
sql: "INSERT INTO wine (grape, color) VALUES (?, ?)",
arguments: [Grape.merlot, Color.red])
// Read
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
while let row = try rows.next() {
let grape: Grape = row["grape"]
let color: Color = row["color"]
}
当数据库值与任何枚举情况都不匹配时,你会得到一个致命错误。 可以使用 DatabaseValue 类型避免此致命错误。
let row = try Row.fetchOne(db, sql: "SELECT 'syrah'")!
row[0] as String // "syrah"
row[0] as Grape? // fatal error: could not convert "syrah" to Grape.
row[0] as Grape // fatal error: could not convert "syrah" to Grape.
let dbValue: DatabaseValue = row[0]
if dbValue.isNull {
// Handle NULL
} else if let grape = Grape.fromDatabaseValue(dbValue) {
// Handle valid grape
} else {
// Handle unknown grape
}
SQLite 允许你定义 SQL 函数和聚合。
自定义 SQL 函数或聚合扩展了 SQLite
SELECT reverse(name) FROM player; -- custom function
SELECT maxLength(name) FROM player; -- custom aggregate
函数 参数接受一个 DatabaseValue 数组,并返回任何有效的 值 (Bool, Int, String, Date, Swift 枚举等)。 数据库值的数量保证是 argumentCount。
当函数是“纯函数”时,SQLite 有机会执行额外的优化,这意味着它们的结果仅取决于它们的参数。 因此,请尽可能将 pure 参数设置为 true。
let reverse = DatabaseFunction("reverse", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
// Extract string value, if any...
guard let string = String.fromDatabaseValue(values[0]) else {
return nil
}
// ... and return reversed string:
return String(string.reversed())
}
你通过数据库连接的配置使函数可用。
var config = Configuration()
config.prepareDatabase { db in
db.add(function: reverse)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
try dbQueue.read { db in
// "oof"
try String.fetchOne(db, sql: "SELECT reverse('foo')")!
}
函数可以接受可变数量的参数。
当你未提供任何显式 argumentCount 时,该函数可以接受任意数量的参数。
let averageOf = DatabaseFunction("averageOf", pure: true) { (values: [DatabaseValue]) in
let doubles = values.compactMap { Double.fromDatabaseValue($0) }
return doubles.reduce(0, +) / Double(doubles.count)
}
db.add(function: averageOf)
// 2.0
try Double.fetchOne(db, sql: "SELECT averageOf(1, 2, 3)")!
函数可以抛出错误。
let sqrt = DatabaseFunction("sqrt", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
guard let double = Double.fromDatabaseValue(values[0]) else {
return nil
}
guard double >= 0 else {
throw DatabaseError(message: "invalid negative number")
}
return sqrt(double)
}
db.add(function: sqrt)
// SQLite error 1 with statement `SELECT sqrt(-1)`: invalid negative number
try Double.fetchOne(db, sql: "SELECT sqrt(-1)")!
在 查询界面 中使用自定义函数。
// SELECT reverseString("name") FROM player
Player.select(reverseString(nameColumn))
GRDB 附带内置的 SQL 函数,这些函数执行 Unicode 感知的字符串转换。 请参见 Unicode。
📖 DatabaseFunction
, DatabaseAggregate
在注册自定义聚合函数之前,你需要定义一个遵循 DatabaseAggregate
协议的类型。
protocol DatabaseAggregate {
// Initializes an aggregate
init()
// Called at each step of the aggregation
mutating func step(_ dbValues: [DatabaseValue]) throws
// Returns the final result
func finalize() throws -> DatabaseValueConvertible?
}
例如
struct MaxLength : DatabaseAggregate {
var maxLength: Int = 0
mutating func step(_ dbValues: [DatabaseValue]) {
// At each step, extract string value, if any...
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return
}
// ... and update the result
let length = string.count
if length > maxLength {
maxLength = length
}
}
func finalize() -> DatabaseValueConvertible? {
maxLength
}
}
let maxLength = DatabaseFunction(
"maxLength",
argumentCount: 1,
pure: true,
aggregate: MaxLength.self)
和 自定义 SQL 函数 一样,你可以通过数据库连接的配置,使聚合函数对数据库连接可用。
var config = Configuration()
config.prepareDatabase { db in
db.add(function: maxLength)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
try dbQueue.read { db in
// Some Int
try Int.fetchOne(db, sql: "SELECT maxLength(name) FROM player")!
}
聚合函数的 step
方法接受一个 DatabaseValue
数组。这个数组包含与 *argumentCount* 参数一样多的值(或者当 *argumentCount* 参数被省略时,包含任意数量的值)。
聚合函数的 finalize
方法返回最终的聚合 值 (Bool, Int, String, Date, Swift 枚举等等)。
当聚合函数是“纯粹的”时,SQLite 有机会执行额外的优化,这意味着它们的结果只取决于它们的输入。因此,如果可能,请确保将 *pure* 参数设置为 true。
在 查询接口 中使用自定义聚合函数。
// SELECT maxLength("name") FROM player
let request = Player.select(maxLength.apply(nameColumn))
try Int.fetchOne(db, request) // Int?
如果并非所有的 SQLite API 都暴露在 GRDB 中,你仍然可以使用 SQLite C 接口 并调用 SQLite C 函数。
要从 SQLCipher 或系统 SQLite 访问 C SQLite 函数,你需要执行额外的导入。
import SQLite3 // System SQLite
import SQLCipher // SQLCipher
let sqliteVersion = String(cString: sqlite3_libversion())
可以通过 Database.sqliteConnection
和 Statement.sqliteStatement
属性访问数据库连接和语句的原始指针。
try dbQueue.read { db in
// The raw pointer to a database connection:
let sqliteConnection = db.sqliteConnection
// The raw pointer to a statement:
let statement = try db.makeStatement(sql: "SELECT ...")
let sqliteStatement = statement.sqliteStatement
}
注意
- 这些指针由 GRDB 拥有:不要关闭 GRDB 创建的连接或结束语句。
- GRDB 在“多线程模式”下打开 SQLite 连接,这(奇怪地)意味着**它们不是线程安全的**。请确保在它们专用的调度队列中操作原始数据库和语句。
- 使用原始 SQLite C 接口的风险自负。GRDB 不会阻止你搬起石头砸自己的脚。
**除了 SQLite API 之外,GRDB 还提供了协议**,这些协议有助于将数据库行作为名为“记录”的常规对象进行操作。
try dbQueue.write { db in
if var place = try Place.fetchOne(db, id: 1) {
place.isFavorite = true
try place.update(db)
}
}
要定义记录类型,请定义一个类型并使用附带重点功能集的协议对其进行扩展。
例如
struct Player {
var id: Int64
var name: String
var score: Int
}
// Players can be fetched from the database.
extension Player: FetchableRecord { ... }
// Players can be saved into the database.
extension Player: PersistableRecord { ... }
查看一些 记录定义示例。
注意:如果你熟悉 Core Data 的 NSManagedObject 或 Realm 的 Object,你可能会感到文化冲击:GRDB 记录不是唯一的,不会自动更新,也不会延迟加载。这既是目的,也是面向协议编程的结果。
提示:设计记录类型的推荐做法 指南提供了通用指导。
提示:查看 Demo Applications 获取使用记录的示例应用程序。
概述
协议和 Record 类
要在数据库中插入记录,请调用 insert
方法。
let player = Player(name: "Arthur", email: "arthur@example.com")
try player.insert(db)
👉 insert
方法适用于遵循 PersistableRecord 协议的类型。
要从数据库中获取记录,请调用一个 获取方法。
let arthur = try Player.fetchOne(db, // Player?
sql: "SELECT * FROM players WHERE name = ?",
arguments: ["Arthur"])
let bestPlayers = try Player // [Player]
.order(Column("score").desc)
.limit(10)
.fetchAll(db)
let spain = try Country.fetchOne(db, id: "ES") // Country?
let italy = try Country.find(db, id: "IT") // Country
👉 对于遵循 FetchableRecord 协议的类型,可以使用原始 SQL 进行获取。
👉 对于同时遵循 FetchableRecord 和 TableRecord 协议的类型,可以使用 查询接口 进行无 SQL 获取。
要在数据库中更新记录,请调用 update
方法。
var player: Player = ...
player.score = 1000
try player.update(db)
可以 避免无用的更新。
// does not hit the database if score has not changed
try player.updateChanges(db) {
$0.score = 1000
}
有关批量更新,请参见 查询接口。
try Player
.filter(Column("team") == "red")
.updateAll(db, Column("score") += 1)
👉 update 方法适用于遵循 PersistableRecord 协议的类型。批量更新适用于 TableRecord 协议。
要在数据库中删除记录,请调用 delete
方法。
let player: Player = ...
try player.delete(db)
你还可以通过主键、唯一键删除,或执行批量删除(参见 删除请求)。
try Player.deleteOne(db, id: 1)
try Player.deleteOne(db, key: ["email": "arthur@example.com"])
try Country.deleteAll(db, ids: ["FR", "US"])
try Player
.filter(Column("email") == nil)
.deleteAll(db)
👉 delete 方法适用于遵循 PersistableRecord 协议的类型。批量删除适用于 TableRecord 协议。
要计数记录,请调用 fetchCount
方法。
let playerCount: Int = try Player.fetchCount(db)
let playerWithEmailCount: Int = try Player
.filter(Column("email") == nil)
.fetchCount(db)
👉 fetchCount
方法适用于遵循 TableRecord 协议的类型。
详细信息如下
**GRDB 附带了三个记录协议**。根据你希望扩展类型的功能,你自己的类型将采用其中一个或多个协议。
FetchableRecord 能够**解码数据库行**。
struct Place: FetchableRecord { ... }
let places = try dbQueue.read { db in
try Place.fetchAll(db, sql: "SELECT * FROM place")
}
💡 **提示**:
FetchableRecord
可以从标准Decodable
协议派生其实现。有关更多信息,请参见 Codable 记录。
FetchableRecord
可以解码数据库行,但它无法为你构建 SQL 请求。为此,你还需要 TableRecord
。
TableRecord 能够**生成 SQL 查询**。
struct Place: TableRecord { ... }
let placeCount = try dbQueue.read { db in
// Generates and runs `SELECT COUNT(*) FROM place`
try Place.fetchCount(db)
}
当一个类型同时遵循 TableRecord
和 FetchableRecord
时,它可以从这些请求中加载数据。
struct Place: TableRecord, FetchableRecord { ... }
try dbQueue.read { db in
let places = try Place.order(Column("title")).fetchAll(db)
let paris = try Place.fetchOne(id: 1)
}
PersistableRecord 能够**写入**:它可以在数据库中创建、更新和删除行。
struct Place : PersistableRecord { ... }
try dbQueue.write { db in
try Place.delete(db, id: 1)
try Place(...).insert(db)
}
一个可持久化的记录还可以针对其他记录进行比较,并避免无用的数据库更新。
💡 **提示**:
PersistableRecord
可以从标准Encodable
协议派生其实现。有关更多信息,请参见 Codable 记录。
**FetchableRecord 协议向任何可以从数据库行构建的类型授予获取方法**。
protocol FetchableRecord {
/// Row initializer
init(row: Row) throws
}
例如
struct Place {
var id: Int64?
var title: String
var coordinate: CLLocationCoordinate2D
}
extension Place : FetchableRecord {
init(row: Row) {
id = row["id"]
title = row["title"]
coordinate = CLLocationCoordinate2D(
latitude: row["latitude"],
longitude: row["longitude"])
}
}
行也接受列枚举。
extension Place : FetchableRecord {
enum Columns: String, ColumnExpression {
case id, title, latitude, longitude
}
init(row: Row) {
id = row[Columns.id]
title = row[Columns.title]
coordinate = CLLocationCoordinate2D(
latitude: row[Columns.latitude],
longitude: row[Columns.longitude])
}
}
有关 row[]
下标的更多信息,请参见 列值。
当你的记录类型遵循标准 Decodable 协议时,你不必为 init(row:)
提供实现。有关更多信息,请参见 Codable 记录。
// That's all
struct Player: Decodable, FetchableRecord {
var id: Int64
var name: String
var score: Int
}
FetchableRecord 允许采用类型从 SQL 查询中获取。
try Place.fetchCursor(db, sql: "SELECT ...", arguments:...) // A Cursor of Place
try Place.fetchAll(db, sql: "SELECT ...", arguments:...) // [Place]
try Place.fetchSet(db, sql: "SELECT ...", arguments:...) // Set<Place>
try Place.fetchOne(db, sql: "SELECT ...", arguments:...) // Place?
有关 fetchCursor
、fetchAll
、fetchSet
和 fetchOne
方法的信息,请参见 获取方法。有关查询参数的更多信息,请参见 StatementArguments
。
**注意**:出于性能原因,在 fetch 查询的迭代过程中,传递给
init(row:)
的相同 row 参数会被重用。 如果你想保留该行以供以后使用,请确保存储一个副本:self.row = row.copy()
。
**注意**:
FetchableRecord.init(row:)
初始化器满足了大多数应用程序的需求。但是,有些应用程序的需求更高。当 FetchableRecord 不能完全提供你需要的支持时,请查看 超越 FetchableRecord 章节。
**TableRecord 协议**为你生成 SQL。
protocol TableRecord {
static var databaseTableName: String { get }
static var databaseSelection: [any SQLSelectable] { get }
}
databaseSelection
类型属性是可选的,并在 请求选择的列 章节中进行了文档说明。
databaseTableName
类型属性是一个数据库表的名称。 默认情况下,它派生自类型名称。
struct Place: TableRecord { }
print(Place.databaseTableName) // prints "place"
例如
place
country
postalAddress
httpRequest
toefl
你仍然可以提供自定义表名。
struct Place: TableRecord {
static let databaseTableName = "location"
}
print(Place.databaseTableName) // prints "location"
当一个类型同时采用 TableRecord 和 FetchableRecord 时,可以使用 查询接口 获取它。
// SELECT * FROM place WHERE name = 'Paris'
let paris = try Place.filter(nameColumn == "Paris").fetchOne(db)
TableRecord 还可以处理主键和唯一键:参见 按键获取 和 测试记录是否存在。
📖 EncodableRecord
, MutablePersistableRecord
, PersistableRecord
GRDB 记录类型可以在数据库中创建、更新和删除行。
这些能力由三个协议授予。
// Defines how a record encodes itself into the database
protocol EncodableRecord {
/// Defines the values persisted in the database
func encode(to container: inout PersistenceContainer) throws
}
// Adds persistence methods
protocol MutablePersistableRecord: TableRecord, EncodableRecord {
/// 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 didInsert(_ inserted: InsertionSuccess)
}
// Adds immutability
protocol PersistableRecord: MutablePersistableRecord {
/// Non-mutating version of the optional didInsert(_:)
func didInsert(_ inserted: InsertionSuccess)
}
是的,三个协议而不是一个。 这是你如何选择其中一个或另一个。
**如果你的类型是一个类**,请选择 PersistableRecord
。 最重要的是,如果数据库表具有自动递增的主键,请实现 didInsert(_:)
。
**如果你的类型是一个结构体,并且数据库表具有自动递增的主键**,请选择 MutablePersistableRecord
,并实现 didInsert(_:)
。
**否则**,选择 PersistableRecord
,并忽略 didInsert(_:)
。
encode(to:)
方法定义了哪些 值(Bool,Int,String,Date,Swift 枚举等等)被分配给数据库列。
可选的 didInsert
方法允许采用类型在成功插入后存储其 rowID,并且仅对具有自动递增主键的表有用。 它从受保护的调度队列调用,并与所有数据库更新序列化。
例如
extension Place : MutablePersistableRecord {
/// The values persisted in the database
func encode(to container: inout PersistenceContainer) {
container["id"] = id
container["title"] = title
container["latitude"] = coordinate.latitude
container["longitude"] = coordinate.longitude
}
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
var paris = Place(
id: nil,
title: "Paris",
coordinate: CLLocationCoordinate2D(latitude: 48.8534100, longitude: 2.3488000))
try paris.insert(db)
paris.id // some value
持久化容器也接受列枚举。
extension Place : MutablePersistableRecord {
enum Columns: String, ColumnExpression {
case id, title, latitude, longitude
}
func encode(to container: inout PersistenceContainer) {
container[Columns.id] = id
container[Columns.title] = title
container[Columns.latitude] = coordinate.latitude
container[Columns.longitude] = coordinate.longitude
}
}
当你的记录类型遵循标准 Encodable 协议时,你不必为 encode(to:)
提供实现。有关更多信息,请参见 Codable 记录。
// That's all
struct Player: Encodable, MutablePersistableRecord {
var id: Int64?
var name: String
var score: Int
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
遵循 PersistableRecord 协议的类型具有插入、更新和删除的方法。
// INSERT
try place.insert(db)
let insertedPlace = try place.inserted(db) // non-mutating
// UPDATE
try place.update(db)
try place.update(db, columns: ["title"])
// Maybe UPDATE
try place.updateChanges(db, from: otherPlace)
try place.updateChanges(db) { $0.isFavorite = true }
// INSERT or UPDATE
try place.save(db)
let savedPlace = place.saved(db) // non-mutating
// UPSERT
try place.upsert(db)
let insertedPlace = place.upsertAndFetch(db)
// DELETE
try place.delete(db)
// EXISTENCE CHECK
let exists = try place.exists(db)
有关 upsert 的更多信息,请参见下面的 Upsert。
TableRecord 协议附带批量操作。:
// UPDATE
try Place.updateAll(db, ...)
// DELETE
try Place.deleteAll(db)
try Place.deleteAll(db, ids:...)
try Place.deleteAll(db, keys:...)
try Place.deleteOne(db, id:...)
try Place.deleteOne(db, key:...)
有关批量更新的更多信息,请参见 更新请求。
所有持久化方法都可以抛出 DatabaseError。
如果数据库不包含该记录的主键的任何行,则 update
和 updateChanges
会抛出 RecordError。
save
确保你的值被存储到数据库中。如果记录具有非空的 primary key(主键),它会执行 UPDATE 操作;如果没有任何行被修改,则执行 INSERT 操作。如果记录没有 primary key,或者 primary key 为空,它会直接执行 INSERT 操作。
delete
和 deleteOne
返回一个数据库行是否被删除。deleteAll
返回被删除的行数。updateAll
返回被更新的行数。updateChanges
返回一个数据库行是否被更新。
支持所有主键,包括跨越多个列的复合主键,以及隐藏的 rowid
列。
要自定义持久化方法,你可以提供持久化回调,如下所述。不要尝试覆盖现成的持久化方法。
UPSERT 是 SQLite 的一项功能,如果 INSERT 操作违反了唯一性约束(primary key 或唯一索引),则使 INSERT 操作表现为 UPDATE 操作或无操作。
注意:Upsert API 在 SQLite 3.35.0+ 版本中可用:iOS 15.0+、macOS 12.0+、tvOS 15.0+、watchOS 8.0+,或者通过自定义 SQLite 构建或SQLCipher。
注意:关于持久化回调,upsert 的行为与 insert 完全相同。特别是:
aroundInsert(_:)
和didInsert(_:)
回调报告了插入或更新行的 rowid;willUpdate
、aroundUdate
、didUdate
不会被调用。
PersistableRecord 提供了三种 upsert 方法
upsert(_:)
插入或更新记录。
如果表上的任何唯一性约束(primary key 或唯一索引)被违反,则会触发 upsert 行为。如果发生冲突,除 primary key 外的所有列都会被插入的值覆盖
struct Player: Encodable, PersistableRecord {
var id: Int64
var name: String
var score: Int
}
// INSERT INTO player (id, name, score)
// VALUES (1, 'Arthur', 1000)
// ON CONFLICT DO UPDATE SET
// name = excluded.name,
// score = excluded.score
let player = Player(id: 1, name: "Arthur", score: 1000)
try player.upsert(db)
upsertAndFetch(_:onConflict:doUpdate:)
(需要符合 FetchableRecord 协议)
插入或更新记录,并返回 upsert 后的记录。
onConflict
和 doUpdate
参数允许你进一步控制 upsert 行为。请务必查看 SQLite UPSERT 文档以获取详细信息。
onConflict
: “冲突目标”是唯一性约束(primary key 或唯一索引)中触发 upsert 的列的数组。
如果为空(默认值),则考虑所有唯一性约束。
doUpdate
:一个闭包,返回在发生冲突时要执行的列赋值。其他列会被插入的值覆盖。
默认情况下,所有插入的列,除了 primary key 和冲突目标外,都会被覆盖。
在下面的例子中,我们 upsert 新的词汇单词 "jovial"。如果该单词尚未在词典中,则插入它。否则,count
会递增,isTainted
不会被覆盖,kind
会被覆盖
// CREATE TABLE vocabulary(
// word TEXT NOT NULL PRIMARY KEY,
// kind TEXT NOT NULL,
// isTainted BOOLEAN DEFAULT 0,
// count INT DEFAULT 1))
struct Vocabulary: Encodable, PersistableRecord {
var word: String
var kind: String
var isTainted: Bool
}
// INSERT INTO vocabulary(word, kind, isTainted)
// VALUES('jovial', 'adjective', 0)
// ON CONFLICT(word) DO UPDATE SET \
// count = count + 1, -- on conflict, count is incremented
// kind = excluded.kind -- on conflict, kind is overwritten
// RETURNING *
let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
let upserted = try vocabulary.upsertAndFetch(
db, onConflict: ["word"],
doUpdate: { _ in
[Column("count") += 1, // on conflict, count is incremented
Column("isTainted").noOverwrite] // on conflict, isTainted is NOT overwritten
})
doUpdate
闭包接受一个 excluded
TableAlias 参数,该参数引用触发冲突的插入值。你可以使用它来指定显式覆盖,或执行计算。在下一个示例中,如果发生冲突,upsert 会保留最大日期
// INSERT INTO message(id, text, date)
// VALUES(...)
// ON CONFLICT DO UPDATE SET \
// text = excluded.text,
// date = MAX(date, excluded.date)
// RETURNING *
let upserted = try message.upsertAndFetch(doUpdate: { excluded in
// keep the maximum date in case of conflict
[Column("date").set(to: max(Column("date"), excluded["date"]))]
})
upsertAndFetch(_:as:onConflict:doUpdate:)
(不需要符合 FetchableRecord 协议)
此方法与上面描述的 upsertAndFetch(_:onConflict:doUpdate:)
相同,但是你可以提供不同的 FetchableRecord 记录类型作为结果,以便指定返回的列。
SQLite 能够使用 RETURNING
子句从插入、更新或删除的行中返回值。
注意:对
RETURNING
子句的支持在 SQLite 3.35.0+ 版本中可用:iOS 15.0+、macOS 12.0+、tvOS 15.0+、watchOS 8.0+,或者通过自定义 SQLite 构建或SQLCipher。
RETURNING
子句有助于处理数据库功能,例如自增 id、默认值和生成的列。例如,你可以插入一些列,并在一个步骤中获取默认列或生成的列。
GRDB 在所有名称中包含 AndFetch
的持久化方法中使用 RETURNING
子句。
例如,给定一个具有自增 primary key 和默认分数的数据库表
try dbQueue.write { db in
try db.execute(sql: """
CREATE TABLE player(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
score INTEGER NOT NULL DEFAULT 1000)
""")
}
你可以定义一个包含完整数据库信息的记录类型,以及另一个处理列子集的局部记录类型
// A player with full database information
struct Player: Codable, PersistableRecord, FetchableRecord {
var id: Int64
var name: String
var score: Int
}
// A partial player
struct PartialPlayer: Encodable, PersistableRecord {
static let databaseTableName = "player"
var name: String
}
现在你可以通过插入一个局部 player 来获得一个完整的 player
try dbQueue.write { db in
let partialPlayer = PartialPlayer(name: "Alice")
// INSERT INTO player (name) VALUES ('Alice') RETURNING *
let player = try partialPlayer.insertAndFetch(db, as: Player.self)
print(player.id) // The inserted id
print(player.name) // The inserted name
print(player.score) // The default score
}
为了获得更高的精度,你可以只选择你需要的列,并从提供的预编译的 Statement
中获取所需的值
try dbQueue.write { db in
let partialPlayer = PartialPlayer(name: "Alice")
// INSERT INTO player (name) VALUES ('Alice') RETURNING score
let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
try Int.fetchOne(statement)
}
print(score) // Prints 1000, the default score
}
还有其他类似的持久化方法,例如 upsertAndFetch
、saveAndFetch
、updateAndFetch
、updateChangesAndFetch
等。它们的行为都类似于 upsert
、save
、update
、updateChanges
,只不过它们返回保存的值。例如
// Save and return the saved player
let savedPlayer = try player.saveAndFetch(db)
有关更多信息,请参见持久化方法、Upsert 和 updateChanges
方法。
批量操作可以返回更新或删除的值
警告:请务必查看
RETURNING
子句的文档,其中描述了批量操作的重要限制和注意事项。
let request = Player.filter(...)...
// Fetch all deleted players
// DELETE FROM player RETURNING *
let deletedPlayers = try request.deleteAndFetchAll(db) // [Player]
// Fetch a selection of columns from the deleted rows
// DELETE FROM player RETURNING name
let statement = try request.deleteAndFetchStatement(db, selection: [Column("name")])
let deletedNames = try String.fetchSet(statement)
// Fetch all updated players
// UPDATE player SET score = score + 10 RETURNING *
let updatedPlayers = try request.updateAndFetchAll(db, [Column("score") += 10]) // [Player]
// Fetch a selection of columns from the updated rows
// UPDATE player SET score = score + 10 RETURNING score
let statement = try request.updateAndFetchStatement(
db, [Column("score") += 10],
select: [Column("score")])
let updatedScores = try Int.fetchAll(statement)
当调用持久化方法时,你的自定义类型可能想要执行额外的工作。
为此,你的记录类型可以实现持久化回调。回调是在记录生命周期的特定时刻被调用的方法。通过回调,可以编写在插入、更新或删除记录时运行的代码。
为了使用回调方法,你需要提供它的实现。例如,一个经常使用的回调是 didInsert
,在自增数据库 id 的情况下
struct Player: MutablePersistableRecord {
var id: Int64?
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
try dbQueue.write { db in
var player = Player(id: nil, ...)
try player.insert(db)
print(player.id) // didInsert was called: prints some non-nil id
}
回调还可以帮助实现记录验证
struct Link: PersistableRecord {
var url: URL
func willSave(_ db: Database) throws {
if url.host == nil {
throw ValidationError("url must be absolute.")
}
}
}
try link.insert(db) // Calls the willSave callback
try link.update(db) // Calls the willSave callback
try link.save(db) // Calls the willSave callback
try link.upsert(db) // Calls the willSave callback
以下是所有可用的持久化回调的列表,按照它们在各自操作期间被调用的顺序排列
插入记录(所有 record.insert
和 record.upsert
方法)
willSave
aroundSave
willInsert
aroundInsert
didInsert
didSave
更新记录(所有 record.update
方法)
willSave
aroundSave
willUpdate
aroundUpdate
didUpdate
didSave
删除记录(仅 record.delete(_:)
方法)
willDelete
aroundDelete
didDelete
有关每个回调的详细信息,请查看参考文档。
在 MutablePersistableRecord
协议中,willInsert
和 didInsert
是 mutating 方法。在 PersistableRecord
中,它们不是 mutating 方法。
注意:如果记录具有非空的 primary key,则
record.save(_:)
方法会执行 UPDATE 操作;如果没有任何行被修改,则执行 INSERT 操作。如果记录没有 primary key,或者 primary key 为空,它会直接执行 INSERT 操作。它会相应地触发 update 和/或 insert 回调。警告:回调仅从对记录实例调用的持久化方法中调用。当你调用类型方法、执行批量操作或执行原始 SQL 时,不会调用回调。
警告:当调用
did***
回调时,不要假设更改实际上已持久化到磁盘,因为数据库可能仍在未提交的事务中。当你需要处理事务完成时,请使用afterNextTransaction(onCommit:onRollback:)。例如struct PictureFile: PersistableRecord { var path: String func willDelete(_ db: Database) { db.afterNextTransaction { _ in try? deleteFileOnDisk() } } }
当记录类型映射一个具有单列 primary key 的表时,建议使其采用标准的 Identifiable 协议。
struct Player: Identifiable, FetchableRecord, PersistableRecord {
var id: Int64 // fulfills the Identifiable requirement
var name: String
var score: Int
}
当 id
具有数据库兼容的类型(Int64、Int、String、UUID 等)时,符合 Identifiable
协议会解锁类型安全的记录和请求方法
let player = try Player.find(db, id: 1) // Player
let player = try Player.fetchOne(db, id: 1) // Player?
let players = try Player.fetchAll(db, ids: [1, 2, 3]) // [Player]
let players = try Player.fetchSet(db, ids: [1, 2, 3]) // Set<Player>
let request = Player.filter(id: 1)
let request = Player.filter(ids: [1, 2, 3])
try Player.deleteOne(db, id: 1)
try Player.deleteAll(db, ids: [1, 2, 3])
注意:并非所有记录类型都可以设置为
Identifiable
,并且并非所有表都具有单列 primary key。GRDB 提供了其他处理 primary key 和唯一键的方法,但它们不会检查其参数的类型// Available on non-Identifiable types try Player.fetchOne(db, key: 1) try Player.fetchOne(db, key: ["email": "arthur@example.com"]) try Country.fetchAll(db, keys: ["FR", "US"]) try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"]) let request = Player.filter(key: 1) let request = Player.filter(keys: [1, 2, 3]) try Player.deleteOne(db, key: 1) try Player.deleteAll(db, keys: [1, 2, 3])
注意:不建议在使用自增 primary key 的记录类型上使用
Identifiable
// AVOID declaring Identifiable conformance when key is auto-incremented struct Player { var id: Int64? // Not an id suitable for Identifiable var name: String var score: Int } extension Player: FetchableRecord, MutablePersistableRecord { // Update auto-incremented id upon successful insertion mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } }有关详细的基本原理,请参阅issue #1435。
某些数据库表具有一个名为 "id" 的单列 primary key
try db.create(table: "country") { t in
t.primaryKey("isoCode", .text)
t.column("name", .text).notNull()
t.column("population", .integer).notNull()
}
在这种情况下,例如,可以通过从 id
属性返回 primary key 列来实现 Identifiable
协议
struct Country: Identifiable, FetchableRecord, PersistableRecord {
var isoCode: String
var name: String
var population: Int
// Fulfill the Identifiable requirement
var id: String { isoCode }
}
let france = try dbQueue.read { db in
try Country.fetchOne(db, id: "FR")
}
采用存档协议(Codable、Encodable 或 Decodable)的记录类型只需声明符合所需的记录协议即可获得免费的数据库支持
// Declare a record...
struct Player: Codable, FetchableRecord, PersistableRecord {
var name: String
var score: Int
}
// ...and there you go:
try dbQueue.write { db in
try Player(name: "Arthur", score: 100).insert(db)
let players = try Player.fetchAll(db)
}
Codable 记录根据其自身 Encodable 和 Decodable 协议的实现来编码和解码其属性。然而,数据库有特定的要求
DatabaseValueConvertible
协议的 值)。有关 Codable 记录的更多信息,请参见
💡 提示:查看 Demo Applications 以获取使用 Codable 记录的示例代码。
当 Codable 记录包含一个不是简单值(Bool、Int、String、Date、Swift 枚举等)的属性时,该值被编码和解码为 JSON 字符串。例如
enum AchievementColor: String, Codable {
case bronze, silver, gold
}
struct Achievement: Codable {
var name: String
var color: AchievementColor
}
struct Player: Codable, FetchableRecord, PersistableRecord {
var name: String
var score: Int
var achievements: [Achievement] // stored in a JSON column
}
try dbQueue.write { db in
// INSERT INTO player (name, score, achievements)
// VALUES (
// 'Arthur',
// 100,
// '[{"color":"gold","name":"Use Codable Records"}]')
let achievement = Achievement(name: "Use Codable Records", color: .gold)
let player = Player(name: "Arthur", score: 100, achievements: [achievement])
try player.insert(db)
}
GRDB 使用标准的 Foundation 的 JSONDecoder 和 JSONEncoder。默认情况下,Data 值使用 .base64
策略处理,Date 使用 .millisecondsSince1970
策略处理,不符合规范的浮点数使用 .throw
策略处理。
你可以通过实现这些方法来自定义 JSON 格式
protocol FetchableRecord {
static func databaseJSONDecoder(for column: String) -> JSONDecoder
}
protocol EncodableRecord {
static func databaseJSONEncoder(for column: String) -> JSONEncoder
}
💡 提示:请确保设置 JSONEncoder 的
sortedKeys
选项。此选项可确保 JSON 输出的稳定性。这种稳定性是 记录比较 按预期工作以及数据库观察工具(如 ValueObservation)准确识别已更改记录所必需的。
默认情况下,Codable Records 将它们的值存储到与其编码键匹配的数据库列中:teamID
属性存储到 teamID
列中。
可以覆盖此行为,以便您可以例如将 teamID
属性存储到 team_id
列中。
protocol FetchableRecord {
static var databaseColumnDecodingStrategy: DatabaseColumnDecodingStrategy { get }
}
protocol EncodableRecord {
static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
}
有关所有可用策略,请参阅 DatabaseColumnDecodingStrategy 和 DatabaseColumnEncodingStrategy。
默认情况下,Codable Records 将其 Data 属性编码和解码为 blobs,并将 Date 和 UUID 属性按照常规 Date 和 DateComponents 以及 UUID 章节中的描述进行编码和解码。
总结:日期以 "YYYY-MM-DD HH:MM:SS.SSS" 格式在 UTC 时区中进行编码,并解码各种日期格式和时间戳。 UUID 将它们自身编码为 16 字节的数据 blobs,并解码 16 字节的数据 blobs 和字符串,例如 "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"。
这些行为可以被覆盖。
protocol FetchableRecord {
static func databaseDataDecodingStrategy(for column: String) -> DatabaseDataDecodingStrategy
static func databaseDateDecodingStrategy(for column: String) -> DatabaseDateDecodingStrategy
}
protocol EncodableRecord {
static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy
static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy
static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy
}
有关所有可用策略,请参阅 DatabaseDataDecodingStrategy、DatabaseDateDecodingStrategy、DatabaseDataEncodingStrategy、DatabaseDateEncodingStrategy 和 DatabaseUUIDEncodingStrategy。
没有 UUID 解码的自定义,因为 UUID 已经可以解码其所有编码的变体(16 字节的 blobs 和 uuid 字符串,包括大写和小写)。
自定义编码策略适用
fetchOne(_:id:)
、filter(id:)
、deleteAll(_:keys:)
等。它们不适用于基于数据、日期或 uuid 值的其他请求。
因此,请确保在您的请求中正确编码这些值。例如
struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable {
// UUIDs are stored as strings
static func databaseUUIDEncodingStrategy(for column: String) -> DatabaseUUIDEncodingStrategy {
.uppercaseString
}
var id: UUID
...
}
try dbQueue.write { db in
let uuid = UUID()
let player = Player(id: uuid, ...)
// OK: inserts a player in the database, with a string uuid
try player.insert(db)
// OK: performs a string-based query, finds the inserted player
_ = try Player.filter(id: uuid).fetchOne(db)
// NOT OK: performs a blob-based query, fails to find the inserted player
_ = try Player.filter(Column("id") == uuid).fetchOne(db)
// OK: performs a string-based query, finds the inserted player
_ = try Player.filter(Column("id") == uuid.uuidString).fetchOne(db)
}
您的 Codable Records 可以存储在数据库中,但它们也可能具有其他用途。在这种情况下,您可能需要根据上下文自定义其 Decodable.init(from:)
和 Encodable.encode(to:)
的实现。
提供此类上下文的标准方法是 userInfo
字典。 实现这些属性
protocol FetchableRecord {
static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get }
}
protocol EncodableRecord {
static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
}
例如,这是一个自定义其解码的 Player 类型
// A key that holds a decoder's name
let decoderName = CodingUserInfoKey(rawValue: "decoderName")!
struct Player: FetchableRecord, Decodable {
init(from decoder: Decoder) throws {
// Print the decoder name
let decoderName = decoder.userInfo[decoderName] as? String
print("Decoded from \(decoderName ?? "unknown decoder")")
...
}
}
您可以从 JSON 进行特定解码...
// prints "Decoded from JSON"
let decoder = JSONDecoder()
decoder.userInfo = [decoderName: "JSON"]
let player = try decoder.decode(Player.self, from: jsonData)
...以及从数据库行进行另一种解码
extension Player: FetchableRecord {
static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] {
[decoderName: "database row"]
}
}
// prints "Decoded from database row"
let player = try Player.fetchOne(db, ...)
注意:请确保
databaseDecodingUserInfo
和databaseEncodingUserInfo
属性被显式声明为[CodingUserInfoKey: Any]
。如果不是,Swift 编译器可能会静默地忽略协议要求,从而导致粘滞的空 userInfo。
Codable 类型被授予 CodingKeys 枚举。您可以使用它们来安全地定义数据库列。
struct Player: Codable {
var id: Int64
var name: String
var score: Int
}
extension Player: FetchableRecord, PersistableRecord {
enum Columns {
static let id = Column(CodingKeys.id)
static let name = Column(CodingKeys.name)
static let score = Column(CodingKeys.score)
}
}
有关更多信息,请参阅 查询接口 和 记录类型设计的推荐实践。
采用 EncodableRecord 协议的记录可以与其他记录或其以前的版本进行比较。
这有助于避免在记录未被编辑时进行代价高昂的 UPDATE 语句。
updateChanges
方法仅对已更改的列执行数据库更新(如果记录没有更改,则不执行任何操作)。
updateChanges(_:from:)
此方法允许您比较两个记录。
if let oldPlayer = try Player.fetchOne(db, id: 42) {
var newPlayer = oldPlayer
newPlayer.score = 100
if try newPlayer.updateChanges(db, from: oldPlayer) {
print("player was modified, and updated in the database")
} else {
print("player was not modified, and database was not hit")
}
}
updateChanges(_:modify:)
此方法允许您就地更新记录。
if var player = try Player.fetchOne(db, id: 42) {
let modified = try player.updateChanges(db) {
$0.score = 100
}
if modified {
print("player was modified, and updated in the database")
} else {
print("player was not modified, and database was not hit")
}
}
此方法返回两个记录是否具有相同的数据库表示。
let oldPlayer: Player = ...
var newPlayer: Player = ...
if newPlayer.databaseEquals(oldPlayer) == false {
try newPlayer.save(db)
}
注意:比较是在记录的数据库表示上执行的。 只要您的记录类型采用 EncodableRecord 协议,您就不需要关心 Equatable。
databaseChanges(from:)
返回两个记录之间差异的字典。
let oldPlayer = Player(id: 1, name: "Arthur", score: 100)
let newPlayer = Player(id: 1, name: "Arthur", score: 1000)
for (column, oldValue) in try newPlayer.databaseChanges(from: oldPlayer) {
print("\(column) was \(oldValue)")
}
// prints "score was 100"
对于同步数据库表的内容与 JSON 有效负载的高效算法,请查看 groue/SortedDifference。
GRDB 记录带有许多默认行为,这些行为旨在适应大多数情况。 许多这些默认值可以根据您的特定需求进行自定义。
player.insert(db)
)时会发生什么。INSERT OR REPLACE
查询,通常定义当持久化方法违反唯一索引时会发生什么。Player.fetchAll(db)
)选择哪些列。Codable Records 有一些额外的选项。
插入和更新可能会产生冲突:例如,查询可能尝试插入违反唯一索引的重复行。
这些冲突通常以错误结束。 然而,SQLite 允许您更改默认行为,并使用特定策略处理冲突。 例如,INSERT OR REPLACE
语句使用 "replace" 策略处理冲突,该策略替换冲突的行而不是抛出错误。
五种不同的策略是:abort(默认)、replace、rollback、fail 和 ignore。
SQLite 允许您在两个不同的位置指定冲突策略
在数据库表的定义中
// CREATE TABLE player (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// email TEXT UNIQUE ON CONFLICT REPLACE
// )
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("email", .text).unique(onConflict: .replace) // <--
}
// Despite the unique index on email, both inserts succeed.
// The second insert replaces the first row:
try db.execute(sql: "INSERT INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
try db.execute(sql: "INSERT INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
在每个修改查询中
// CREATE TABLE player (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// email TEXT UNIQUE
// )
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("email", .text).unique()
}
// Again, despite the unique index on email, both inserts succeed.
try db.execute(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
try db.execute(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
当您想在查询级别处理冲突时,请在采用 PersistableRecord 协议的类型中指定自定义 persistenceConflictPolicy
。 它将更改由 insert
、update
和 save
持久化方法运行的 INSERT 和 UPDATE 查询。
protocol MutablePersistableRecord {
/// The policy that handles SQLite conflicts when records are
/// inserted or updated.
///
/// This property is optional: its default value uses the ABORT
/// policy for both insertions and updates, so that GRDB generate
/// regular INSERT and UPDATE queries.
static var persistenceConflictPolicy: PersistenceConflictPolicy { get }
}
struct Player : MutablePersistableRecord {
static let persistenceConflictPolicy = PersistenceConflictPolicy(
insert: .replace,
update: .replace)
}
// INSERT OR REPLACE INTO player (...) VALUES (...)
try player.insert(db)
注意:如果为插入指定
ignore
策略,则在插入失败的情况下,将使用一些随机 ID 调用didInsert
回调。 您可以使用insertAndFetch
检测插入失败。// How to detect failed `INSERT OR IGNORE`: // INSERT OR IGNORE INTO player ... RETURNING * do { let insertedPlayer = try player.insertAndFetch(db) { // Succesful insertion catch RecordError.recordNotFound { // Failed insertion due to IGNORE policy }注意:
replace
策略可能需要删除行,以便插入和更新可以成功。 这些删除不会报告给 事务观察者(这可能会在 SQLite 的未来版本中更改)。
一些 GRDB 用户最终发现 FetchableRecord 协议不适合所有情况。 FetchableRecord 不能很好地处理的用例包括:
您的应用程序需要多态行解码:它根据数据库行中包含的值解码某种类型。
您的应用程序需要使用上下文解码行:每个解码的值都应该使用一些不来自数据库的额外值进行初始化。
由于 FetchableRecord 不能很好地处理这些用例,因此不要尝试在此协议之上实现它们:您只会与框架作斗争。
我们将在下面展示如何为以下数据库表声明记录类型
try dbQueue.write { db in
try db.create(table: "place") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
t.column("isFavorite", .boolean).notNull().defaults(to: false)
t.column("longitude", .double).notNull()
t.column("latitude", .double).notNull()
}
}
以下三个示例中的每一个都是正确的。 您将根据您的个人喜好和应用程序的要求选择一个或另一个。
这是定义记录类型最短的方法。
有关更多信息,请参阅 记录协议概述 和 Codable Records。
struct Place: Codable {
var id: Int64?
var title: String
var isFavorite: Bool
private var latitude: CLLocationDegrees
private var longitude: CLLocationDegrees
var coordinate: CLLocationCoordinate2D {
get {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
set {
latitude = newValue.latitude
longitude = newValue.longitude
}
}
}
// SQL generation
extension Place: TableRecord {
/// The table columns
enum Columns {
static let id = Column(CodingKeys.id)
static let title = Column(CodingKeys.title)
static let isFavorite = Column(CodingKeys.isFavorite)
static let latitude = Column(CodingKeys.latitude)
static let longitude = Column(CodingKeys.longitude)
}
}
// Fetching methods
extension Place: FetchableRecord { }
// Persistence methods
extension Place: MutablePersistableRecord {
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
有关更多信息,请参阅 记录协议概述。
struct Place {
var id: Int64?
var title: String
var isFavorite: Bool
var coordinate: CLLocationCoordinate2D
}
// SQL generation
extension Place: TableRecord {
/// The table columns
enum Columns: String, ColumnExpression {
case id, title, isFavorite, latitude, longitude
}
}
// Fetching methods
extension Place: FetchableRecord {
/// Creates a record from a database row
init(row: Row) {
id = row[Columns.id]
title = row[Columns.title]
isFavorite = row[Columns.isFavorite]
coordinate = CLLocationCoordinate2D(
latitude: row[Columns.latitude],
longitude: row[Columns.longitude])
}
}
// Persistence methods
extension Place: MutablePersistableRecord {
/// The values persisted in the database
func encode(to container: inout PersistenceContainer) {
container[Columns.id] = id
container[Columns.title] = title
container[Columns.isFavorite] = isFavorite
container[Columns.latitude] = coordinate.latitude
container[Columns.longitude] = coordinate.longitude
}
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
此结构体从标准 Encodable 协议派生其持久化方法(参见 Codable Records),但通过使用数字索引访问数据库列来执行优化的行解码。
有关更多信息,请参阅 记录协议概述。
struct Place: Encodable {
var id: Int64?
var title: String
var isFavorite: Bool
private var latitude: CLLocationDegrees
private var longitude: CLLocationDegrees
var coordinate: CLLocationCoordinate2D {
get {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
set {
latitude = newValue.latitude
longitude = newValue.longitude
}
}
}
// SQL generation
extension Place: TableRecord {
/// The table columns
enum Columns {
static let id = Column(CodingKeys.id)
static let title = Column(CodingKeys.title)
static let isFavorite = Column(CodingKeys.isFavorite)
static let latitude = Column(CodingKeys.latitude)
static let longitude = Column(CodingKeys.longitude)
}
/// Arrange the selected columns and lock their order
static var databaseSelection: [any SQLSelectable] {
[
Columns.id,
Columns.title,
Columns.favorite,
Columns.latitude,
Columns.longitude,
]
}
}
// Fetching methods
extension Place: FetchableRecord {
/// Creates a record from a database row
init(row: Row) {
// For high performance, use numeric indexes that match the
// order of Place.databaseSelection
id = row[0]
title = row[1]
isFavorite = row[2]
coordinate = CLLocationCoordinate2D(
latitude: row[3],
longitude: row[4])
}
}
// Persistence methods
extension Place: MutablePersistableRecord {
// Update auto-incremented id upon successful insertion
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
查询接口允许您编写纯 Swift 代码而不是 SQL。
try dbQueue.write { db in
// Update database schema
try db.create(table: "wine") { t in ... }
// Fetch records
let wines = try Wine
.filter(originColumn == "Burgundy")
.order(priceColumn)
.fetchAll(db)
// Count
let count = try Wine
.filter(colorColumn == Color.red)
.fetchCount(db)
// Update
try Wine
.filter(originColumn == "Burgundy")
.updateAll(db, priceColumn *= 0.75)
// Delete
try Wine
.filter(corkedColumn == true)
.deleteAll(db)
}
您需要打开一个 数据库连接 才能查询数据库。
请记住,查询接口无法生成所有可能的 SQL 查询。 您也可能更喜欢编写 SQL,这没问题。 从小片段到完整查询,欢迎您的 SQL 技能。
try dbQueue.write { db in
// Update database schema (with SQL)
try db.execute(sql: "CREATE TABLE wine (...)")
// Fetch records (with SQL)
let wines = try Wine.fetchAll(db,
sql: "SELECT * FROM wine WHERE origin = ? ORDER BY price",
arguments: ["Burgundy"])
// Count (with an SQL snippet)
let count = try Wine
.filter(sql: "color = ?", arguments: [Color.red])
.fetchCount(db)
// Update (with SQL)
try db.execute(sql: "UPDATE wine SET price = price * 0.75 WHERE origin = 'Burgundy'")
// Delete (with SQL)
try db.execute(sql: "DELETE FROM wine WHERE corked")
}
所以不要错过 SQL API。
注意:生成的 SQL 可能会在 GRDB 版本之间更改,恕不另行通知:不要让您的应用程序依赖于任何特定的 SQL 输出。
📖 QueryInterfaceRequest
, Table
查询接口请求允许您从数据库中获取值。
let request = Player.filter(emailColumn != nil).order(nameColumn)
let players = try request.fetchAll(db) // [Player]
let count = try request.fetchCount(db) // Int
查询接口请求通常从采用 TableRecord
协议的类型开始。
struct Player: TableRecord { ... }
// The request for all players:
let request = Player.all()
let players = try request.fetchAll(db) // [Player]
当您无法使用记录类型时,请使用 Table
。
// The request for all rows from the player table:
let table = Table("player")
let request = table.all()
let rows = try request.fetchAll(db) // [Row]
// The request for all players from the player table:
let table = Table<Player>("player")
let request = table.all()
let players = try request.fetchAll(db) // [Player]
注意:以下文档中的所有示例都使用记录类型,但您可以随时替换为
Table
。
接下来,声明您想要用于过滤或排序的表列。
let idColumn = Column("id")
let nameColumn = Column("name")
如果您愿意,也可以声明列枚举。
// Columns.id and Columns.name can be used just as
// idColumn and nameColumn declared above.
enum Columns: String, ColumnExpression {
case id
case name
}
现在,您可以使用以下方法构建请求:all
、none
、select
、distinct
、filter
、matching
、group
、having
、order
、reversed
、limit
、joining
、including
、with
。 所有这些方法都返回另一个请求,您可以通过应用另一个方法来进一步优化它:Player.select(...).filter(...).order(...)
。
// SELECT * FROM player
Player.all()
默认情况下,会选择所有列。请参阅请求选择的列。
select(...)
和 select(..., as:)
定义选择的列。请参阅请求选择的列。
// SELECT name FROM player
Player.select(nameColumn, as: String.self)
selectID()
在 可识别记录 上可用。它支持所有具有单列主键的表
// SELECT id FROM player
Player.selectID()
// SELECT id FROM player WHERE name IS NOT NULL
Player.filter(nameColumn != nil).selectID()
annotated(with: expression...)
扩展了选择。
// SELECT *, (score + bonus) AS total FROM player
Player.annotated(with: (scoreColumn + bonusColumn).forKey("total"))
annotated(with: aggregate)
使用关联聚合扩展了选择。
// SELECT team.*, COUNT(DISTINCT player.id) AS playerCount
// FROM team
// LEFT JOIN player ON player.teamId = team.id
// GROUP BY team.id
Team.annotated(with: Team.players.count)
annotated(withRequired: association)
和 annotated(withOptional: association)
使用关联扩展了选择。
// SELECT player.*, team.color
// FROM player
// JOIN team ON team.id = player.teamId
Player.annotated(withRequired: Player.team.select(colorColumn))
distinct()
执行去重。
// SELECT DISTINCT name FROM player
Player.select(nameColumn, as: String.self).distinct()
filter(expression)
应用条件。
// SELECT * FROM player WHERE id IN (1, 2, 3)
Player.filter([1,2,3].contains(idColumn))
// SELECT * FROM player WHERE (name IS NOT NULL) AND (height > 1.75)
Player.filter(nameColumn != nil && heightColumn > 1.75)
filter(id:)
和 filter(ids:)
是在 可识别记录 上可用的类型安全方法
// SELECT * FROM player WHERE id = 1
Player.filter(id: 1)
// SELECT * FROM country WHERE isoCode IN ('FR', 'US')
Country.filter(ids: ["FR", "US"])
filter(key:)
和 filter(keys:)
对主键和唯一键应用条件
// SELECT * FROM player WHERE id = 1
Player.filter(key: 1)
// SELECT * FROM country WHERE isoCode IN ('FR', 'US')
Country.filter(keys: ["FR", "US"])
// SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])
// SELECT * FROM player WHERE email = 'arthur@example.com'
Player.filter(key: ["email": "arthur@example.com"])
matching(pattern)
(FTS3, FTS5) 执行全文搜索。
// SELECT * FROM document WHERE document MATCH 'sqlite database'
let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
Document.matching(pattern)
当 pattern 为 nil 时,将没有行匹配。
group(expression, ...)
对行进行分组。
// SELECT name, MAX(score) FROM player GROUP BY name
Player
.select(nameColumn, max(scoreColumn))
.group(nameColumn)
having(expression)
对分组后的行应用条件。
// SELECT team, MAX(score) FROM player GROUP BY team HAVING MIN(score) >= 1000
Player
.select(teamColumn, max(scoreColumn))
.group(teamColumn)
.having(min(scoreColumn) >= 1000)
having(aggregate)
根据关联聚合对分组后的行应用条件。
// SELECT team.*
// FROM team
// LEFT JOIN player ON player.teamId = team.id
// GROUP BY team.id
// HAVING COUNT(DISTINCT player.id) >= 5
Team.having(Team.players.count >= 5)
// SELECT * FROM player ORDER BY name
Player.order(nameColumn)
// SELECT * FROM player ORDER BY score DESC, name
Player.order(scoreColumn.desc, nameColumn)
SQLite 认为 NULL 值比任何其他值都小,用于排序目的。 因此,NULL 自然出现在升序排序的开头和降序排序的结尾。 使用自定义 SQLite 构建,可以使用.ascNullsLast
和.descNullsFirst
更改此行为
// SELECT * FROM player ORDER BY score ASC NULLS LAST
Player.order(nameColumn.ascNullsLast)
每次调用 order
都会清除任何先前的排序
// SELECT * FROM player ORDER BY name
Player.order(scoreColumn).order(nameColumn)
reversed()
反转最终的排序。
// SELECT * FROM player ORDER BY score ASC, name DESC
Player.order(scoreColumn.desc, nameColumn).reversed()
如果尚未指定任何排序,则此方法无效
// SELECT * FROM player
Player.all().reversed()
limit(limit, offset: offset)
限制和分页结果。
// SELECT * FROM player LIMIT 5
Player.limit(5)
// SELECT * FROM player LIMIT 5 OFFSET 10
Player.limit(5, offset: 10)
joining(required:)
、joining(optional:)
、including(required:)
、including(optional:)
和 including(all:)
通过关联获取和连接记录。
// SELECT player.*, team.*
// FROM player
// JOIN team ON team.id = player.teamId
Player.including(required: Player.team)
// WITH ... SELECT * FROM player
let cte = CommonTableExpression(...)
Player.with(cte)
其他涉及主键的请求
selectPrimaryKey(as:)
选择主键。
// SELECT id FROM player
Player.selectPrimaryKey(as: Int64.self) // QueryInterfaceRequest<Int64>
// SELECT code FROM country
Country.selectPrimaryKey(as: String.self) // QueryInterfaceRequest<String>
// SELECT citizenId, countryCode FROM citizenship
Citizenship.selectPrimaryKey(as: Row.self) // QueryInterfaceRequest<Row>
orderByPrimaryKey()
按主键排序。
// SELECT * FROM player ORDER BY id
Player.orderByPrimaryKey()
// SELECT * FROM country ORDER BY code
Country.orderByPrimaryKey()
// SELECT * FROM citizenship ORDER BY citizenId, countryCode
Citizenship.orderByPrimaryKey()
groupByPrimaryKey()
按主键对行进行分组。
您可以通过链接这些方法来优化请求
// SELECT * FROM player WHERE (email IS NOT NULL) ORDER BY name
Player.order(nameColumn).filter(emailColumn != nil)
select
、order
、group
和 limit
方法会忽略并替换先前应用的 selection、orderings、grouping 和 limits。 相反,filter
、matching
和 having
方法会扩展查询
Player // SELECT * FROM player
.filter(nameColumn != nil) // WHERE (name IS NOT NULL)
.filter(emailColumn != nil) // AND (email IS NOT NULL)
.order(nameColumn) // - ignored -
.reversed() // - ignored -
.order(scoreColumn) // ORDER BY score
.limit(20, offset: 40) // - ignored -
.limit(10) // LIMIT 10
也接受原始 SQL 代码片段,并附带最终的参数
// SELECT DATE(creationDate), COUNT(*) FROM player WHERE name = 'Arthur' GROUP BY date(creationDate)
Player
.select(sql: "DATE(creationDate), COUNT(*)")
.filter(sql: "name = ?", arguments: ["Arthur"])
.group(sql: "DATE(creationDate)")
默认情况下,查询接口请求选择所有列
// SELECT * FROM player
struct Player: TableRecord { ... }
let request = Player.all()
// SELECT * FROM player
let table = Table("player")
let request = table.all()
可以为每个单独的请求更改选择,或者对于基于记录的请求,可以为从此记录类型构建的所有请求更改选择。
select(...)
和 select(..., as:)
方法更改单个请求的选择(有关详细信息,请参阅从请求中获取)
let request = Player.select(max(Column("score")))
let maxScore = try Int.fetchOne(db, request) // Int?
let request = Player.select(max(Column("score")), as: Int.self)
let maxScore = try request.fetchOne(db) // Int?
记录类型的默认选择由 databaseSelection
属性控制。 例如
// Select a limited set of columns
struct RestrictedPlayer : TableRecord {
static let databaseTableName = "player"
static var databaseSelection: [any SQLSelectable] {
[Column("id"), Column("name")]
}
}
// SELECT id, name FROM player
let request = RestrictedPlayer.all()
// Select all but a few columns
struct Player : TableRecord {
static var databaseSelection: [any SQLSelectable] {
[.allColumns(excluding: ["generatedColumn"])]
}
}
// SELECT id, name FROM player
let request = RestrictedPlayer.all()
// Select all columns are more
struct ExtendedPlayer : TableRecord {
static let databaseTableName = "player"
static var databaseSelection: [any SQLSelectable] {
[.allColumns, .rowID]
}
}
// SELECT *, rowid FROM player
let request = ExtendedPlayer.all()
注意:请确保将
databaseSelection
属性显式声明为[any SQLSelectable]
。 如果没有,Swift 编译器可能会静默地忽略协议要求,从而导致粘滞的SELECT *
请求。 要验证您的设置,请参阅如何将请求打印为 SQL?常见问题解答。
使用从您的 Swift 代码构建的 SQL 表达式来提供请求
GRDB 附带了许多 SQLite 内置运算符的 Swift 版本,如下所示。 但并非全部:有关添加对缺失的 SQL 运算符的支持的方法,请参阅在查询接口请求中嵌入 SQL。
=
, <>
, <
, <=
, >
, >=
, IS
, IS NOT
比较运算符基于 Swift 运算符 ==
、!=
、===
、!==
、<
、<=
、>
、>=
// SELECT * FROM player WHERE (name = 'Arthur')
Player.filter(nameColumn == "Arthur")
// SELECT * FROM player WHERE (name IS NULL)
Player.filter(nameColumn == nil)
// SELECT * FROM player WHERE (score IS 1000)
Player.filter(scoreColumn === 1000)
// SELECT * FROM rectangle WHERE width < height
Rectangle.filter(widthColumn < heightColumn)
支持子查询
// SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
let maximumScore = Player.select(max(scoreColumn))
Player.filter(scoreColumn == maximumScore)
// SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
let maximumScore = SQLRequest("SELECT max(score) FROM player")
Player.filter(scoreColumn == maximumScore)
注意:默认情况下,SQLite 字符串比较区分大小写且不感知 Unicode。 如果您需要更多控制,请参阅字符串比较。
*
, /
, +
, -
SQLite 算术运算符源自其 Swift 等效项
// SELECT ((temperature * 1.8) + 32) AS fahrenheit FROM planet
Planet.select((temperatureColumn * 1.8 + 32).forKey("fahrenheit"))
注意:像
nameColumn + "rrr"
这样的表达式将被 SQLite 解释为数值加法(结果很有趣),而不是字符串连接。 请参阅下面的concat
运算符。
当您想使用 +
或 *
运算符连接一系列表达式时,请使用 joined(operator:)
// SELECT score + bonus + 1000 FROM player
let values = [
scoreColumn,
bonusColumn,
1000.databaseValue]
Player.select(values.joined(operator: .add))
请注意上面的示例中如何连接原始值:1000.databaseValue
。 普通的 1000
将无法编译。
当序列为空时,joined(operator: .add)
返回 0,joined(operator: .multiply)
返回 1。
&
, |
, ~
, <<
, >>
按位运算(按位与、或、非、左移、右移)源自其 Swift 等效项
// SELECT mask & 2 AS isRocky FROM planet
Planet.select((Column("mask") & 2).forKey("isRocky"))
||
连接多个字符串
// SELECT firstName || ' ' || lastName FROM player
Player.select([firstNameColumn, " ".databaseValue, lastNameColumn].joined(operator: .concat))
请注意上面的示例中如何连接原始字符串:" ".databaseValue
。 普通的 " "
将无法编译。
当序列为空时,joined(operator: .concat)
返回空字符串。
AND
, OR
, NOT
SQL 逻辑运算符源自 Swift &&
、||
和 !
// SELECT * FROM player WHERE ((NOT verified) OR (score < 1000))
Player.filter(!verifiedColumn || scoreColumn < 1000)
当您想使用 AND
或 OR
运算符连接一系列表达式时,请使用 joined(operator:)
// SELECT * FROM player WHERE (verified AND (score >= 1000) AND (name IS NOT NULL))
let conditions = [
verifiedColumn,
scoreColumn >= 1000,
nameColumn != nil]
Player.filter(conditions.joined(operator: .and))
当序列为空时,joined(operator: .and)
返回 true,joined(operator: .or)
返回 false
// SELECT * FROM player WHERE 1
Player.filter([].joined(operator: .and))
// SELECT * FROM player WHERE 0
Player.filter([].joined(operator: .or))
BETWEEN
, IN
, NOT IN
要检查是否包含在 Swift 序列(数组、集合、范围...)中,请调用 contains
方法
// SELECT * FROM player WHERE id IN (1, 2, 3)
Player.filter([1, 2, 3].contains(idColumn))
// SELECT * FROM player WHERE id NOT IN (1, 2, 3)
Player.filter(![1, 2, 3].contains(idColumn))
// SELECT * FROM player WHERE score BETWEEN 0 AND 1000
Player.filter((0...1000).contains(scoreColumn))
// SELECT * FROM player WHERE (score >= 0) AND (score < 1000)
Player.filter((0..<1000).contains(scoreColumn))
// SELECT * FROM player WHERE initial BETWEEN 'A' AND 'N'
Player.filter(("A"..."N").contains(initialColumn))
// SELECT * FROM player WHERE (initial >= 'A') AND (initial < 'N')
Player.filter(("A"..<"N").contains(initialColumn))
要检查是否包含在子查询中,也请调用 contains
方法
// SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
let selectedPlayerIds = PlayerSelection.select(playerIdColumn)
Player.filter(selectedPlayerIds.contains(idColumn))
// SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
let selectedPlayerIds = SQLRequest("SELECT playerId FROM playerSelection")
Player.filter(selectedPlayerIds.contains(idColumn))
要检查是否包含在公用表表达式中,也请调用 contains
方法
// WITH selectedName AS (...)
// SELECT * FROM player WHERE name IN selectedName
let cte = CommonTableExpression(named: "selectedName", ...)
Player
.with(cte)
.filter(cte.contains(nameColumn))
注意:默认情况下,SQLite 字符串比较区分大小写且不感知 Unicode。 如果您需要更多控制,请参阅字符串比较。
EXISTS
, NOT EXISTS
要检查子查询是否会返回行,请调用 exists
方法
// Teams that have at least one other player
//
// SELECT * FROM team
// WHERE EXISTS (SELECT * FROM player WHERE teamID = team.id)
let teamAlias = TableAlias()
let player = Player.filter(Column("teamID") == teamAlias[Column("id")])
let teams = Team.aliased(teamAlias).filter(player.exists())
// Teams that have no player
//
// SELECT * FROM team
// WHERE NOT EXISTS (SELECT * FROM player WHERE teamID = team.id)
let teams = Team.aliased(teamAlias).filter(!player.exists())
在上面的示例中,您使用 TableAlias
以便子查询可以引用另一个表中的列。
在下一个示例中,它涉及同一个表两次,表别名需要使用 TableAlias(name:)
进行显式消除歧义
// Players who coach at least one other player
//
// SELECT coach.* FROM player coach
// WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
let coachAlias = TableAlias(name: "coach")
let coachedPlayer = Player.filter(Column("coachId") == coachAlias[Column("id")])
let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())
最后,子查询也可以表示为 SQL,并使用SQL 插值
// SELECT coach.* FROM player coach
// WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
let coachedPlayer = SQLRequest("SELECT * FROM player WHERE coachId = \(coachAlias[Column("id")])")
let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())
LIKE
SQLite LIKE 运算符可用作 like
方法
// SELECT * FROM player WHERE (email LIKE '%@example.com')
Player.filter(emailColumn.like("%@example.com"))
// SELECT * FROM book WHERE (title LIKE '%10\%%' ESCAPE '\')
Player.filter(emailColumn.like("%10\\%%", escape: "\\"))
注意:SQLite LIKE 运算符不区分大小写,但不感知 Unicode。 例如,表达式
'a' LIKE 'A'
为 true,但'æ' LIKE 'Æ'
为 false。
MATCH
全文 MATCH 运算符可通过 FTS3Pattern(对于 FTS3 和 FTS4 表)和 FTS5Pattern(对于 FTS5)获得
FTS3 和 FTS4
let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
// SELECT * FROM document WHERE document MATCH 'sqlite database'
Document.matching(pattern)
// SELECT * FROM document WHERE content MATCH 'sqlite database'
Document.filter(contentColumn.match(pattern))
FTS5
let pattern = FTS5Pattern(matchingAllTokensIn: "SQLite database")
// SELECT * FROM document WHERE document MATCH 'sqlite database'
Document.matching(pattern)
AS
要为表达式指定别名,请使用 forKey
方法
// SELECT (score + bonus) AS total
// FROM player
Player.select((Column("score") + Column("bonus")).forKey("total"))
如果您需要在请求的另一个位置引用此别名列,请使用分离的列
// SELECT (score + bonus) AS total
// FROM player
// ORDER BY total
Player
.select((Column("score") + Column("bonus")).forKey("total"))
.order(Column("total").detached)
与 Column("total")
不同,分离的列 Column("total").detached
永远不会与“player”表关联,因此在生成的 SQL 中始终呈现为 total
,即使请求通过关联或公用表表达式涉及其他表。
GRDB 附带了许多 SQLite 内置函数的 Swift 版本,如下所示。 但并非全部:有关添加对缺失的 SQL 函数的支持的方法,请参阅在查询接口请求中嵌入 SQL。
ABS
, AVG
, COALESCE
, COUNT
, DATETIME
, JULIANDAY
, LENGTH
, MAX
, MIN
, SUM
, TOTAL
这些函数基于 Swift 的 abs
, average
, coalesce
, count
, dateTime
, julianDay
, length
, max
, min
, sum
, 和 total
函数。
// SELECT MIN(score), MAX(score) FROM player
Player.select(min(scoreColumn), max(scoreColumn))
// SELECT COUNT(name) FROM player
Player.select(count(nameColumn))
// SELECT COUNT(DISTINCT name) FROM player
Player.select(count(distinct: nameColumn))
// SELECT JULIANDAY(date, 'start of year') FROM game
Game.select(julianDay(dateColumn, .startOfYear))
有关 dateTime
和 julianDay
函数的更多信息,请参阅 日期和时间函数。
CAST
使用 Swift 的 cast
函数。
// SELECT (CAST(wins AS REAL) / games) AS successRate FROM player
Player.select((cast(winsColumn, as: .real) / gamesColumn).forKey("successRate"))
有关 SQLite 转换的更多信息,请参阅 CAST 表达式。
IFNULL
使用 Swift 的 ??
运算符。
// SELECT IFNULL(name, 'Anonymous') FROM player
Player.select(nameColumn ?? "Anonymous")
// SELECT IFNULL(name, email) FROM player
Player.select(nameColumn ?? emailColumn)
LOWER
, UPPER
查询接口不提供对这些 SQLite 函数的访问。并非反对它们,而是因为它们不支持 Unicode。
相反,GRDB 使用 SQL 函数扩展了 SQLite,这些函数调用 Swift 内置字符串函数 capitalized
, lowercased
, uppercased
, localizedCapitalized
, localizedLowercased
和 localizedUppercased
。
Player.select(nameColumn.uppercased())
注意:当比较字符串时,最好使用 排序规则。
let name: String = ... // Not recommended nameColumn.uppercased() == name.uppercased() // Better nameColumn.collating(.caseInsensitiveCompare) == name
自定义 SQL 函数和聚合
您可以应用自己的 自定义 SQL 函数和聚合。
let f = DatabaseFunction("f", ...)
// SELECT f(name) FROM player
Player.select(f.apply(nameColumn))
有时您可能需要使用 SQL 代码片段扩展您的查询接口请求。这可能是因为 GRDB 没有为某些 SQL 函数或运算符提供 Swift 接口,或者因为您想使用 GRDB 不支持的 SQLite 构造。
对可扩展性的支持是广泛的,但并非没有限制。查询接口请求构建的所有 SQL 查询都具有以下形状。如果您需要其他的东西,您将必须使用 原始 SQL 请求。
WITH ... -- 1
SELECT ... -- 2
FROM ... -- 3
JOIN ... -- 4
WHERE ... -- 5
GROUP BY ... -- 6
HAVING ... -- 7
ORDER BY ... -- 8
LIMIT ... -- 9
WITH ...
: 请参阅 公共表表达式。
SELECT ...
选择可以作为原始 SQL 提供。
// SELECT IFNULL(name, 'O''Brien'), score FROM player
let request = Player.select(sql: "IFNULL(name, 'O''Brien'), score")
// SELECT IFNULL(name, 'O''Brien'), score FROM player
let defaultName = "O'Brien"
let request = Player.select(sql: "IFNULL(name, ?), score", arguments: [suffix])
选择可以使用 SQL 插值提供。
// SELECT IFNULL(name, 'O''Brien'), score FROM player
let defaultName = "O'Brien"
let request = Player.select(literal: "IFNULL(name, \(defaultName)), score")
选择可以使用 Swift 和 SQL 插值的混合方式提供。
// SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
let defaultName = "O'Brien"
let displayName: SQL = "IFNULL(\(Column("name")), \(defaultName)) AS displayName"
let request = Player.select(displayName, Column("score"))
当自定义 SQL 代码片段应表现为功能齐全的表达式,支持 +
Swift 运算符、forKey
别名方法和所有其他 SQL 运算符时,使用 SQL.sqlExpression
方法构建表达式字面量。
// SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
let defaultName = "O'Brien"
let displayName = SQL("IFNULL(\(Column("name")), \(defaultName))").sqlExpression
let request = Player.select(displayName.forKey("displayName"), Column("score"))
此类表达式字面量允许您构建可重用的 SQL 函数或运算符支持库,这些函数或运算符在查询接口中缺失。例如,您可以定义一个 Swift date
函数。
func date(_ value: some SQLSpecificExpressible) -> SQLExpression {
SQL("DATE(\(value))").sqlExpression
}
// SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
let request = Player.filter(date(Column("createdAt")) == "2020-01-23")
有关 SQLSpecificExpressible
和 SQLExpression
的更多信息,请参阅 查询接口组织。
FROM ...
:这里仅支持一个表。您无法自定义此 SQL 部分。
JOIN ...
:连接完全由 关联控制。您无法自定义此 SQL 部分。
WHERE ...
WHERE 子句可以作为原始 SQL 提供。
// SELECT * FROM player WHERE score >= 1000
let request = Player.filter(sql: "score >= 1000")
// SELECT * FROM player WHERE score >= 1000
let minScore = 1000
let request = Player.filter(sql: "score >= ?", arguments: [minScore])
WHERE 子句可以使用 SQL 插值提供。
// SELECT * FROM player WHERE score >= 1000
let minScore = 1000
let request = Player.filter(literal: "score >= \(minScore)")
WHERE 子句可以使用 Swift 和 SQL 插值的混合方式提供。
// SELECT * FROM player WHERE (score >= 1000) AND (team = 'red')
let minScore = 1000
let scoreCondition: SQL = "\(Column("score")) >= \(minScore)"
let request = Player.filter(scoreCondition && Column("team") == "red")
有关更多 SQL 插值示例,请参阅上面的 SELECT ...
。
GROUP BY ...
GROUP BY 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。
HAVING ...
HAVING 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。
ORDER BY ...
ORDER BY 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。
为了支持 desc
和 asc
查询接口运算符以及 reversed()
查询接口方法,您必须使用 SQL.sqlExpression
方法将您的排序作为表达式字面量提供。
// SELECT * FROM "player"
// ORDER BY (score + bonus) ASC, name DESC
let total = SQL("(score + bonus)").sqlExpression
let request = Player
.order(total.desc, Column("name"))
.reversed()
LIMIT ...
:使用 limit(_:offset:)
方法。您无法自定义此 SQL 部分。
一旦您有了一个请求,您就可以获取请求来源的记录。
// Some request based on `Player`
let request = Player.filter(...)... // QueryInterfaceRequest<Player>
// Fetch players:
try request.fetchCursor(db) // A Cursor of Player
try request.fetchAll(db) // [Player]
try request.fetchSet(db) // Set<Player>
try request.fetchOne(db) // Player?
例如
let allPlayers = try Player.fetchAll(db) // [Player]
let arthur = try Player.filter(nameColumn == "Arthur").fetchOne(db) // Player?
有关 fetchCursor
、fetchAll
、fetchSet
和 fetchOne
方法的信息,请参阅 获取方法。
有时您想获取其他值。.
最简单的方法是将请求用作所需类型的获取方法的参数。
// Fetch an Int
let request = Player.select(max(scoreColumn))
let maxScore = try Int.fetchOne(db, request) // Int?
// Fetch a Row
let request = Player.select(min(scoreColumn), max(scoreColumn))
let row = try Row.fetchOne(db, request)! // Row
let minScore = row[0] as Int?
let maxScore = row[1] as Int?
您还可以更改请求,使其知道它必须获取的类型。
使用 asRequest(of:)
,当您使用 关联时很有用。
struct BookInfo: FetchableRecord, Decodable {
var book: Book
var author: Author
}
// A request of BookInfo
let request = Book
.including(required: Book.author)
.asRequest(of: BookInfo.self)
let bookInfos = try dbQueue.read { db in
try request.fetchAll(db) // [BookInfo]
}
使用 select(..., as:)
,当您更改选择时很方便。
// A request of Int
let request = Player.select(max(scoreColumn), as: Int.self)
let maxScore = try dbQueue.read { db in
try request.fetchOne(db) // Int?
}
根据主键获取记录是一项常见的任务。
可识别的记录可以使用类型安全的方法 find(_:id:)
、fetchOne(_:id:)
、fetchAll(_:ids:)
和 fetchSet(_:ids:)
。
try Player.find(db, id: 1) // Player
try Player.fetchOne(db, id: 1) // Player?
try Country.fetchAll(db, ids: ["FR", "US"]) // [Countries]
所有记录类型都可以使用 find(_:key:)
、fetchOne(_:key:)
、fetchAll(_:keys:)
和 fetchSet(_:keys:)
,这些方法将条件应用于主键和唯一键。
try Player.find(db, key: 1) // Player
try Player.fetchOne(db, key: 1) // Player?
try Country.fetchAll(db, keys: ["FR", "US"]) // [Country]
try Player.fetchOne(db, key: ["email": "arthur@example.com"]) // Player?
try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"]) // Citizenship?
当表没有显式主键时,GRDB 使用 隐藏的 rowid
列。
// SELECT * FROM document WHERE rowid = 1
try Document.fetchOne(db, key: 1) // Document?
当您想要构建一个请求并计划稍后从中获取数据时,请使用 filter
方法。
let request = Player.filter(id: 1)
let request = Country.filter(ids: ["FR", "US"])
let request = Player.filter(key: ["email": "arthur@example.com"])
let request = Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])
您可以检查请求在数据库中是否具有匹配的行。
// Some request based on `Player`
let request = Player.filter(...)...
// Check for player existence:
let noSuchPlayer = try request.isEmpty(db) // Bool
您应该检查是否为空,而不是计数。
// Correct
let noSuchPlayer = try request.fetchCount(db) == 0
// Even better
let noSuchPlayer = try request.isEmpty(db)
您还可以检查给定的主键或唯一键是否存在于数据库中。
可识别的记录可以使用类型安全的方法 exists(_:id:)
。
try Player.exists(db, id: 1)
try Country.exists(db, id: "FR")
所有记录类型都可以使用 exists(_:key:)
,它可以检查主键和唯一键。
try Player.exists(db, key: 1)
try Country.exists(db, key: "FR")
try Player.exists(db, key: ["email": "arthur@example.com"])
try Citizenship.exists(db, key: ["citizenId": 1, "countryCode": "FR"])
您应该检查键是否存在,而不是获取记录并检查是否为 nil。
// Correct
let playerExists = try Player.fetchOne(db, id: 1) != nil
// Even better
let playerExists = try Player.exists(db, id: 1)
请求可以计数。 fetchCount()
方法返回获取请求将返回的行数。
// SELECT COUNT(*) FROM player
let count = try Player.fetchCount(db) // Int
// SELECT COUNT(*) FROM player WHERE email IS NOT NULL
let count = try Player.filter(emailColumn != nil).fetchCount(db)
// SELECT COUNT(DISTINCT name) FROM player
let count = try Player.select(nameColumn).distinct().fetchCount(db)
// SELECT COUNT(*) FROM (SELECT DISTINCT name, score FROM player)
let count = try Player.select(nameColumn, scoreColumn).distinct().fetchCount(db)
还可以选择和获取其他聚合值(请参阅 SQL 函数)。
let request = Player.select(max(scoreColumn))
let maxScore = try Int.fetchOne(db, request) // Int?
let request = Player.select(min(scoreColumn), max(scoreColumn))
let row = try Row.fetchOne(db, request)! // Row
let minScore = row[0] as Int?
let maxScore = row[1] as Int?
请求可以删除记录,使用 deleteAll()
方法。
// DELETE FROM player
try Player.deleteAll(db)
// DELETE FROM player WHERE team = 'red'
try Player
.filter(teamColumn == "red")
.deleteAll(db)
// DELETE FROM player ORDER BY score LIMIT 10
try Player
.order(scoreColumn)
.limit(10)
.deleteAll(db)
注意 删除方法在采用 TableRecord 协议的类型和
Table
上可用。struct Player: TableRecord { ... } try Player.deleteAll(db) // Fine try Table("player").deleteAll(db) // Just as fine
根据主键删除记录是一项常见的任务。
可识别的记录可以使用类型安全的方法 deleteOne(_:id:)
和 deleteAll(_:ids:)
。
try Player.deleteOne(db, id: 1)
try Country.deleteAll(db, ids: ["FR", "US"])
所有记录类型都可以使用 deleteOne(_:key:)
和 deleteAll(_:keys:)
,这些方法将条件应用于主键和唯一键。
try Player.deleteOne(db, key: 1)
try Country.deleteAll(db, keys: ["FR", "US"])
try Player.deleteOne(db, key: ["email": "arthur@example.com"])
try Citizenship.deleteOne(db, key: ["citizenId": 1, "countryCode": "FR"])
当表没有显式主键时,GRDB 使用 隐藏的 rowid
列。
// DELETE FROM document WHERE rowid = 1
try Document.deleteOne(db, id: 1) // Document?
请求可以批量更新记录。 updateAll()
方法接受使用 set(to:)
方法定义的列赋值。
// UPDATE player SET score = 0, isHealthy = 1, bonus = NULL
try Player.updateAll(db,
Column("score").set(to: 0),
Column("isHealthy").set(to: true),
Column("bonus").set(to: nil))
// UPDATE player SET score = 0 WHERE team = 'red'
try Player
.filter(Column("team") == "red")
.updateAll(db, Column("score").set(to: 0))
// UPDATE player SET top = 1 ORDER BY score DESC LIMIT 10
try Player
.order(Column("score").desc)
.limit(10)
.updateAll(db, Column("top").set(to: true))
// UPDATE country SET population = 67848156 WHERE id = 'FR'
try Country
.filter(id: "FR")
.updateAll(db, Column("population").set(to: 67_848_156))
列赋值接受任何表达式。
// UPDATE player SET score = score + (bonus * 2)
try Player.updateAll(db, Column("score").set(to: Column("score") + Column("bonus") * 2))
为方便起见,您还可以使用 +=
、-=
、*=
或 /=
运算符。
// UPDATE player SET score = score + (bonus * 2)
try Player.updateAll(db, Column("score") += Column("bonus") * 2)
默认的 冲突解决规则适用,您也可以提供一个特定的规则。
// UPDATE OR IGNORE player SET ...
try Player.updateAll(db, onConflict: .ignore, /* assignments... */)
注意
updateAll
方法在采用 TableRecord 协议的类型和Table
上可用。struct Player: TableRecord { ... } try Player.updateAll(db, ...) // Fine try Table("player").updateAll(db, ...) // Just as fine
到目前为止,我们已经看到了从采用 TableRecord 协议的任何类型创建的 请求。
let request = Player.all() // QueryInterfaceRequest<Player>
类型为 QueryInterfaceRequest
的这些请求可以获取和计数。
try request.fetchCursor(db) // A Cursor of Player
try request.fetchAll(db) // [Player]
try request.fetchSet(db) // Set<Player>
try request.fetchOne(db) // Player?
try request.fetchCount(db) // Int
当查询接口无法生成您需要的 SQL 时,您仍然可以回退到 原始 SQL。
// Custom SQL is always welcome
try Player.fetchAll(db, sql: "SELECT ...") // [Player]
但是您可能更愿意恢复一些优雅,并构建自定义请求。
// No custom SQL in sight
try Player.customRequest().fetchAll(db) // [Player]
要构建自定义请求,您可以使用内置请求之一或从其他请求派生请求。
SQLRequest 是从原始 SQL 构建的获取请求。例如:
extension Player {
static func filter(color: Color) -> SQLRequest<Player> {
SQLRequest<Player>(
sql: "SELECT * FROM player WHERE color = ?"
arguments: [color])
}
}
// [Player]
try Player.filter(color: .red).fetchAll(db)
SQLRequest 支持 SQL 插值。
extension Player {
static func filter(color: Color) -> SQLRequest<Player> {
"SELECT * FROM player WHERE color = \(color)"
}
}
asRequest(of:)
方法更改请求获取的类型。 例如,当您使用 关联时,它很有用。
struct BookInfo: FetchableRecord, Decodable {
var book: Book
var author: Author
}
let request = Book
.including(required: Book.author)
.asRequest(of: BookInfo.self)
// [BookInfo]
try request.fetchAll(db)
adapted(_:)
方法简化了使用行适配器使用复杂行的过程。 有关使用 adapted(_:)
的示例代码,请参阅 RowAdapter
和 splittingRowAdapters(columnCounts:)
。
GRDB 可以使用 SQLCipher v3.4+ 加密您的数据库。
使用 CocoaPods,并在您的 Podfile
中指定:
# GRDB with SQLCipher 4
pod 'GRDB.swift/SQLCipher'
pod 'SQLCipher', '~> 4.0'
# GRDB with SQLCipher 3
pod 'GRDB.swift/SQLCipher'
pod 'SQLCipher', '~> 3.4'
确保从您的 Podfile 中删除所有现有的 pod 'GRDB.swift'
。 GRDB.swift/SQLCipher
必须是您的整个项目中唯一活动的 GRDB pod,否则您将面临链接器或运行时错误,这是由于 SQLCipher 和系统 SQLite 之间的冲突。
您可以通过向您的 数据库连接提供密码来创建和打开加密数据库
var config = Configuration()
config.prepareDatabase { db in
try db.usePassphrase("secret")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
您还可以在 prepareDatabase
中执行其他 SQLCipher 配置步骤,这些步骤必须在 SQLCipher 连接的生命周期的早期发生。 例如:
var config = Configuration()
config.prepareDatabase { db in
try db.usePassphrase("secret")
try db.execute(sql: "PRAGMA cipher_page_size = ...")
try db.execute(sql: "PRAGMA kdf_iter = ...")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
当您想使用 SQLCipher 4 打开现有的 SQLCipher 3 数据库时,您可能需要运行 cipher_compatibility
pragma。
// Open an SQLCipher 3 database with SQLCipher 4
var config = Configuration()
config.prepareDatabase { db in
try db.usePassphrase("secret")
try db.execute(sql: "PRAGMA cipher_compatibility = 3")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
有关更多信息,请参阅 SQLCipher 4.0.0 发布 和 升级到 SQLCipher 4。
您可以更改已加密数据库的密码。
当您使用 数据库队列 时,请使用旧密码打开数据库,然后应用新密码。
try dbQueue.write { db in
try db.changePassphrase("newSecret")
}
当您使用 数据库连接池 时,请确保通过在 barrierWriteWithoutTransaction
代码块中更改密码,避免发生并发读取。您还必须通过调用 invalidateReadOnlyConnections
方法来确保所有未来的读取都打开一个新的数据库连接。
try dbPool.barrierWriteWithoutTransaction { db in
try db.changePassphrase("newSecret")
dbPool.invalidateReadOnlyConnections()
}
注意:当应用程序想要在密码更改后继续使用数据库队列或连接池时,它有责任向在数据库准备函数中调用的
usePassphrase
方法提供正确的密码。 请考虑以下情况:// WRONG: this won't work across a passphrase change let passphrase = try getPassphrase() var config = Configuration() config.prepareDatabase { db in try db.usePassphrase(passphrase) } // CORRECT: get the latest passphrase when it is needed var config = Configuration() config.prepareDatabase { db in let passphrase = try getPassphrase() try db.usePassphrase(passphrase) }
注意:
DatabasePool.barrierWriteWithoutTransaction
方法不能阻止 数据库快照 在密码更改期间或新密码已应用于数据库后访问数据库。 这些数据库访问可能会引发错误。 应用程序应提供自己的机制,以便在密码更改之前使打开的快照失效。
注意:除了此处描述的“就地”更改密码之外,您还可以将数据库导出到使用新密码的新加密数据库中。 请参阅 将数据库导出到加密数据库。
但是,提供密码不会加密已经存在的明文数据库。 SQLCipher 无法做到这一点,您会收到一个错误:SQLite error 26: file is encrypted or is not a database
。
相反,创建一个新的加密数据库,位于不同的位置,并导出现有数据库的内容。 这既可以加密明文数据库,也可以更改加密数据库的密码。
执行此操作的技术已由 SQLCipher 记录。
使用 GRDB,它给出:
// The existing database
let existingDBQueue = try DatabaseQueue(path: "/path/to/existing.db")
// The new encrypted database, at some distinct location:
var config = Configuration()
config.prepareDatabase { db in
try db.usePassphrase("secret")
}
let newDBQueue = try DatabaseQueue(path: "/path/to/new.db", configuration: config)
try existingDBQueue.inDatabase { db in
try db.execute(
sql: """
ATTACH DATABASE ? AS encrypted KEY ?;
SELECT sqlcipher_export('encrypted');
DETACH DATABASE encrypted;
""",
arguments: [newDBQueue.path, "secret"])
}
// Now the export is completed, and the existing database can be deleted.
建议避免将密码在内存中保留过长时间。 为此,请确保从 prepareDatabase
方法加载密码。
// NOT RECOMMENDED: this keeps the passphrase in memory longer than necessary
let passphrase = try getPassphrase()
var config = Configuration()
config.prepareDatabase { db in
try db.usePassphrase(passphrase)
}
// RECOMMENDED: only load the passphrase when it is needed
var config = Configuration()
config.prepareDatabase { db in
let passphrase = try getPassphrase()
try db.usePassphrase(passphrase)
}
此技术有助于管理密码的生命周期,但请记住,即使在对象释放后,字符串的内容也可能在内存中保持完整很长时间。
为了更好地控制内存中密码的生命周期,请使用本机提供 resetBytes
函数的 Data 对象。
// RECOMMENDED: only load the passphrase when it is needed and reset its content immediately after use
var config = Configuration()
config.prepareDatabase { db in
var passphraseData = try getPassphraseData() // Data
defer {
passphraseData.resetBytes(in: 0..<passphraseData.count)
}
try db.usePassphrase(passphraseData)
}
一些要求苛刻的用户会希望更进一步,并管理原始密码字节的生命周期。 请参见下文。
GRDB 提供了方便的方法,用于将数据库密码作为 Swift 字符串提供:usePassphrase(_:)
和 changePassphrase(_:)
。 这些方法不会将密码字符串在内存中保留过长时间。 但它们与标准 String 类型一样安全:内存中实际密码字节的生命周期不受控制。
当您想要精确管理密码字节时,请使用其原始 C 函数直接与 SQLCipher 对话。
例如
var config = Configuration()
config.prepareDatabase { db in
... // Carefully load passphrase bytes
let code = sqlite3_key(db.sqliteConnection, /* passphrase bytes */)
... // Carefully dispose passphrase bytes
guard code == SQLITE_OK else {
throw DatabaseError(
resultCode: ResultCode(rawValue: code),
message: db.lastErrorMessage)
}
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
当密码安全地存储在系统钥匙串中时,您的应用程序可以使用 kSecAttrAccessible
属性来保护它。
这种保护方式可以防止 GRDB 在密码不可用时创建 SQLite 连接。
var config = Configuration()
config.prepareDatabase { db in
let passphrase = try loadPassphraseFromSystemKeychain()
try db.usePassphrase(passphrase)
}
// Success if and only if the passphrase is available
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
出于同样的原因,数据库连接池 会按需打开 SQLite 连接,一旦密码不可用,则可能随时失败。
// Success if and only if the passphrase is available
let dbPool = try DatabasePool(path: dbPath, configuration: config)
// May fail if passphrase has turned unavailable
try dbPool.read { ... }
// May trigger value observation failure if passphrase has turned unavailable
try dbPool.write { ... }
由于 DatabasePool 维护着一个长期存在的 SQLite 连接池,因此某些数据库访问将使用现有连接并成功。 并且其他一些数据库访问将会失败,因为连接池想要打开一个新的连接。 无法预测哪些访问会成功或失败。
出于同样的原因,即使密码已变为不可用,数据库队列(同样维护着一个长期存在的 SQLite 连接)仍将保持可用。
因此,应用程序有责任在密码不可用时保护数据库访问。 为此,他们可以使用 数据保护。 当密码变为不可用时,他们还可以销毁他们的数据库队列或连接池实例。
您可以将一个数据库备份(复制)到另一个数据库中。
例如,当您实现 NSDocument 子类时,备份可以帮助您在内存数据库和数据库文件之间进行复制。
let source: DatabaseQueue = ... // or DatabasePool
let destination: DatabaseQueue = ... // or DatabasePool
try source.backup(to: destination)
backup
方法会阻止当前线程,直到目标数据库包含与源数据库相同的内容。
当源是 数据库连接池 时,在备份期间可能会发生并发写入。 这些写入可能反映在备份中,也可能不反映在备份中,但它们不会触发任何错误。
Database
具有类似的 backup
方法。
let source: DatabaseQueue = ... // or DatabasePool
let destination: DatabaseQueue = ... // or DatabasePool
try source.write { sourceDb in
try destination.barrierWriteWithoutTransaction { destDb in
try sourceDb.backup(to: destDb)
}
}
此方法允许选择源和目标 Database
句柄,以备份数据库。
backup
方法采用可选的 pagesPerStep
和 progress
参数。 这些参数可以一起用于跟踪数据库备份的进度并中止未完成的备份。
提供 pagesPerStep
时,数据库备份将以步骤执行。 在每个步骤中,从源复制到目标的数据库页数不超过 pagesPerStep
。 备份将逐步进行,直到所有页面都被复制。
提供 progress
回调时,progress
会在每个备份步骤之后(包括最后一个步骤)被调用。 即使指定了非默认的 pagesPerStep
,或者备份以单个步骤完成,也会调用 progress
回调。
try source.backup(
to: destination,
pagesPerStep: ...)
{ backupProgress in
print("Database backup progress:", backupProgress)
}
如果在 backupProgress.isComplete == false
时调用 progress
抛出异常,则备份将中止并重新抛出该错误。 但是,如果在 backupProgress.isComplete == true
时调用 progress
抛出异常,则备份不受影响,并且该错误将被静默忽略。
警告:将非默认值
pagesPerStep
或progress
传递给备份方法是一种高级 API,旨在为专业用户提供额外的功能。 GRDB 的备份 API 为底层 SQLite 在线备份 API 提供了一个忠实的、低级的包装器。 GRDB 的文档不能完全替代官方 SQLite 的 备份 API 文档。
interrupt()
方法 使任何待处理的数据库操作中止并在其最早的机会返回。
可以从任何线程调用它。
dbQueue.interrupt()
dbPool.interrupt()
当没有正在运行的 SQL 语句时,调用 interrupt()
是一个空操作,并且对在 interrupt()
返回后启动的 SQL 语句没有任何影响。
中断的数据库操作将抛出一个代码为 SQLITE_INTERRUPT
的 DatabaseError。 如果中断的 SQL 操作是显式事务中的 INSERT、UPDATE 或 DELETE,则整个事务将自动回滚。 如果回滚的事务是由事务包装方法(如 DatabaseWriter.write
或 Database.inTransaction
)启动的,则所有数据库访问都将抛出一个代码为 SQLITE_ABORT
的 DatabaseError,直到包装方法返回。
例如
try dbQueue.write { db in
try Player(...).insert(db) // throws SQLITE_INTERRUPT
try Player(...).insert(db) // not executed
} // throws SQLITE_INTERRUPT
try dbQueue.write { db in
do {
try Player(...).insert(db) // throws SQLITE_INTERRUPT
} catch { }
} // throws SQLITE_ABORT
try dbQueue.write { db in
do {
try Player(...).insert(db) // throws SQLITE_INTERRUPT
} catch { }
try Player(...).insert(db) // throws SQLITE_ABORT
} // throws SQLITE_ABORT
您可以捕获 SQLITE_INTERRUPT
和 SQLITE_ABORT
错误
do {
try dbPool.write { db in ... }
} catch DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ABORT {
// Oops, the database was interrupted.
}
有关更多信息,请参阅 中断长时间运行的查询。
SQL 注入是一种让攻击者摧毁您的数据库的技术。
这是一个容易受到 SQL 注入攻击的代码示例
// BAD BAD BAD
let id = 1
let name = textField.text
try dbQueue.write { db in
try db.execute(sql: "UPDATE students SET name = '\(name)' WHERE id = \(id)")
}
如果用户输入一个有趣的字符串,例如 Robert'; DROP TABLE students; --
,SQLite 将看到以下 SQL,并删除您的数据库表,而不是按预期更新名称
UPDATE students SET name = 'Robert';
DROP TABLE students;
--' WHERE id = 1
为了避免这些问题,永远不要将原始值嵌入到您的 SQL 查询中。 唯一正确的技术是向您的原始 SQL 查询提供 参数。
let name = textField.text
try dbQueue.write { db in
// Good
try db.execute(
sql: "UPDATE students SET name = ? WHERE id = ?",
arguments: [name, id])
// Just as good
try db.execute(
sql: "UPDATE students SET name = :name WHERE id = :id",
arguments: ["name": name, "id": id])
}
当您使用 记录 和 查询接口 时,GRDB 始终会防止 SQL 注入。
let id = 1
let name = textField.text
try dbQueue.write { db in
if var student = try Student.fetchOne(db, id: id) {
student.name = name
try student.update(db)
}
}
GRDB 可能会抛出 DatabaseError、RecordError,或者通过 致命错误 崩溃您的程序。
考虑到本地数据库不是从远程服务器加载的一些 JSON,GRDB 专注于受信任的数据库。 处理 不受信任的数据库 需要格外小心。
DatabaseError 在 SQLite 错误时抛出。
do {
try Pet(masterId: 1, name: "Bobby").insert(db)
} catch let error as DatabaseError {
// The SQLite error code: 19 (SQLITE_CONSTRAINT)
error.resultCode
// The extended error code: 787 (SQLITE_CONSTRAINT_FOREIGNKEY)
error.extendedResultCode
// The eventual SQLite message: FOREIGN KEY constraint failed
error.message
// The eventual erroneous SQL query
// "INSERT INTO pet (masterId, name) VALUES (?, ?)"
error.sql
// The eventual SQL arguments
// [1, "Bobby"]
error.arguments
// Full error description
// > SQLite error 19: FOREIGN KEY constraint failed -
// > while executing `INSERT INTO pet (masterId, name) VALUES (?, ?)`
error.description
}
如果您想在错误描述中查看语句参数,请 使语句参数公开。
SQLite 使用 结果代码 来区分各种错误。.
您可以捕获 DatabaseError 并匹配结果代码。
do {
try ...
} catch let error as DatabaseError {
switch error {
case DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY:
// foreign key constraint error
case DatabaseError.SQLITE_CONSTRAINT:
// any other constraint error
default:
// any other database error
}
}
您也可以直接在结果代码上匹配错误。
do {
try ...
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
// foreign key constraint error
} catch DatabaseError.SQLITE_CONSTRAINT {
// any other constraint error
} catch {
// any other database error
}
每个 DatabaseError 都有两个代码:一个 extendedResultCode
(参见 扩展结果代码),和一个不太精确的 resultCode
(参见 主结果代码)。 扩展结果代码是主结果代码的改进,例如 SQLITE_CONSTRAINT_FOREIGNKEY
对于 SQLITE_CONSTRAINT
。
警告:SQLite 在其版本中逐步引入了扩展结果代码。 SQLite 发行说明 遗憾的是对此不太清楚:请谨慎编写对扩展结果代码的处理。
当 update
方法找不到任何要更新的行时,PersistableRecord 协议会抛出 RecordError。
do {
try player.update(db)
} catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
print("Key \(key) was not found in table \(table).")
}
当 find
方法找不到任何记录时,FetchableRecord 协议也会抛出 RecordError。
do {
let player = try Player.find(db, id: 42)
} catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
print("Key \(key) was not found in table \(table).")
}
致命错误通知程序或数据库必须更改。
它们揭示了程序员的错误、错误的假设并防止误用。 以下是一些示例
代码要求一个非可选值,而数据库包含 NULL。
// fatal error: could not convert NULL to String.
let name: String = row["name"]
解决方案:修复数据库内容,使用NOT NULL约束,或者加载可选值
let name: String? = row["name"]
从数据库值到Swift类型的转换失败
// fatal error: could not convert "Mom’s birthday" to Date.
let date: Date = row["date"]
// fatal error: could not convert "" to URL.
let url: URL = row["url"]
解决方案:修复数据库内容,或者使用DatabaseValue来处理所有可能的情况
let dbValue: DatabaseValue = row["date"]
if dbValue.isNull {
// Handle NULL
} else if let date = Date.fromDatabaseValue(dbValue) {
// Handle valid date
} else {
// Handle invalid date
}
数据库无法保证代码的行为与声明的一致
// fatal error: table player has no unique index on column email
try Player.deleteOne(db, key: ["email": "arthur@example.com"])
解决方案:向player.email列添加唯一索引,或者使用deleteAll
方法来明确表示您可能会删除多行
try Player.filter(Column("email") == "arthur@example.com").deleteAll(db)
数据库连接不可重入
// fatal error: Database methods are not reentrant.
dbQueue.write { db in
dbQueue.write { db in
...
}
}
解决方案:避免重入,改为传递数据库连接。
让我们考虑以下代码
let sql = "SELECT ..."
// Some untrusted arguments for the query
let arguments: [String: Any] = ...
let rows = try Row.fetchCursor(db, sql: sql, arguments: StatementArguments(arguments))
while let row = try rows.next() {
// Some untrusted database value:
let date: Date? = row[0]
}
它有两个机会抛出致命错误
在这种情况下,您仍然可以通过公开和处理每个故障点来避免致命错误,降低到GRDB API的更低一层
// Untrusted arguments
if let arguments = StatementArguments(arguments) {
let statement = try db.makeStatement(sql: sql)
try statement.setArguments(arguments)
var cursor = try Row.fetchCursor(statement)
while let row = try iterator.next() {
// Untrusted database content
let dbValue: DatabaseValue = row[0]
if dbValue.isNull {
// Handle NULL
if let date = Date.fromDatabaseValue(dbValue) {
// Handle valid date
} else {
// Handle invalid date
}
}
}
有关更多信息,请参见Statement
和DatabaseValue。
可以配置SQLite,以便在发生异常时调用包含错误代码和简短错误消息的回调函数。
必须在应用程序生命周期的早期配置此全局错误回调
Database.logError = { (resultCode, message) in
NSLog("%@", "SQLite error \(resultCode): \(message)")
}
警告:必须在打开任何数据库连接之前设置Database.logError。 这包括您的应用程序使用GRDB打开的连接,以及其他工具(例如第三方库)打开的连接。 在打开连接后进行设置是滥用SQLite,并且无效。
有关更多信息,请参见错误和警告日志。
SQLite允许您在数据库中存储unicode字符串。
但是,SQLite不提供任何unicode感知的字符串转换或比较。
内置的SQLite函数UPPER
和LOWER
不是unicode感知的
// "JéRôME"
try String.fetchOne(db, sql: "SELECT UPPER('Jérôme')")
GRDB使用调用Swift内置字符串函数capitalized
、lowercased
、uppercased
、localizedCapitalized
、localizedLowercased
和localizedUppercased
的SQL函数扩展了SQLite
// "JÉRÔME"
let uppercased = DatabaseFunction.uppercase
try String.fetchOne(db, sql: "SELECT \(uppercased.name)('Jérôme')")
这些unicode感知的字符串函数也可以在查询接口中轻松使用
Player.select(nameColumn.uppercased)
SQLite在许多情况下比较字符串:当您根据字符串列对行进行排序时,或者当您使用比较运算符(例如=
和<=
)时。
比较结果来自排序规则函数或collation。 SQLite带有三个不支持Unicode的内置collation:binary, nocase, 和 rtrim。
GRDB带有五个额外的collation,它们利用基于标准Swift String比较函数和运算符的unicode感知比较
unicodeCompare
(使用内置的<=
和==
Swift运算符)caseInsensitiveCompare
localizedCaseInsensitiveCompare
localizedCompare
localizedStandardCompare
可以将collation应用于表列。 然后,涉及此列的所有比较将自动触发比较函数
try db.create(table: "player") { t in
// Guarantees case-insensitive email unicity
t.column("email", .text).unique().collate(.nocase)
// Sort names in a localized case insensitive way
t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
}
// Players are sorted in a localized case insensitive way:
let players = try Player.order(nameColumn).fetchAll(db)
警告:SQLite要求宿主应用程序提供除binary, nocase 和 rtrim 之外的任何collation的定义。 当必须将数据库文件共享或迁移到另一个SQLite库或平台(例如应用程序的Android版本)时,请确保您提供兼容的collation。
如果您不能或不想定义列的比较行为(请参阅上面的警告),您仍然可以在SQL请求和查询接口中使用显式collation
let collation = DatabaseCollation.localizedCaseInsensitiveCompare
let players = try Player.fetchAll(db,
sql: "SELECT * FROM player ORDER BY name COLLATE \(collation.name))")
let players = try Player.order(nameColumn.collating(collation)).fetchAll(db)
您还可以定义自己的collation:
let collation = DatabaseCollation("customCollation") { (lhs, rhs) -> NSComparisonResult in
// return the comparison of lhs and rhs strings.
}
// Make the collation available to a database connection
var config = Configuration()
config.prepareDatabase { db in
db.add(collation: collation)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
SQLite和GRDB都使用非必要的内存来帮助它们更好地执行。
您可以使用releaseMemory
方法回收此内存
// Release as much memory as possible.
dbQueue.releaseMemory()
dbPool.releaseMemory()
此方法会阻塞当前线程,直到完成所有当前数据库访问并收集内存。
警告:如果在并发执行长时间读取时调用
DatabasePool.releaseMemory()
,则直到完成此长时间读取并释放内存后,才可能进行其他读取访问。 如果这不符合您的应用程序需求,请寻找下面的异步选项
您也可以以异步方式释放内存
// On a DatabaseQueue
dbQueue.asyncWriteWithoutTransaction { db in
db.releaseMemory()
}
// On a DatabasePool
dbPool.releaseMemoryEventually()
DatabasePool.releaseMemoryEventually()
不会阻塞当前线程,也不会阻止并发数据库访问。 作为对此便利的交换,您不知道何时释放了内存。
iOS操作系统喜欢不消耗太多内存的应用程序。
数据库队列和池在应用程序收到内存警告以及应用程序进入后台时,会自动释放非必要的内存。
您可以选择退出此自动内存管理
var config = Configuration()
config.automaticMemoryManagement = false
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config) // or DatabasePool
首先,为数据库文件选择合适的位置。 基于文档的应用程序将让用户选择一个位置。 使用数据库作为全局存储的应用程序将更喜欢Application Support目录。
下面的示例代码在其专用目录中创建或打开一个数据库文件(一个推荐做法)。 首次运行时,将创建一个新的空数据库文件。 在后续运行时,数据库文件已经存在,因此它仅打开一个连接
// HOW TO create an empty database, or open an existing database file
// Create the "Application Support/MyDatabase" directory
let fileManager = FileManager.default
let appSupportURL = try fileManager.url(
for: .applicationSupportDirectory, in: .userDomainMask,
appropriateFor: nil, create: true)
let directoryURL = appSupportURL.appendingPathComponent("MyDatabase", isDirectory: true)
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
// Open or create the database
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
let dbQueue = try DatabaseQueue(path: databaseURL.path)
打开到您的资源的只读连接
// HOW TO open a read-only connection to a database resource
// Get the path to the database resource.
if let dbPath = Bundle.main.path(forResource: "db", ofType: "sqlite") {
// If the resource exists, open a read-only connection.
// Writes are disallowed because resources can not be modified.
var config = Configuration()
config.readonly = true
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
} else {
// The database resource can not be found.
// Fix your setup, or report the problem to the user.
}
当DatabaseQueue
或DatabasePool
实例被反初始化时,数据库连接会自动关闭。
如果程序的正确执行依赖于精确的数据库关闭,请显式调用close()
。 此方法可能会失败并创建僵尸连接,因此请检查其详细文档。
当您要调试未传递预期结果的请求时,您可能需要打印实际执行的SQL。
您可以将请求编译为预处理的Statement
try dbQueue.read { db in
let request = Player.filter(Column("email") == "arthur@example.com")
let statement = try request.makePreparedRequest(db).statement
print(statement) // SELECT * FROM player WHERE email = ?
print(statement.arguments) // ["arthur@example.com"]
}
另一种选择是设置一个跟踪函数,该函数打印出执行的SQL请求。 例如,在连接到数据库时提供一个跟踪函数
// Prints all SQL statements
var config = Configuration()
config.prepareDatabase { db in
db.trace { print($0) }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
try dbQueue.read { db in
// Prints "SELECT * FROM player WHERE email = ?"
let players = try Player.filter(Column("email") == "arthur@example.com").fetchAll(db)
}
如果您想在已记录的语句中看到语句参数(例如'arthur@example.com'
),请使语句参数公开。
注意:生成的 SQL 可能会在 GRDB 版本之间更改,恕不另行通知:不要让您的应用程序依赖于任何特定的 SQL 输出。
使用带有.profile
选项的trace(options:_:)
方法
var config = Configuration()
config.prepareDatabase { db in
db.trace(options: .profile) { event in
// Prints all SQL statements with their duration
print(event)
// Access to detailed profiling information
if case let .profile(statement, duration) = event, duration > 0.5 {
print("Slow query: \(statement.sql)")
}
}
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
try dbQueue.read { db in
let players = try Player.filter(Column("email") == "arthur@example.com").fetchAll(db)
// Prints "0.003s SELECT * FROM player WHERE email = ?"
}
如果您想在已记录的语句中看到语句参数(例如'arthur@example.com'
),请使语句参数公开。
自GRDB 1.0起,所有向后兼容性保证都适用于语义版本控制:在库的下一个主要版本之前,不会发生重大更改。
但是,有一个例外:用“🔥 实验性”徽章标记的实验性功能。 这些是过于年轻或缺乏用户反馈的高级功能。 它们尚未稳定。
这些实验性功能不受语义版本控制的保护,并且可能在库的两个次要版本之间中断。 为了帮助它们变得稳定,您的反馈非常感谢。
否,GRDB不支持库演进和ABI稳定性。 唯一的承诺是根据语义版本控制的API稳定性,但实验性功能除外。
但是,可以使用“为分发构建库” Xcode选项 (BUILD_LIBRARY_FOR_DISTRIBUTION
) 构建 GRDB,以便您可以方便地构建二进制框架。
假设您有两种记录类型,Book
和Author
,并且您只想获取有作者的书籍,并丢弃匿名书籍。
我们首先定义书籍和作者之间的关联
struct Book: TableRecord {
...
static let author = belongsTo(Author.self)
}
struct Author: TableRecord {
...
}
然后,我们可以编写我们的请求并仅获取有作者的书籍,丢弃匿名的书籍
let books: [Book] = try dbQueue.read { db in
// SELECT book.* FROM book
// JOIN author ON author.id = book.authorID
let request = Book.joining(required: Book.author)
return try request.fetchAll(db)
}
请注意,此请求不使用filter
方法。 实际上,我们没有任何条件可以在任何列上表达。 相反,我们只需要“要求一本书可以加入到它的作者”。
有关相反的问题,请参阅下面的如何过滤记录并仅保留未与另一个记录关联的记录?。
假设您有两种记录类型,Book
和Author
,并且您只想获取没有任何作者的匿名书籍。
我们首先定义书籍和作者之间的关联
struct Book: TableRecord {
...
static let author = belongsTo(Author.self)
}
struct Author: TableRecord {
...
}
然后,我们可以编写我们的请求并仅获取没有任何作者的匿名书籍
let books: [Book] = try dbQueue.read { db in
// SELECT book.* FROM book
// LEFT JOIN author ON author.id = book.authorID
// WHERE author.id IS NULL
let authorAlias = TableAlias()
let request = Book
.joining(optional: Book.author.aliased(authorAlias))
.filter(!authorAlias.exists)
return try request.fetchAll(db)
}
此请求使用TableAlias,以便能够过滤最终关联的作者。 我们确保Author.primaryKey
为nil,这是另一种说法:这本书没有作者。
请参阅上面如何过滤记录并仅保留与另一个记录相关的记录?以了解相反的问题。
假设您有两种记录类型,Book
和Author
,并且您想获取所有书籍及其作者姓名,而不是完整的关联作者记录。
我们首先定义书籍和作者之间的关联
struct Book: Decodable, TableRecord {
...
static let author = belongsTo(Author.self)
}
struct Author: Decodable, TableRecord {
...
enum Columns {
static let name = Column(CodingKeys.name)
}
}
然后我们可以编写我们的请求以及解码它的临时记录
struct BookInfo: Decodable, FetchableRecord {
var book: Book
var authorName: String? // nil when the book is anonymous
static func all() -> QueryInterfaceRequest<BookInfo> {
// SELECT book.*, author.name AS authorName
// FROM book
// LEFT JOIN author ON author.id = book.authorID
let authorName = Author.Columns.name.forKey(CodingKeys.authorName)
return Book
.annotated(withOptional: Book.author.select(authorName))
.asRequest(of: BookInfo.self)
}
}
let bookInfos: [BookInfo] = try dbQueue.read { db in
BookInfo.all().fetchAll(db)
}
通过将请求定义为 BookInfo 的静态方法,您可以访问私有的CodingKeys.authorName
以及编译器检查过的 SQL 列名。
通过使用annotated(withOptional:)
方法,您可以将作者姓名附加到可以由临时记录解码的顶层选择中。
通过使用asRequest(of:)
,您可以增强请求的类型安全性。
有时,ValueObservation 看起来并没有通知您期望的更改。
这可能有四个可能的原因
要回答前两个问题,请查看数据库执行的 SQL 语句。 这在您打开数据库连接时完成
// Prints all SQL statements
var config = Configuration()
config.prepareDatabase { db in
db.trace { print("SQL: \($0)") }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
如果在那之后,您确信预期的更改已提交到数据库中,并且没有很快被覆盖,请跟踪观察事件
let observation = ValueObservation
.tracking { db in ... }
.print() // <- trace observation events
let cancellable = observation.start(...)
查看以 cancel
或 failure
开头的观察日志:也许观察被您的应用程序取消了,或者确实因错误而失败了。
查看以 value
开头的观察日志:再次确保实际未通知预期值,然后将其覆盖。
最后,查看以 tracked region
开头的观察日志。 打印的数据库区域是否涵盖了预期的更改?
例如
empty
:空区域,不跟踪任何内容,也永远不会触发观察。player(*)
:完整的 player
表player(id,name)
:player
表的 id
和 name
列player(id,name)[1]
:player
表中 id 为 1 的行的 id
和 name
列player(*),team(*)
:完整的 player
和 team
表如果您碰巧使用 ValueObservation.trackingConstantRegion(_:)
方法,并且发现跟踪的区域与您的预期不符,请使用 tracking(_:)
更改观察的定义。 您应该看到以 tracked region
开头的日志现在会演变以包含预期的更改,并且您会收到预期的通知。
如果在完成所有这些步骤后(谢谢!),您的观察仍然失败,请打开一个 issue并提供一个最小可重现示例!
在使用数据库队列和池的 read
和 write
方法时,您可能会收到此错误
// Generic parameter 'T' could not be inferred
let string = try dbQueue.read { db in
let result = try String.fetchOne(db, ...)
return result
}
这是 Swift 编译器的局限性。
通用的解决方法是显式声明闭包结果的类型
// General Workaround
let string = try dbQueue.read { db -> String? in
let result = try String.fetchOne(db, ...)
return result
}
在可能的情况下,您也可以编写单行闭包
// Single-line closure workaround:
let string = try dbQueue.read { db in
try String.fetchOne(db, ...)
}
insert
和 save
持久化方法可能会在异步上下文中触发编译器错误
var player = Player(id: nil, name: "Arthur")
try await dbWriter.write { db in
// Error: Mutation of captured var 'player' in concurrently-executing code
try player.insert(db)
}
print(player.id) // A non-nil id
发生这种情况时,请首选 inserted
和 saved
方法
// OK
var player = Player(id: nil, name: "Arthur")
player = try await dbWriter.write { [player] db in
return try player.inserted(db)
}
print(player.id) // A non-nil id
此错误消息是不言自明的:请检查拼写错误或不存在的列名。
但是,有时仅当应用程序在最新的操作系统(iOS 14+、Big Sur+ 等)上运行时才会发生此错误。 在以前的版本中不会发生此错误。
如果是这种情况,则有两种可能的解释
也许列名确实拼写错误或从数据库模式中丢失。
要找到它,请检查随 DatabaseError 一起的 SQL 语句。
也许应用程序正在使用字符 "
而不是单引号 '
作为原始 SQL 查询中字符串文字的分隔符。 最新版本的 SQLite 已经学会了说明与 SQL 标准的这种偏差,这就是您看到此错误的原因。
例如:这不是标准 SQL:UPDATE player SET name = "Arthur"
。
标准版本是:UPDATE player SET name = 'Arthur'
。
只是旧版本的 SQLite 过去常常接受前者,即非标准版本。 较新版本能够拒绝它并出现错误。
解决方法是更改应用程序运行的 SQL 语句:在字符串文字中将 "
替换为 '
。
现在也可能是时候了解语句参数和 SQL 注入 了
let name: String = ...
// NOT STANDARD (double quote)
try db.execute(sql: """
UPDATE player SET name = "\(name)"
""")
// STANDARD, BUT STILL NOT RECOMMENDED (single quote)
try db.execute(sql: "UPDATE player SET name = '\(name)'")
// STANDARD, AND RECOMMENDED (statement arguments)
try db.execute(sql: "UPDATE player SET name = ?", arguments: [name])
有关更多信息,请参阅 接受双引号字符串文字 和 Configuration.acceptsDoubleQuotedStringLiterals。
这些错误可能是 SQLite 无法访问数据库的信号,原因是数据保护。
当您的应用程序应该能够在锁定的设备上在后台运行时,它必须捕获此错误,例如,等待 UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:) 或 UIApplicationProtectedDataDidBecomeAvailable 通知,然后重试失败的数据库操作。
do {
try ...
} catch DatabaseError.SQLITE_IOERR, DatabaseError.SQLITE_AUTH {
// Handle possible data protection error
}
也可以通过使用更宽松的文件保护来完全避免此错误。
执行类似于以下的 LIKE 查询时,您可能会收到错误“语句参数数量错误”
let name = textField.text
let players = try dbQueue.read { db in
try Player.fetchAll(db, sql: "SELECT * FROM player WHERE name LIKE '%?%'", arguments: [name])
}
问题在于 '%?%'
模式。
仅当 ?
是整个值(int、double、string、blob、null)的占位符时,SQLite 才将其解释为参数。 在此不正确的查询中,?
只是 '%?%'
字符串中的一个字符:它不是查询参数,并且不会以任何方式处理。 有关 SQLite 参数的更多信息,请参见https://www.sqlite.org/lang_expr.html#varparam。
要修复此错误,您可以将模式本身而不是名称提供给请求
let name = textField.text
let players: [Player] = try dbQueue.read { db in
let pattern = "%\(name)%"
return try Player.fetchAll(db, sql: "SELECT * FROM player WHERE name LIKE ?", arguments: [pattern])
}
GRDB.xcworkspace
:它包含启用了 GRDB 的 playground,可供您使用。感谢
本章已重命名为在查询界面请求中嵌入 SQL。
本章已移动。
本章节已移动。
本章已移动。
本章节已重命名为记录比较。
本章已移动。
自定义值类型遵循 DatabaseValueConvertible
协议。
本章节已重命名为 超越 FetchableRecord。
本章节已被持久化回调取代。
本章节已移动。
本章节已移动。
本章节已移动。
本章节已移动。
本章已移动。
本章节已删除。请参阅 DatabaseReader 和 DatabaseWriter 的参考。
本章节已重命名为 Data、Date 和 UUID 编码策略。
本章节已被共享数据库指南取代。
本章已移动。
自 GRDB 6.7.0 起,默认启用 FTS5。
FetchedRecordsController 已在 GRDB 5 中删除。
数据库观察章节介绍了观察数据库的其他方法。
本章节已移动。
本章已移动。
本章节已被 splittingRowAdapters(columnCounts:) 的文档取代。
请参阅记录和查询接口。
本章节已移动。
本章节已移动。
该协议已在 GRDB 3.0 中重命名为 PersistableRecord。
此错误已重命名为 RecordError。
本章节已移动。
Record
类是旧版的 GRDB 类型。自 GRDB 7 以来,不建议通过继承 Record
类来定义记录类型。
本章节已移动。
该协议已在 GRDB 3.0 中重命名为 FetchableRecord。
该协议已在 GRDB 3.0 中重命名为 TableRecord。
本章节已移动。
本章节已移动。
本章节已移动。
本章已移动。
本章节已移动。
本章节已被 ValueObservation 和 DatabaseRegionObservation 取代。