GRDB: A toolkit for SQLite databases, with a focus on application development.

一个 SQLite 数据库工具包,专注于应用程序开发
自 2015 年以来,自豪地为社区服务

Swift 6 License CI Status

最新版本: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+

联系方式:

什么是 GRDB?

使用此库将应用程序的永久数据保存到 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)
}
访问原始 SQL
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)
}

请参阅 记录

使用 Swift 查询接口查询数据库
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 文档

演示应用程序 & 常见问题

参考

入门

SQLite 和 SQL

记录和查询接口

应用程序工具

须知

配套库

常见问题

示例代码

安装

以下安装过程使 GRDB 使用目标操作系统附带的 SQLite 版本。

有关使用 SQLCipher 的 GRDB 安装过程,请参阅 加密

有关使用 SQLite 的自定义构建的 GRDB 安装过程,请参阅 自定义 SQLite 构建

Swift Package Manager

Swift Package Manager 自动执行 Swift 代码的分发。 要将 GRDB 与 SPM 一起使用,请添加对 https://github.com/groue/GRDB.swift.git 的依赖

GRDB 提供两个库,GRDBGRDB-dynamic。 只选择一个。 如有疑问,请首选 GRDB。 如果您要将其与应用程序中的多个目标链接,并且只想链接到共享的动态框架一次,则 GRDB-dynamic 库可以显示出有用。 有关更多信息,请参见如何将 Swift Package 链接为动态库

注意:目前不支持 Linux。

CocoaPods

CocoaPods是 Xcode 项目的依赖项管理器。 要将 GRDB 与 CocoaPods(1.2 或更高版本)一起使用,请在您的 Podfile 中指定

pod 'GRDB.swift'

GRDB 可以作为框架或静态库安装。

CocoaPods 安装的重要提示

由于 CocoaPods 中的一个问题,目前无法将新版本的 GRDB 部署到 CocoaPods。 CocoaPods 上提供的最新版本是 6.24.1。 要使用 CocoaPods 安装更高版本的 GRDB,请使用以下解决方法之一

Carthage

Carthage 不受支持。 有关此决定的上下文,请参见 #433

手动

  1. 下载 GRDB 的副本,或者克隆其存储库并确保您检出最新的标记版本。

  2. GRDB.xcodeproj 项目嵌入到您自己的项目中。

  3. 在应用程序目标(WatchOS 的扩展目标)的“构建阶段”选项卡的“目标依赖项”部分中添加 GRDB 目标。

  4. GRDB.framework 添加到应用程序目标(WatchOS 的扩展目标)的“常规”选项卡的“嵌入式二进制文件”部分。

数据库连接

GRDB 提供了两个用于访问 SQLite 数据库的类:DatabaseQueueDatabasePool

import GRDB

// Pick one:
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

区别在于

如果您不确定,请选择 DatabaseQueue 您始终可以稍后切换到 DatabasePool

有关打开连接的更多信息和提示,请参见 数据库连接

SQLite API

在本节文档中,我们将讨论 SQL。 如果 SQL 不是您的菜,请跳转到 查询接口

高级主题

执行更新

一旦获得数据库连接execute(sql:arguments:) 方法就会执行不返回任何数据库行的 SQL 语句,例如 CREATE TABLEINSERTDELETEALTER 等。

例如

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?

所有这些获取方法都需要一个包含单个 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
    }
}

如果你看不到或不关心差异,请使用数组。 如果你关心内存和性能,请在适当的时候使用游标。

行查询

获取行

获取行的 游标 数组 集合 单个 行(请参见 获取方法

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 值转换,就可以提取所需的类型

DatabaseValue

📖 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)

当你从行构建字典时,你必须消除重复列的歧义,并选择如何表示数据库值。 例如:

有关更多信息,请参见 Dictionary.init(_:uniquingKeysWith:) 的文档。

值查询

📖 DatabaseValueConvertible

你可以直接获取值,而不是获取行。 有许多支持的 值类型 (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 附带对以下值类型的内置支持:

值可以用作 语句参数

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 and DateComponents)

DateDateComponents 可以存储和从数据库中获取。

以下是 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

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 时区存储。 它精确到毫秒。

注意:选择此格式是因为它是唯一一个:

警告:SQLite 日期格式中有效年份的范围是 0000-9999。 你会遇到超出此范围的年份的问题,例如解码错误,或使用 SQLite 日期和时间函数 进行的无效日期计算。

某些应用程序可能更喜欢另一种日期格式。

在选择不同的日期格式之前,你应该三思。

日期格式的自定义是明确的。 例如:

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

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, NSDecimalNumber 和 Decimal

NSNumberDecimal 可以像其他 一样存储和从数据库中获取。

以下是 GRDB 如何支持 SQLite 支持的各种数据类型。

Integer Double String
NSNumber 读取 / 写入 读取 / 写入 读取
NSDecimalNumber 读取 / 写入 读取 / 写入 读取
Decimal 读取 读取 读取 / 写入

UUID

UUID 可以像其他 一样存储和从数据库中获取。

GRDB 将 uuid 存储为 16 字节的数据 blob,并从 16 字节的数据 blob 和字符串(例如 "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")中解码它们。

Swift 枚举

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
}

自定义 SQL 函数和聚合

SQLite 允许你定义 SQL 函数和聚合。

自定义 SQL 函数或聚合扩展了 SQLite

SELECT reverse(name) FROM player;   -- custom function
SELECT maxLength(name) FROM player; -- custom aggregate

自定义 SQL 函数

📖 DatabaseFunction

函数 参数接受一个 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 指针

如果并非所有的 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.sqliteConnectionStatement.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
}

注意

记录

**除了 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 进行获取。

👉 对于同时遵循 FetchableRecordTableRecord 协议的类型,可以使用 查询接口 进行无 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 协议

📖 FetchableRecord

**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?

有关 fetchCursorfetchAllfetchSetfetchOne 方法的信息,请参见 获取方法。有关查询参数的更多信息,请参见 StatementArguments

**注意**:出于性能原因,在 fetch 查询的迭代过程中,传递给 init(row:) 的相同 row 参数会被重用。 如果你想保留该行以供以后使用,请确保存储一个副本:self.row = row.copy()

**注意**:FetchableRecord.init(row:) 初始化器满足了大多数应用程序的需求。但是,有些应用程序的需求更高。当 FetchableRecord 不能完全提供你需要的支持时,请查看 超越 FetchableRecord 章节。

TableRecord 协议

📖 TableRecord

**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"

例如

你仍然可以提供自定义表名。

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 还可以处理主键和唯一键:参见 按键获取测试记录是否存在

PersistableRecord 协议

📖 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)
}

是的,三个协议而不是一个。 这是你如何选择其中一个或另一个。

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:...)

有关批量更新的更多信息,请参见 更新请求

支持所有主键,包括跨越多个列的复合主键,以及隐藏的 rowid

要自定义持久化方法,你可以提供持久化回调,如下所述。不要尝试覆盖现成的持久化方法。

Upsert(插入或更新)

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;willUpdatearoundUdatedidUdate 不会被调用。

PersistableRecord 提供了三种 upsert 方法

持久化方法和 RETURNING 子句

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
}

还有其他类似的持久化方法,例如 upsertAndFetchsaveAndFetchupdateAndFetchupdateChangesAndFetch 等。它们的行为都类似于 upsertsaveupdateupdateChanges,只不过它们返回保存的值。例如

// Save and return the saved player
let savedPlayer = try player.saveAndFetch(db)

有关更多信息,请参见持久化方法UpsertupdateChanges 方法

批量操作可以返回更新或删除的值

警告:请务必查看 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

可用回调

以下是所有可用的持久化回调的列表,按照它们在各自操作期间被调用的顺序排列

有关每个回调的详细信息,请查看参考文档

MutablePersistableRecord 协议中,willInsertdidInsert 是 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 记录

采用存档协议(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 协议的实现来编码和解码其属性。然而,数据库有特定的要求

有关 Codable 记录的更多信息,请参见

💡 提示:查看 Demo Applications 以获取使用 Codable 记录的示例代码。

JSON 列

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 的 JSONDecoderJSONEncoder。默认情况下,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 }
}

有关所有可用策略,请参阅 DatabaseColumnDecodingStrategyDatabaseColumnEncodingStrategy

Data、Date 和 UUID 编码策略

默认情况下,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
}

有关所有可用策略,请参阅 DatabaseDataDecodingStrategyDatabaseDateDecodingStrategyDatabaseDataEncodingStrategyDatabaseDateEncodingStrategyDatabaseUUIDEncodingStrategy

没有 UUID 解码的自定义,因为 UUID 已经可以解码其所有编码的变体(16 字节的 blobs 和 uuid 字符串,包括大写和小写)。

自定义编码策略适用

它们不适用于基于数据、日期或 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)
}

userInfo 字典

您的 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, ...)

注意:请确保 databaseDecodingUserInfodatabaseEncodingUserInfo 属性被显式声明为 [CodingUserInfoKey: Any]。如果不是,Swift 编译器可能会静默地忽略协议要求,从而导致粘滞的空 userInfo。

提示:从 Coding Keys 派生列

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 方法仅对已更改的列执行数据库更新(如果记录没有更改,则不执行任何操作)。

databaseEquals 方法

此方法返回两个记录是否具有相同的数据库表示。

let oldPlayer: Player = ...
var newPlayer: Player = ...
if newPlayer.databaseEquals(oldPlayer) == false {
    try newPlayer.save(db)
}

注意:比较是在记录的数据库表示上执行的。 只要您的记录类型采用 EncodableRecord 协议,您就不需要关心 Equatable。

databaseChangeshasDatabaseChanges 方法

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 记录带有许多默认行为,这些行为旨在适应大多数情况。 许多这些默认值可以根据您的特定需求进行自定义。

Codable Records 有一些额外的选项。

冲突解决

插入和更新可能会产生冲突:例如,查询可能尝试插入违反唯一索引的重复行。

这些冲突通常以错误结束。 然而,SQLite 允许您更改默认行为,并使用特定策略处理冲突。 例如,INSERT OR REPLACE 语句使用 "replace" 策略处理冲突,该策略替换冲突的行而不是抛出错误。

五种不同的策略是:abort(默认)、replace、rollback、fail 和 ignore。

SQLite 允许您在两个不同的位置指定冲突策略

当您想在查询级别处理冲突时,请在采用 PersistableRecord 协议的类型中指定自定义 persistenceConflictPolicy。 它将更改由 insertupdatesave 持久化方法运行的 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 的未来版本中更改)。

超越 FetchableRecord

一些 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 结构体,并采用您需要的记录协议。

这是定义记录类型最短的方法。

有关更多信息,请参阅 记录协议概述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
}

现在,您可以使用以下方法构建请求:allnoneselectdistinctfiltermatchinggrouphavingorderreversedlimitjoiningincludingwith。 所有这些方法都返回另一个请求,您可以通过应用另一个方法来进一步优化它:Player.select(...).filter(...).order(...)

您可以通过链接这些方法来优化请求

// SELECT * FROM player WHERE (email IS NOT NULL) ORDER BY name
Player.order(nameColumn).filter(emailColumn != nil)

selectordergrouplimit 方法会忽略并替换先前应用的 selection、orderings、grouping 和 limits。 相反,filtermatchinghaving 方法会扩展查询

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 表达式来提供请求

SQL 运算符

📖 SQLSpecificExpressible

GRDB 附带了许多 SQLite 内置运算符的 Swift 版本,如下所示。 但并非全部:有关添加对缺失的 SQL 运算符的支持的方法,请参阅在查询接口请求中嵌入 SQL

SQL 函数

📖 SQLSpecificExpressible

GRDB 附带了许多 SQLite 内置函数的 Swift 版本,如下所示。 但并非全部:有关添加对缺失的 SQL 函数的支持的方法,请参阅在查询接口请求中嵌入 SQL

在查询接口请求中嵌入 SQL

有时您可能需要使用 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
  1. WITH ...: 请参阅 公共表表达式

  2. 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")

    有关 SQLSpecificExpressibleSQLExpression 的更多信息,请参阅 查询接口组织

  3. FROM ...:这里仅支持一个表。您无法自定义此 SQL 部分。

  4. JOIN ...:连接完全由 关联控制。您无法自定义此 SQL 部分。

  5. 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 ...

  6. GROUP BY ...

    GROUP BY 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。

  7. HAVING ...

    HAVING 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。

  8. ORDER BY ...

    ORDER BY 子句可以作为原始 SQL、SQL 插值或 Swift 和 SQL 插值的混合方式提供,就像选择和 WHERE 子句一样(参见上面)。

    为了支持 descasc 查询接口运算符以及 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()
  9. 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?

有关 fetchCursorfetchAllfetchSetfetchOne 方法的信息,请参阅 获取方法

有时您想获取其他值。.

最简单的方法是将请求用作所需类型的获取方法的参数。

// 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?

您还可以更改请求,使其知道它必须获取的类型。

按键获取

根据主键获取记录是一项常见的任务。

可识别的记录可以使用类型安全的方法 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]

要构建自定义请求,您可以使用内置请求之一或从其他请求派生请求。

加密

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 方法采用可选的 pagesPerStepprogress 参数。 这些参数可以一起用于跟踪数据库备份的进度并中止未完成的备份。

提供 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 抛出异常,则备份不受影响,并且该错误将被静默忽略。

警告:将非默认值 pagesPerStepprogress 传递给备份方法是一种高级 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.writeDatabase.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_INTERRUPTSQLITE_ABORT 错误

do {
    try dbPool.write { db in ... }
} catch DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ABORT {
    // Oops, the database was interrupted.
}

有关更多信息,请参阅 中断长时间运行的查询

避免 SQL 注入

SQL 注入是一种让攻击者摧毁您的数据库的技术。

XKCD: Exploits of a Mom

https://xkcd.com/327/

这是一个容易受到 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 可能会抛出 DatabaseErrorRecordError,或者通过 致命错误 崩溃您的程序。

考虑到本地数据库不是从远程服务器加载的一些 JSON,GRDB 专注于受信任的数据库。 处理 不受信任的数据库 需要格外小心。

DatabaseError

📖 DatabaseError

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 发行说明 遗憾的是对此不太清楚:请谨慎编写对扩展结果代码的处理。

RecordError

📖 RecordError

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).")
}

致命错误

致命错误通知程序或数据库必须更改。

它们揭示了程序员的错误、错误的假设并防止误用。 以下是一些示例

如何处理不受信任的输入

让我们考虑以下代码

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
        }
    }
}

有关更多信息,请参见StatementDatabaseValue

错误日志

可以配置SQLite,以便在发生异常时调用包含错误代码和简短错误消息的回调函数。

必须在应用程序生命周期的早期配置此全局错误回调

Database.logError = { (resultCode, message) in
    NSLog("%@", "SQLite error \(resultCode): \(message)")
}

警告:必须在打开任何数据库连接之前设置Database.logError。 这包括您的应用程序使用GRDB打开的连接,以及其他工具(例如第三方库)打开的连接。 在打开连接后进行设置是滥用SQLite,并且无效。

有关更多信息,请参见错误和警告日志

Unicode

SQLite允许您在数据库中存储unicode字符串。

但是,SQLite不提供任何unicode感知的字符串转换或比较。

Unicode函数

内置的SQLite函数UPPERLOWER不是unicode感知的

// "JéRôME"
try String.fetchOne(db, sql: "SELECT UPPER('Jérôme')")

GRDB使用调用Swift内置字符串函数capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercasedSQL函数扩展了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感知比较

可以将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上的内存管理

iOS操作系统喜欢不消耗太多内存的应用程序。

数据库队列在应用程序收到内存警告以及应用程序进入后台时,会自动释放非必要的内存。

您可以选择退出此自动内存管理

var config = Configuration()
config.automaticMemoryManagement = false
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config) // or DatabasePool

常见问题

常见问题解答:打开连接

常见问题解答:SQL

常见问题解答:常规

常见问题解答:关联

常见问题解答:ValueObservation

常见问题解答:错误

常见问题解答:打开连接

如何在我的应用程序中创建数据库?

首先,为数据库文件选择合适的位置。 基于文档的应用程序将让用户选择一个位置。 使用数据库作为全局存储的应用程序将更喜欢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. 
}

如何关闭数据库连接?

DatabaseQueueDatabasePool实例被反初始化时,数据库连接会自动关闭。

如果程序的正确执行依赖于精确的数据库关闭,请显式调用close()。 此方法可能会失败并创建僵尸连接,因此请检查其详细文档。

常见问题解答:SQL

如何将请求打印为SQL?

当您要调试未传递预期结果的请求时,您可能需要打印实际执行的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稳定性?

否,GRDB不支持库演进和ABI稳定性。 唯一的承诺是根据语义版本控制的API稳定性,但实验性功能除外。

但是,可以使用“为分发构建库” Xcode选项 (BUILD_LIBRARY_FOR_DISTRIBUTION) 构建 GRDB,以便您可以方便地构建二进制框架。

常见问题解答:关联

如何过滤记录并仅保留与另一个记录关联的记录?

假设您有两种记录类型,BookAuthor,并且您只想获取有作者的书籍,并丢弃匿名书籍。

我们首先定义书籍和作者之间的关联

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方法。 实际上,我们没有任何条件可以在任何列上表达。 相反,我们只需要“要求一本书可以加入到它的作者”。

有关相反的问题,请参阅下面的如何过滤记录并仅保留未与另一个记录关联的记录?

如何过滤记录并仅保留未与另一个记录关联的记录?

假设您有两种记录类型,BookAuthor,并且您只想获取没有任何作者的匿名书籍。

我们首先定义书籍和作者之间的关联

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,这是另一种说法:这本书没有作者。

请参阅上面如何过滤记录并仅保留与另一个记录相关的记录?以了解相反的问题。

如何仅选择关联记录的一列?

假设您有两种记录类型,BookAuthor,并且您想获取所有书籍及其作者姓名,而不是完整的关联作者记录。

我们首先定义书籍和作者之间的关联

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

为什么ValueObservation不发布值更改?

有时,ValueObservation 看起来并没有通知您期望的更改。

这可能有四个可能的原因

  1. 预期的更改没有提交到数据库中。
  2. 预期的更改已提交到数据库中,但很快就被覆盖了。
  3. 观察已停止。
  4. 观察没有跟踪预期的数据库区域。

要回答前两个问题,请查看数据库执行的 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(...)

查看以 cancelfailure 开头的观察日志:也许观察被您的应用程序取消了,或者确实因错误而失败了。

查看以 value 开头的观察日志:再次确保实际未通知预期值,然后将其覆盖。

最后,查看以 tracked region 开头的观察日志。 打印的数据库区域是否涵盖了预期的更改?

例如

如果您碰巧使用 ValueObservation.trackingConstantRegion(_:) 方法,并且发现跟踪的区域与您的预期不符,请使用 tracking(_:) 更改观察的定义。 您应该看到以 tracked region 开头的日志现在会演变以包含预期的更改,并且您会收到预期的通知。

如果在完成所有这些步骤后(谢谢!),您的观察仍然失败,请打开一个 issue并提供一个最小可重现示例

常见问题解答:错误

无法推断泛型参数“T”

在使用数据库队列和池的 readwrite 方法时,您可能会收到此错误

// 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, ...)
}

在并发执行的代码中改变捕获的变量

insertsave 持久化方法可能会在异步上下文中触发编译器错误

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

发生这种情况时,请首选 insertedsaved 方法

// 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

SQLite错误1“没有这样的列”

此错误消息是不言自明的:请检查拼写错误或不存在的列名。

但是,有时仅当应用程序在最新的操作系统(iOS 14+、Big Sur+ 等)上运行时才会发生此错误。 在以前的版本中不会发生此错误。

如果是这种情况,则有两种可能的解释

  1. 也许列名确实拼写错误或从数据库模式中丢失。

    要找到它,请检查随 DatabaseError 一起的 SQL 语句。

  2. 也许应用程序正在使用字符 " 而不是单引号 ' 作为原始 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错误10“磁盘I/O错误”,SQLite错误23“未授权”

这些错误可能是 SQLite 无法访问数据库的信号,原因是数据保护

当您的应用程序应该能够在锁定的设备上在后台运行时,它必须捕获此错误,例如,等待 UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:)UIApplicationProtectedDataDidBecomeAvailable 通知,然后重试失败的数据库操作。

do {
    try ...
} catch DatabaseError.SQLITE_IOERR, DatabaseError.SQLITE_AUTH {
    // Handle possible data protection error
}

也可以通过使用更宽松的文件保护来完全避免此错误。

带有LIKE查询的SQLite错误21“语句参数数量错误”

执行类似于以下的 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])
}

示例代码


感谢


URI 不会更改:是人们更改了它们。

添加对缺少的 SQL 函数或运算符的支持

本章已重命名为在查询界面请求中嵌入 SQL

高级 DatabasePool

本章已移动

提交后钩子

本章节已移动

异步 API

本章已移动

变更追踪

本章节已重命名为记录比较

并发

本章已移动

自定义值类型

自定义值类型遵循 DatabaseValueConvertible 协议。

自定义数据库行解码

本章节已重命名为 超越 FetchableRecord

自定义持久化方法

本章节已被持久化回调取代。

数据库变更观察

本章节已移动

数据库配置

本章节已移动

数据库队列

本章节已移动

数据库连接池

本章节已移动

数据库快照

本章已移动

DatabaseWriter 和 DatabaseReader 协议

本章节已删除。请参阅 DatabaseReaderDatabaseWriter 的参考。

Date 和 UUID 编码策略

本章节已重命名为 Data、Date 和 UUID 编码策略

处理外部连接

本章节已被共享数据库指南取代。

数据库队列和连接池之间的差异

本章已移动

启用 FTS5 支持

自 GRDB 6.7.0 起,默认启用 FTS5。

FetchedRecordsController

FetchedRecordsController 已在 GRDB 5 中删除。

数据库观察章节介绍了观察数据库的其他方法。

全文搜索

本章节已移动

保证和规则

本章已移动

连接查询支持

本章节已被 splittingRowAdapters(columnCounts:) 的文档取代。

记录方法列表

请参阅记录和查询接口

迁移

本章节已移动

NSNumber 和 NSDecimalNumber

本章节已移动

Persistable 协议

该协议已在 GRDB 3.0 中重命名为 PersistableRecord

PersistenceError

此错误已重命名为 RecordError

预处理语句

本章节已移动

记录类

Record 类是旧版的 GRDB 类型。自 GRDB 7 以来,不建议通过继承 Record 类来定义记录类型。

行适配器

本章节已移动

RowConvertible 协议

该协议已在 GRDB 3.0 中重命名为 FetchableRecord

TableMapping 协议

该协议已在 GRDB 3.0 中重命名为 TableRecord

事务和保存点

本章节已移动

事务钩子

本章节已移动

TransactionObserver 协议

本章节已移动

不安全的并发 API

本章已移动

ValueObservation

本章节已移动

ValueObservation 和 DatabaseRegionObservation

本章节已被 ValueObservationDatabaseRegionObservation 取代。