GRDBCipher 3.7.0

GRDBCipher 3.7.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2019年3月
SPM支持SPM

Gwendal RouéGwendal RouéGwendal Roué维护。



  • 作者:
  • Gwendal Roué

GRDB 3 Swift Swift Platforms License Build Status

一个针对SQLite数据库,重点在应用开发上的工具包

最新版本:2019年3月9日 • 版本 3.7.0 • 变更日志从GRDB 2迁移到GRDB 3

要求:iOS 8.0+ / macOS 10.9+ / watchOS 2.0+ • Swift 4.1+ / Xcode 9.3+

Swift版本 GRDB版本
Swift 5 v4.0.0 🚧开发中
Swift 4.2 v3.7.0
Swift 4.1 v3.7.0
Swift 4 v2.10.0
Swift 3.2 v1.3.0
Swift 3.1 v1.3.0
Swift 3 v1.0
Swift 2.3 v0.81.2
Swift 2.2 v0.80.2

联系方式:

这是什么?

GRDB提供了对原始SQL和高级SQLite特性的直接访问,因为有时人们喜欢锋利的工具。它具有强大的并发原语,因此多线程应用程序可以高效地使用它们的数据库。它为您的应用程序提供具有持久性和检索方法的模型,这样当您不想处理SQL和原始数据库行时,您就不必这样做。

SQLite.swiftFMDB相比,GRDB可以为您节省大量的粘合代码。与Core DataRealm相比,它可以使您的多线程应用程序更简化。

它附带最新的文档通用指南,并且速度快

如果您正在寻找您最喜欢的数据库库,请查看为什么选择GRDB?


特性用法安装文档演示应用程序常见问题解答


特性

GRDB附带了

增强和扩展GRDB的配套库

  • RxGRDB:使用RxSwift以响应式方式跟踪数据库更改。
  • GRDBObjc:将GRDB与FMDB兼容的绑定。

用法

连接到SQLite数据库
import GRDB

// Simple database connection
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")

// Enhanced multithreading based on SQLite's WAL mode
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

参见数据库连接

执行SQL语句
try dbQueue.write { db in
    try db.execute("""
        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("""
        INSERT INTO place (title, favorite, latitude, longitude)
        VALUES (?, ?, ?, ?)
        """, arguments: ["Paris", true, 48.85341, 2.3488])
    
    let parisId = db.lastInsertedRowID
}

参见执行更新

检索数据库行和数据
try dbQueue.read { db in
    // Fetch database rows
    let rows = try Row.fetchCursor(db, "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, "SELECT COUNT(*) FROM place")! // Int
    let placeTitles = try String.fetchAll(db, "SELECT title FROM place") // [String]
}

let placeCount = try dbQueue.read { db in
    try Int.fetchOne(db, "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.write { db in
    // Place?
    let paris = try Place.fetchOne(db, key: 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, "SELECT * FROM place")
}

参见查询接口

接收数据库更改通知
let request = Place.order(Column("title"))
try ValueObservation
    .trackingAll(request)
    .start(in: dbQueue) { places: [Place] in
        print("Places have changed.")
    }

参见数据库更改观察

文档

GRDB在SQLite之上运行:您应该熟悉SQLite常见问题解答。有关一般和详细信息,请参见SQLite文档

参考

入门

SQLite和SQL

记录和查询接口

应用工具

须知事项

通用指南和最佳实践

常见问题解答

示例代码

安装

以下安装步骤使GRDB使用目标操作系统中包含的SQLite版本。

有关与SQLCipher一起安装GRDB的安装程序,请参阅加密

有关与自定义构建的SQLite 3.25.2一起安装GRDB的安装程序,请参阅自定义SQLite构建

有关支持FTS5全文引擎的GRDB的安装程序,请参阅启用FTS5支持

CocoaPods

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

use_frameworks!
pod 'GRDB.swift'

Swift 包管理器

Swift 包管理器 可自动化 Swift 代码的分布。要使用 GRDB 与 SPM,请将依赖项添加到您的 Package.swift 文件中。

let package = Package(
    dependencies: [
        .package(url: "https://github.com/groue/GRDB.swift.git", from: "3.7.0")
    ]
)

请注意,当前不支持 Linux。

Carthage

Carthage 可以构建 GRDB 框架,但也可能会无缘无故地失败。因此,此安装方法 不受支持

如果您不顾此警告仍决定使用 Carthage,并遇到任何 Carthage 相关错误,请打开 Carthage 仓库 的问题,在 Stack Overflow 上提问,召唤您本地的 Xcode 高手,或者提交一个包含 make test_CarthageBuild 命令的拉取请求(一次成功是不够的)。有关更多信息,请参阅 #262

手动安装

  1. 下载 GRDB 的副本,或通过克隆其仓库并使用使用 git checkout v3.7.0 命令确保使用最新标记的版本。

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

  3. 在您的应用程序目标(为 WatchOS 的扩展目标)的 目标依赖项 部分添加 GRDBOSXGRDBiOSGRDBWatchOS 目标(在 构建阶段 标签页的 目标依赖项 部分中)。

  4. 将目标平台上的 GRDB.framework 添加到您的应用程序目标(为 WatchOS 的扩展目标)的 通用 标签页的 嵌入二进制文件 部分。

💡 提示:请参阅 演示应用程序 来了解这种集成的示例。

演示应用程序

该仓库附带了一个 演示应用程序,该应用程序展示了以下内容:

数据库连接

GRDB提供了两个用于访问SQLite数据库的类:`DatabaseQueue` 和 `DatabasePool`

import GRDB

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

它们的不同之处在于:

  • 数据库池允许并发数据库访问(这可以提高多线程应用程序的性能)。
  • 数据库池(除非为只读模式)在WAL模式下打开您的SQLite数据库。
  • 数据库队列支持内存数据库

如果您不确定,请选择DatabaseQueue。 您以后总是可以切换到DatabasePool。

数据库队列

使用数据库文件路径打开数据库队列

import GRDB

let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let inMemoryDBQueue = DatabaseQueue()

如果数据库文件不存在,SQLite会创建该数据库文件。数据库队列被销毁时连接会关闭。

数据库队列可以从任何线程中使用。 `write` 和 `read` 方法是同步的,并且在当前线程堵塞直到数据库语句在受保护的调度队列中执行完毕

// Modify the database:
try dbQueue.write { db in
    try db.create(table: "place") { ... }
    try Place(...).insert(db)
}

// Read values:
try dbQueue.read { db in
    let places = try Place.fetchAll(db)
    let placeCount = try Place.fetchCount(db)
}

数据库访问方法可以返回值

let placeCount = try dbQueue.read { db in
    try Place.fetchCount(db)
}

let newPlaceCount = try dbQueue.write { db -> Int in
    try Place(...).insert(db)
    return try Place.fetchCount(db)
}

数据库队列序列化对数据库的访问,这意味着任何时候都不会有多个线程使用数据库。

  • 当不需要修改数据库时,优先使用`read`方法。它可以防止对数据库进行任何修改。

  • `write`方法将您的数据库语句包装在一个事务中,只有在没有错误发生时才会提交此事务。在 pierwszej nieobsłużonej błędem, wszystkie zmiany są cofnięte, cała transakcja jest unroll'owana i błąd jest ponownie rzucany.

    需要精确事务处理时,请参阅《事务和保存点》。

数据库队列需要您的应用程序遵循规则才能提供其安全保证。 请参考《并发》章节。

💡 提示:请参阅示例应用程序中的示例代码,该代码在iOS上设置数据库队列。

数据库Queue配置

var config = Configuration()
config.readonly = true
config.foreignKeysEnabled = true // Default is already true
config.trace = { print($0) }     // Prints all SQL statements
config.label = "MyDatabase"      // Useful when your app opens multiple databases

let dbQueue = try DatabaseQueue(
    path: "/path/to/database.sqlite",
    configuration: config)

请参阅配置获取更多详细信息。

数据库连接池

数据库连接池允许并发访问数据库。

import GRDB
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

当SQLite发现数据库文件不存在时,它会创建该文件。当数据库连接池被释放时,连接将被关闭。

☝️ 注意:除非设置为只读,否则数据库连接池将以SQLite的"WAL模式"打开数据库。WAL模式并不适用于所有情况。请参阅https://www.sqlite.org/wal.html获取更多信息。

数据库连接池可以从任何线程中访问。 writeread 方法是同步的,会阻塞当前线程,直到在受保护的派遣队列中执行数据库语句。

// Modify the database:
try dbPool.write { db in
    try db.create(table: "place") { ... }
    try Place(...).insert(db)
}

// Read values:
try dbPool.read { db in
    let places = try Place.fetchAll(db)
    let placeCount = try Place.fetchCount(db)
}

数据库访问方法可以返回值

let placeCount = try dbPool.read { db in
    try Place.fetchCount(db)
}

let newPlaceCount = try dbPool.write { db -> Int in
    try Place(...).insert(db)
    return try Place.fetchCount(db)
}

数据库连接池允许多线程同时访问数据库。

  • 当您不需要修改数据库时,建议使用read方法,因为多个线程可以并行执行读取操作。

    读取通常是非阻塞的,除非达到最大并发读取数。在这种情况下,读取操作必须等待另一个读取操作完成。该最大数可以配置

  • 读取保证有一个不可变的数据库提交状态视图,无论并行写入如何。这种隔离称为快照隔离

  • 与读取不同,写入是序列化的。写入数据库的线程永远不会多于一个。

  • `write`方法将您的数据库语句包装在一个事务中,只有在没有错误发生时才会提交此事务。在 pierwszej nieobsłużonej błędem, wszystkie zmiany są cofnięte, cała transakcja jest unroll'owana i błąd jest ponownie rzucany.

    需要精确事务处理时,请参阅《事务和保存点》。

  • 数据库连接池可以抓取数据库的快照。

数据库连接池需要您的应用程序遵循规则,才能提供其安全保证。 请参阅并发章节以获取有关数据库连接池的更多信息,包括它与数据库队列的不同之处以及高级用例。

💡 提示:请参阅示例应用程序,其中包含在iOS上设置数据库队列的示例代码,只需将DatabaseQueue替换为DatabasePool即可。

数据库连接池配置

var config = Configuration()
config.readonly = true
config.foreignKeysEnabled = true // Default is already true
config.trace = { print($0) }     // Prints all SQL statements
config.label = "MyDatabase"      // Useful when your app opens multiple databases
config.maximumReaderCount = 10   // The default is 5

let dbPool = try DatabasePool(
    path: "/path/to/database.sqlite",
    configuration: config)

请参阅配置获取更多详细信息。

数据库连接池比数据库连接队列更占用内存。有关更多信息,请参阅内存管理

SQLite API

在本文档的这一部分,我们将讨论SQL。 如果您不熟悉SQL,请跳转到查询接口

高级主题

执行更新

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

例如

try dbQueue.write { db in
    try db.execute("""
        CREATE TABLE player (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            score INT)
        """)
    
    try db.execute(
        "INSERT INTO player (name, score) VALUES (?, ?)",
        arguments: ["Barbara", 1000])
    
    try db.execute(
        "UPDATE player SET score = :score WHERE id = :id",
        arguments: ["score": 1000, "id": 1])
    }
}

SQL查询中的?和冒号前缀的键如:score参数语句。您可以通过数组或字典传递参数,如上面的示例所示。有关支持的参数类型(布尔值、整数、字符串、日期、Swift枚举等)的更多信息,请参阅,有关SQLite参数的详细文档请参阅StatementArguments

绝对不要直接在SQL字符串中嵌入值,始终使用参数。有关更多信息,请参阅避免SQL注入

// WRONG
let id = 123
let name = textField.text
try db.execute("UPDATE player SET name = '\(name)' WHERE id = \(id)")

// CORRECT
try db.execute(
    "UPDATE player SET name = :name WHERE id = :id",
    arguments: ["name": name, "id": id])

使用分号连接多个语句:

try db.execute("""
    INSERT INTO player (name, score) VALUES (?, ?);
    INSERT INTO player (name, score) VALUES (?, ?)
    """, arguments: ["Arthur", 750, "Barbara", 1000])

当您想确保执行单个语句时,请使用准备好的语句

执行INSERT语句后,您可以获取插入行的行ID

try db.execute(
    "INSERT INTO player (name, score) VALUES (?, ?)",
    arguments: ["Arthur", 1000])
let playerId = db.lastInsertedRowID

不要错过记录,它们提供了经典的持久方法

let 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, "SELECT * FROM wine WHERE id = ?", arguments: [1]) {
        let name: String = row["name"]
        let color: Color = row["color"]
        print(name, color)
    }
}

是存储在行列中的布尔值、整数、字符串、日期、Swift枚举等。

try dbQueue.read { db in
    let urls = try URL.fetchCursor(db, "SELECT url FROM wine")
    while let url = try urls.next() {
        print(url)
    }
}

记录是您可以从行初始化的应用程序对象

let wines = try dbQueue.read { db in
    try Wine.fetchAll(db, "SELECT * FROM wine")
}

获取方法

在整个GRDB中,您始终可以获取任何可获取类型的游标数组单个值(数据库、简单的或自定义记录)。

try Row.fetchCursor(...) // A Cursor of Row
try Row.fetchAll(...)    // [Row]
try Row.fetchOne(...)    // Row?
  • fetchCursor返回一个游标,用于获取值

    let rows = try Row.fetchCursor(db, "SELECT ...") // A Cursor of Row
  • fetchAll返回一个数组

    let players = try Player.fetchAll(db, "SELECT ...") // [Player]
  • fetchOne返回一个单个可选值,并消耗一个数据库行(如果有)。

    let count = try Int.fetchOne(db, "SELECT COUNT(*) ...") // Int?

光标

每次从数据库中读取多行时,您可以获取一个数组或一个光标。.

fetchAll() 方法返回一个常规的 Swift 数组,您可以像迭代所有其他数组一样迭代它。

try dbQueue.read { db in
    // [Player]
    let players = try Player.fetchAll(db, "SELECT ...")
    for player in players {
        // use player
    }
}

与数组不同,fetchCursor() 返回的光标是逐步加载其结果的。

try dbQueue.read { db in
    // Cursor of Player
    let players = try Player.fetchCursor(db, "SELECT ...")
    while let player = try players.next() {
        // use player
    }
}

数组和光标都可以遍历数据库结果。您如何选择其一?看看它们的区别。

  • 光标不能在任何线程上使用:您必须在创建光标的调度队列上消耗光标。特别是,不要从数据库访问方法中提取光标。

    // Wrong
    let cursor = try dbQueue.read { db in
        try Player.fetchCursor(db, ...)
    }
    while let player = try cursor.next() { ... }

    相反,数组可以在任何线程上消耗。

    // OK
    let array = try dbQueue.read { db in
        try Player.fetchAll(db, ...)
    }
    for player in array { ... }
  • 光标只能迭代一次。数组可以多次迭代。

  • 光标以延迟的方式迭代数据库结果,并且不消耗太多内存。数组包含数据库值的副本,并且在有许多检索结果时可能需要大量内存。

  • 光标提供了对 SQLite 的直接访问,而数组必须花费时间来复制数据库值。如果您需要额外的性能,您可能会更喜欢光标。

  • 光标采用 Cursor 协议,这与 Swift 的标准 延迟序列 非常相似。 因此,光标附带了许多便利方法:compactMapcontainsdropFirstdropLastdrop(while:)enumeratedfilterfirstflatMapforEachjoinedjoined(separator:)maxmax(by:)minmin(by:)mapprefixprefix(while:)reducereduce(into:)suffix

    // Prints all Github links
    try URL
        .fetchCursor(db, "SELECT url FROM link")
        .filter { url in url.host == "github.com" }
        .forEach { url in print(url) }
    
    // An efficient cursor of coordinates:
    let locations = try Row.
        .fetchCursor(db, "SELECT latitude, longitude FROM place")
        .map { row in
            CLLocationCoordinate2D(latitude: row[0], longitude: row[1])
        }
    
    // Turn cursors into arrays or sets:
    let array = try Array(cursor)
    let set = try Set(cursor)
  • 光标不是 Swift 序列。 这是因为 Swift 序列无法处理迭代错误,而读取 SQLite 结果可能在任何时候失败。SQL 函数可能会引发错误。在 iOS 上,数据保护 可能会阻止在后台访问数据库文件。在 macOS 上,您的应用程序用户可能会破坏文件系统。

  • 光标需要一点小心。:

    • 不要在光标迭代过程中修改结果。

      // Undefined behavior
      while let player = try players.next() {
          try db.execute("DELETE ...")
      }
    • 不要将 Row 光标转换为数组。您不会得到预期的唯一行。要获取行的数组,请使用 Row.fetchAll(...)。一般来说,确保在稍后使用时从光标中提取每次复制一行:row.copy()

如果您没有看到或不在乎差异,请使用数组。如果您关心内存和性能,请根据需要使用光标。

行查询

获取行

获取行、数组或单独行的指针(见获取方法

try dbQueue.read { db in
    try Row.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Row
    try Row.fetchAll(db, "SELECT ...", arguments: ...)    // [Row]
    try Row.fetchOne(db, "SELECT ...", arguments: ...)    // Row?
    
    let rows = try Row.fetchCursor(db, "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, "SELECT * FROM player")
}

参数可以是可选数组或字典,用于填充查询中的位置 ? 和冒号前缀键,例如 :name

let rows = try Row.fetchAll(db,
    "SELECT * FROM player WHERE name = ?",
    arguments: ["Arthur"])

let rows = try Row.fetchAll(db,
    "SELECT * FROM player WHERE name = :name",
    arguments: ["name": "Arthur"])

有关支持的参数类型(布尔型、整型、字符串、日期、Swift 枚举等)的更多信息,请参阅,以及有关 SQLite 参数详细文档的 StatementArguments

与包含数据库行副本的行数组不同,行游标接近 SQLite 金属,需要小心处理

☝️ 不要将 Row 指针转换为数组。您将不会得到期望的独行列。要获取行数组,请使用 Row.fetchAll(...)。一般来说,每次从游标中提取行以供后续使用时,请确保复制行: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

一般来说,您可以提取所需类型,前提是它可以从 SQLite 基础值转换

  • 成功的转换包括

    • 所有数值 SQLite 值转换成所有数值 Swift 类型,以及布尔型(只有零是假布尔值)。
    • 文本 SQLite 值转换为 Swift 字符串。
    • Blob SQLite 值转换为 Foundation Data。

    有关支持的类型(布尔型、整型、字符串、日期、Swift 枚举等)的更多信息,请参阅

  • NULL 返回 nil。

    let row = try Row.fetchOne(db, "SELECT NULL")!
    row[0] as Int? // nil
    row[0] as Int  // fatal error: could not convert NULL to Int.

    不过有一个例外:DatabaseValue 类型

    row[0] as DatabaseValue // DatabaseValue.null
  • 缺失的列返回 nil。

    let row = try Row.fetchOne(db, "SELECT 'foo' AS foo")!
    row["missing"] as String? // nil
    row["missing"] as String  // fatal error: no such column: missing

    您可以使用 hasColumn 方法显式检查列是否存在

  • 无效的转换会导致抛出致命错误。

    let row = try Row.fetchOne(db, "SELECT 'Mom’s birthday'")!
    row[0] as String // "Mom’s birthday"
    row[0] as Date?  // fatal error: could not convert "Mom’s birthday" to Date.
    row[0] as Date   // fatal error: could not convert "Mom’s birthday" to Date.
    
    let row = try Row.fetchOne(db, "SELECT 256")!
    row[0] as Int    // 256
    row[0] as UInt8? // fatal error: could not convert 256 to UInt8.
    row[0] as UInt8  // fatal error: could not convert 256 to UInt8.

    这些转换致命错误可以通过使用 DatabaseValue 类型避免

    let row = try Row.fetchOne(db, "SELECT 'Mom’s birthday'")!
    let dbValue: DatabaseValue = row[0]
    if dbValue.isNull {
        // Handle NULL
    } else if let date = Date.fromDatabaseValue(dbValue) {
        // Handle valid date
    } else {
        // Handle invalid date
    }

    这种额外的详尽性是必须处理不受信任的数据库的结果:您可以考虑修复数据库的内容。有关更多信息,请参阅致命错误

  • SQLite 有一种较弱的数据类型系统,并提供了一些便捷转换,例如将 String 转换为 Int、Double 转换为 Blob 等。

    GRDB 有时会允许这些转换通过

    let rows = try Row.fetchCursor(db, "SELECT '20 small cigars'")
    while let row = try rows.next() {
        row[0] as Int   // 20
    }

    不要慌张:这些转换并没有阻止 SQLite 成为您想要使用的极其成功的数据库引擎。GRDB 还添加了上述描述的安全检查。您还可以通过使用 DatabaseValue 类型完全防止这些便捷转换。

数据库值

数据库值是在SQLite和你的值之间的一种中间类型,它提供了有关数据库中存储的原始值的信息。

你像其他值类型一样获取数据库值

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

您可以使用DatabaseValueConvertible.fromDatabaseValue()方法从数据库值中提取常规值(布尔值、整数、字符串、日期、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, "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

作为字典的行

行实现了标准RandomAccessCollection协议,可以被视为一个包含数据库值的字典

// 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, "SELECT 1 AS foo, 2 AS foo")!
row.columnNames    // ["foo", "foo"]
row.databaseValues // [1, 2]
row["foo"]         // 1 (leftmost matching column)
for (columnName, dbValue) in row { ... } // ("foo", 1), ("foo", 2)

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

  • 在名称重复列时,保持左侧值的[String: DatabaseValue]字典

    let dict = Dictionary(row, uniquingKeysWith: { (left, _) in left })
  • 在名称重复列时,保留右侧值的[String: AnyObject]字典。这个字典与FMResultSet的FMDB中的resultDictionary相同。它包含null列的NSNull值,并且可以与Objective-C共享

    let dict = Dictionary(
        row.map { (column, dbValue) in
            (column, dbValue.storage.value as AnyObject)
        },
        uniquingKeysWith: { (_, right) in right })
  • 一个能够提供例如JSONSerialization的[String: Any]字典

    let dict = Dictionary(
        row.map { (column, dbValue) in
            (column, dbValue.storage.value)
        },
        uniquingKeysWith: { (left, _) in left })

有关更多信息,请参阅Dictionary.init(_:uniquingKeysWith:)

值查询

您可以直接获取,而不一定使用行。像行一样,以游标数组单个值(请参阅获取方法)的形式获取它们。值是从SQL查询的左侧列提取的

try dbQueue.read { db in
    try Int.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Int
    try Int.fetchAll(db, "SELECT ...", arguments: ...)    // [Int]
    try Int.fetchOne(db, "SELECT ...", arguments: ...)    // Int?
    
    // When database may contain NULL:
    try Optional<Int>.fetchCursor(db, "SELECT ...", arguments: ...) // A Cursor of Int?
    try Optional<Int>.fetchAll(db, "SELECT ...", arguments: ...)    // [Int?]
}

let playerCount = try dbQueue.read { db in
    try Int.fetchOne(db, "SELECT COUNT(*) FROM player")!
}

fetchOne返回一个可选值,在以下两种情况下为nil:SELECT语句没有产出行,或者行中有一个NULL值。

有许多支持的数据类型(布尔值、整数、字符串、日期、Swift枚举等)。有关更多信息,请参阅

let count = try Int.fetchOne(db, "SELECT COUNT(*) FROM player")! // Int
let urls = try URL.fetchAll(db, "SELECT url FROM link")          // [URL]

GRDB 随带内置对以下值类型的支持

值可以用作 语句参数

let url: URL = ...
let verified: Bool = ...
try db.execute(
    "INSERT INTO link (url, verified) VALUES (?, ?)",
    arguments: [url, verified])

值可以从行 中提取

let rows = try Row.fetchCursor(db, "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, "SELECT url FROM link")  // [URL]

记录 中使用值

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, "SELECT data, ...")
while let row = try rows.next() {
    let data: Data = row["data"]
}

在请求迭代的每个步骤中,row[] 下标创建 两个副本 的数据库字节:一个由 SQLite 获取,另一个存储在 Swift Data 值中。

您有机会节省内存,通过不复制 SQLite 获取的数据

while let row = try rows.next() {
    let data = row.dataNoCopy(named: "data") // Data?
}

未复制的数据不超过迭代步骤的持续时间:请确保您不会在此之后使用它。

日期和 DateComponents

日期DateComponents 可以存储和从数据库中检索。

以下是 GRDB 如何支持 SQLite 支持的各种 日期格式 的方法

SQLite 格式 日期 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 时区。缺失的组件假定为零。

² GRDB 2+ 将数值解释为 Date(timeIntervalSince1970:) 的时间戳。之前的 GRDB 版本将数字解释为 儒略日。儒略日仍然支持,使用 Date(julianDay:) 初始化器。

日期

日期可以像其他一样存入和从数据库中取出。

try db.execute(
    "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”格式存储,保留到毫秒级别。

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

  • 可比较的(ORDER BY date工作)
  • 与SQLite关键字CURRENT_TIMESTAMP(WHERE date > CURRENT_TIMESTAMP工作)可比较
  • 能够提供SQLite日期和时间函数
  • 足够精确

当默认格式不符合您的要求时,您可以自定义日期转换。例如

try db.execute(
    "INSERT INTO player (creationDate, ...) VALUES (?, ...)",
    arguments: [Date().timeIntervalSinceReferenceDate, ...])

let row = try Row.fetchOne(db, ...)!
let creationDate = Date(timeIntervalSinceReferenceDate: row["creationDate"])

请参阅可编码记录了解更多日期自定义选项。

日期组件

日期组件通过DatabaseDateComponents辅助类型间接支持。

DatabaseDateComponents可以读取所有由SQLite所支持的日期格式,并将它们以您选择的格式存储,从HH:MM到YYYY-MM-DD HH:MM:SS.SSS。

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(
    "INSERT INTO player (birthDate, ...) VALUES (?, ...)",
    arguments: [dbComponents, ...])

// Read "1973-09-18"
let row = try Row.fetchOne(db, "SELECT birthDate ...")!
let dbComponents: DatabaseDateComponents = row["birthDate"]
dbComponents.format         // .YMD (the actual format found in the database)
dbComponents.dateComponents // DateComponents

NSNumber和NSDecimalNumber

NSNumber可以像其他一样存入和从数据库中取出。浮点NSNumber以Double存储。整数和布尔值以Int64存储。不适合Int64的整数将不会被存储:您将收到一个致命错误。请小心使用包含UInt64的NSNumber,例如。

NSDecimalNumber值得深入探讨

SQLite不支持十进制数。根据以下表格,SQLite实际上会存储整数或双精度浮点数

CREATE TABLE transfer (
    amount DECIMAL(10,5) -- will store integer or double, actually
)

这意味着计算将不会精确

try db.execute("INSERT INTO transfer (amount) VALUES (0.1)")
try db.execute("INSERT INTO transfer (amount) VALUES (0.2)")
let sum = try NSDecimalNumber.fetchOne(db, "SELECT SUM(amount) FROM transfer")!

// Yikes! 0.3000000000000000512
print(sum)

不要责怪SQLite或GRDB,而是以不同的方式存储您的十进制数。

一种经典技术是存储整数,因为SQLite执行整数的确切计算。例如,不要存储欧元,而是存储分

// Write
let amount = NSDecimalNumber(string: "0.10")
let integerAmount = amount.multiplying(byPowerOf10: 2).int64Value
try db.execute("INSERT INTO transfer (amount) VALUES (?)", arguments: [integerAmount])

// Read
let integerAmount = try Int64.fetchOne(db, "SELECT SUM(amount) FROM transfer")!
let amount = NSDecimalNumber(value: integerAmount).multiplying(byPowerOf10: -2) // 0.10

UUID

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

GRDB 以 16 字节数据 blob 存储 uuids,并从 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(
    "INSERT INTO wine (grape, color) VALUES (?, ?)",
    arguments: [Grape.merlot, Color.red])

// Read
let rows = try Row.fetchCursor(db, "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, "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
}

自定义值类型

数据库的转换基于 DatabaseValueConvertible 协议。

protocol DatabaseValueConvertible {
    /// Returns a value that can be stored in the database.
    var databaseValue: DatabaseValue { get }
    
    /// Returns a value initialized from dbValue, if possible.
    static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self?
}

采用此协议的所有类型都可以像所有其他 (Bool、Int、String、Date、Swift 枚举等)一样使用。

属性 databaseValue 返回 DatabaseValue,一个包装 SQLite 支持的五种值的类型:NULL、Int64、Double、String 和 Date。由于 DatabaseValue 没有公共初始化器,使用 DatabaseValue.null 或另一个已经采用该协议的类型: 1.databaseValue"foo".databaseValue 等。转换到 DatabaseValue 的 必须不失败

工厂方法 fromDatabaseValue() 如果数据库值包含合适的值,则返回您自定义类型的实例。如果数据库值不包含合适的值,例如 Date 中的 "foo",则 fromDatabaseValue 必须 返回 nil(GRDB 将 nil 结果解释为转换错误,并相应地作出反应)。

事务和保存点

事务与安全

事务是SQLite的基本工具,它确保了应用线程与数据库连接之间的数据一致性适当的隔离

GRDB通常为您打开事务,以强制执行其并发保证,并提供对您的应用程序数据和应用程序逻辑的最大安全性

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.write { db in
    try Credit(destinationAccout, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbPool.write { db in
    try Credit(destinationAccout, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

尽管如此,您可能需要精确控制事务发生的时间

显式事务

DatabaseQueue.inDatabase()DatabasePool.writeWithoutTransaction() 在任何事务之外执行您的数据库语句

// INSERT INTO credit ...
// INSERT INTO debit ...
try dbQueue.inDatabase { db in
    try Credit(destinationAccout, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

// INSERT INTO credit ...
// INSERT INTO debit ...
try dbPool.writeWithoutTransaction { db in
    try Credit(destinationAccout, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

在任何事务之外写入是危险的,原因有两个

  • 在我们的借记/贷记示例中,您可能能够成功插入贷记,但插入借记时失败,最终导致账户失衡(哎呀)。

    // UNSAFE DATABASE INTEGRITY
    try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
        try Credit(destinationAccout, amount).insert(db) // may succeed
        try Debit(sourceAccount, amount).insert(db)      // may fail
    }

    事务可以避免这种类型的错误。

  • 数据库池并发读取可以查看数据库的不一致状态

    // UNSAFE CONCURRENCY
    try dbPool.writeWithoutTransaction { db in
        try Credit(destinationAccout, amount).insert(db)
        // <- Concurrent dbPool.read sees a partial db update here
        try Debit(sourceAccount, amount).insert(db)
    }

    事务也可以避免这种类型的错误。

要显式打开事务,请使用 Database.inTransactionDatabaseQueue.inTransactionDatabasePool.writeInTransaction 中的任何一个方法

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.inDatabase { db in  // or dbPool.writeWithoutTransaction
    try db.inTransaction {
        try Credit(destinationAccout, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
        return .commit
    }
}

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.inTransaction { db in  // or dbPool.writeInTransaction
    try Credit(destinationAccout, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
    return .commit
}

如果从事务块中抛出错误,事务将回滚,并通过 inTransaction 方法重新抛出错误。如果您返回 .rollback 而不是 .commit,则事务也将回滚,但不会抛出错误。

您还可以执行手动事务管理

try dbQueue.inDatabase { db in  // or dbPool.writeWithoutTransaction
    try db.beginTransaction()
    ...
    try db.commit()
    
    try db.execute("BEGIN TRANSACTION")
    ...
    try db.execute("ROLLBACK")
}

除非您设置了 allowsUnsafeTransactions 配置标志,否则事务不能保持打开状态

// fatal error: A transaction has been left opened at the end of a database access
try dbQueue.inDatabase { db in
    try db.execute("BEGIN TRANSACTION")
    // <- no commit or rollback
}

您还可以查询当前是否打开了事务

func myCriticalMethod(_ db: Database) throws {
    precondition(db.isInsideTransaction, "This method requires a transaction")
    try ...
}

然而,您有比检查事务更好的选择:应使用以下描述的保存点来处理关键数据库部分

func myCriticalMethod(_ db: Database) throws {
    try db.inSavepoint {
        // Here the database is guaranteed to be inside a transaction.
        try ...
    }
}

保存点

可以回滚保存在一起的语句,而不会使整个事务无效

try dbQueue.write { db in
    // Makes sure both inserts succeed, or none:
    try db.inSavepoint {
        try Credit(destinationAccout, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
        return .commit
    }
    
    // Other savepoints, etc...
}

如果从保存点块中抛出错误,则保存点将回滚,并通过 inSavepoint 方法重新抛出错误。如果您返回 .rollback 而不是 .commit,则保存点也将回滚,但不会抛出错误。

与事务不同,保存点可以嵌套。如果没有在开始保存点时打开事务,它们将隐式打开一个事务。因此,它们的表现就像嵌套事务一样。但是,数据库更改只有在最外层事务提交时才写入磁盘

try dbQueue.inDatabase { db in
    try db.inSavepoint {
        ...
        try db.inSavepoint {
            ...
            return .commit
        }
        ...
        return .commit  // writes changes to disk
    }
}

尽管如此,SQLite保存点不仅是嵌套事务。对于更高级的使用,请参阅 SQLite保存点文档

事务类型

SQLite 支持 三种事务类型:延迟(默认)、立即和独占。

可以在数据库配置中更改事务类型,或者在每次事务中更改。

// 1) Default configuration:
let dbQueue = try DatabaseQueue(path: "...")

// BEGIN DEFERED TRANSACTION ...
dbQueue.write { db in ... }

// BEGIN EXCLUSIVE TRANSACTION ...
dbQueue.inTransaction(.exclusive) { db in ... }

// 2) Customized default transaction kind:
var config = Configuration()
config.defaultTransactionKind = .immediate
let dbQueue = try DatabaseQueue(path: "...", configuration: config)

// BEGIN IMMEDIATE TRANSACTION ...
dbQueue.write { db in ... }

// BEGIN EXCLUSIVE TRANSACTION ...
dbQueue.inTransaction(.exclusive) { db in ... }

预编译语句

预编译语句 允许您预编译一个 SQL 查询,然后稍后执行它,如果需要,可以多次执行,并传递不同的参数。

预编译语句有两种:选择语句更新语句

try dbQueue.write { db in
    let updateSQL = "INSERT INTO player (name, score) VALUES (:name, :score)"
    let updateStatement = try db.makeUpdateStatement(updateSQL)
    
    let selectSQL = "SELECT * FROM player WHERE name = ?"
    let selectStatement = try db.makeSelectStatement(selectSQL)
}

SQL 查询中的 ? 和以冒号开头的键(如 :name)是语句参数。您可以使用数组或字典(实际上是类型为 StatementArguments 的参数,这些参数实现了 ExpressibleByArrayLiteral 和 ExpressibleByDictionaryLiteral 协议)来设置这些参数。

updateStatement.arguments = ["name": "Arthur", "score": 1000]
selectStatement.arguments = ["Arthur"]

设置参数后,您可以执行预编译语句

try updateStatement.execute()

选择语句可以用在任何需要原生 SQL 查询字符串的位置(参见 fetch 查询

let rows = try Row.fetchCursor(selectStatement)    // A Cursor of Row
let players = try Player.fetchAll(selectStatement) // [Player]
let player = try Player.fetchOne(selectStatement)  // Player?

您可以在执行语句时设置参数

try updateStatement.execute(arguments: ["name": "Arthur", "score": 1000])
let player = try Player.fetchOne(selectStatement, arguments: ["Arthur"])

☝️ 注意:重复使用已失败的预编译语句是程序员错误:如果您这样做,GRDB 可能会崩溃。

有关更多信息,请参阅 行查询值查询记录

预编译语句缓存

如果您的应用程序生命周期中要多次使用相同的查询,您可能会自然地想要缓存预编译语句。

不要自己缓存语句。

☝️ 注意:这是因为您没有必需的工具。语句绑定到特定的 SQLite 连接和调度队列,您无法自行管理,特别是当您使用 数据库池 时。数据库模式的变化 可能会或可能不会 使语句无效。在 iOS 8.2 和 OSX 10.10 之前,如果没有 sqlite3_close_v2 函数 的系统上,如果保留语句活动状态,SQLite 连接将无法正确关闭。

相反,请使用 cachedUpdateStatementcachedSelectStatement 方法。GRDB 为您做了所有困难的缓存和 内存管理 工作。

let updateStatement = try db.cachedUpdateStatement(sql)
let selectStatement = try db.cachedSelectStatement(sql)

如果缓存的预编译语句抛出错误,不要重用它(这是程序员错误)。相反,从缓存中重新加载它。

自定义 SQL 函数和聚合

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

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

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

自定义 SQL 函数

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())
}
dbQueue.add(function: reverse)   // Or dbPool.add(function: ...)

try dbQueue.read { db in
    // "oof"
    try String.fetchOne(db, "SELECT reverse('foo')")!
}

参数 function 接受一个 DatabaseValue 的数组,并返回任何有效的 (布尔值,整数,字符串,日期,Swift 枚举等)。数据库值的数量保证为 argumentCount

当函数“纯化”时,SQLite 有机会进行额外的优化,这意味着它们的输出结果仅依赖于它们的参数。因此,当可能时,请确保将 pure 参数设置为 true。

函数可以接受可变数量的参数

当您未提供任何显式的 argumentCount 时,函数可以接受任何数量的参数

let averageOf = DatabaseFunction("averageOf", pure: true) { (values: [DatabaseValue]) in
    let doubles = values.compactMap { Double.fromDatabaseValue($0) }
    return doubles.reduce(0, +) / Double(doubles.count)
}
dbQueue.add(function: averageOf)

try dbQueue.read { db in
    // 2.0
    try Double.fetchOne(db, "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)
}
dbQueue.add(function: sqrt)

// SQLite error 1 with statement `SELECT sqrt(-1)`: invalid negative number
try dbQueue.read { db in
    try Double.fetchOne(db, "SELECT sqrt(-1)")!
}

查询接口 中使用自定义函数

// SELECT reverseString("name") FROM player
Player.select(reverseString.apply(nameColumn))

GRDB 随附一些内置的 SQL 函数,用于执行基于 Unicode 的字符串转换。请参阅 Unicode

自定义聚合

在注册自定义聚合之前,您需要定义一个协议类型,该类型使用 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? {
        return maxLength
    }
}

let maxLength = DatabaseFunction(
    "maxLength",
    argumentCount: 1,
    pure: true,
    aggregate: MaxLength.self)

dbQueue.add(function: maxLength)   // Or dbPool.add(function: ...)

try dbQueue.read { db in
    // Some Int
    try Int.fetchOne(db, "SELECT maxLength(name) FROM player")!
}

聚合的 step 方法接受一个 DatabaseValue 的数组。此数组包含与 argumentCount 参数一样多的值(或当 argumentCount 被省略时,任何数量的值)。

聚合的 finalize 方法返回最终聚合的 (布尔值,整数,字符串,日期,Swift 枚举等)。

当聚合“纯化”时,SQLite 有机会执行额外的优化,这意味着它们的输出结果仅依赖于它们的输入。因此,当可能时,请确保将 pure 参数设置为 true。

查询接口 中使用自定义聚合

// SELECT maxLength("name") FROM player
let request = Player.select(maxLength.apply(nameColumn))
try Int.fetchOne(db, request) // Int?

数据库模式反向工程

GRDB 带有一套模式内省方法

try dbQueue.read { db in
    // Bool, true if the table exists
    try db.tableExists("player")
    
    // [ColumnInfo], the columns in the table
    try db.columns(in: "player")
    
    // PrimaryKeyInfo
    try db.primaryKey("player")
    
    // [ForeignKeyInfo], the foreign keys defined on the table
    try db.foreignKeys(on: "player")
    
    // [IndexInfo], the indexes defined on the table
    try db.indexes(on: "player")
    
    // Bool, true if column(s) is a unique key (primary key or unique index)
    try db.table("player", hasUniqueKey: ["email"])
}

// Bool, true if argument is the name of an internal SQLite table
Database.isSQLiteInternalTable(...)

// Bool, true if argument is the name of an internal GRDB table
Database.isGRDBInternalTable(...)

行适配器

行适配器允许你按照行消费者预期的方式呈现数据库行。

它们基本上帮助两个不兼容的行接口协同工作。例如,行消费者期望有一个名为 "consumed" 的列,但生成的行有一个名为 "produced" 的列。

在这种情况下,使用 ColumnMapping 行适配器就非常方便

// Fetch a 'produced' column, and consume a 'consumed' column:
let adapter = ColumnMapping(["consumed": "produced"])
let row = try Row.fetchOne(db, "SELECT 'Hello' AS produced", adapter: adapter)!
row["consumed"] // "Hello"
row["produced"] // nil

行适配器是采用 RowAdapter 协议的值。你可以实现自己的自定义适配器(🔥 实验),或者使用下面描述的四个内置适配器之一。

要了解如何使用行适配器,请参阅连接查询支持

ColumnMapping

ColumnMapping 更改列名。使用包含适配列名的键和原始行中的列名的值的字典建立一个 ColumnMapping。

// [newName:"Hello"]
let adapter = ColumnMapping(["newName": "oldName"])
let row = try Row.fetchOne(db, "SELECT 'Hello' AS oldName", adapter: adapter)!

SuffixRowAdapter

SuffixRowAdapter 隐藏行中的前几列

// [b:1 c:2]
let adapter = SuffixRowAdapter(fromIndex: 1)
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

RangeRowAdapter

RangeRowAdapter 仅公开一范围的列。

// [b:1]
let adapter = RangeRowAdapter(1..<2)
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

EmptyRowAdapter

EmptyRowAdapter 隐藏所有列。

let adapter = EmptyRowAdapter()
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
row.isEmpty // true

这个限制适配器可能在某些特定情况下很有用。当你需要它时,你会很高兴找到它的。

ScopeAdapter

ScopeAdapter 定义 行范围

let adapter = ScopeAdapter([
    "left": RangeRowAdapter(0..<2),
    "right": RangeRowAdapter(2..<4)])
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

ScopeAdapter 不会改变所获取行的列和值。相反,它定义了 范围,您可以通过 Row.scopes 属性访问这些范围

row                   // [a:0 b:1 c:2 d:3]
row.scopes["left"]    // [a:0 b:1]
row.scopes["right"]   // [c:2 d:3]
row.scopes["missing"] // nil

范围可以嵌套

let adapter = ScopeAdapter([
    "left": ScopeAdapter([
        "left": RangeRowAdapter(0..<1),
        "right": RangeRowAdapter(1..<2)]),
    "right": ScopeAdapter([
        "left": RangeRowAdapter(2..<3),
        "right": RangeRowAdapter(3..<4)])
    ])
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

let leftRow = row.scopes["left"]!
leftRow.scopes["left"]  // [a:0]
leftRow.scopes["right"] // [b:1]

let rightRow = row.scopes["right"]!
rightRow.scopes["left"]  // [c:2]
rightRow.scopes["right"] // [d:3]

任何适配器都可以通过范围进行扩展

let baseAdapter = RangeRowAdapter(0..<2)
let adapter = ScopeAdapter(base: baseAdapter, scopes: [
    "remainder": SuffixRowAdapter(fromIndex: 2)])
let row = try Row.fetchOne(db, "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

row // [a:0 b:1]
row.scopes["remainder"] // [c:2 d:3]

原始 SQLite 指针

如果 GRDB 没有公开所有 SQLite API,您仍然可以使用 SQLite C 接口 并调用 SQLite C 函数

这些函数直接嵌入到 GRDBCustom 和 GRCBCipher 模块中。对于 "常规" GRDB 框架:您需要导入 SQLite3CSQLite,具体取决于您是否使用 Swift 包管理器

#if SWIFT_PACKAGE
    import CSQLite // For Swift Package Manager
#else
    import SQLite3 // Otherwise
#endif

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.makeSelectStatement("SELECT ...")
    let sqliteStatement = statement.sqliteStatement
}

☝️ 注意事项

  • 这些指针由 GRDB 拥有:不要关闭由 GRDB 创建的连接或最终语句。
  • GRDB 以 "多线程模式" 打开 SQLite 连接,这(奇怪地)意味着 它们不是线程安全的。请确保在各自的分配队列内触摸原始数据库和语句。
  • 使用原始 SQLite C 接口的风险自负。GRDB 不会阻止您自陷困境。

在深入到底层之前,以下是 GRDB 所使用的所有 SQLite API 列表

  • sqlite3_aggregate_contextsqlite3_create_function_v2sqlite3_result_blobsqlite3_result_doublesqlite3_result_errorsqlite3_result_error_codesqlite3_result_int64sqlite3_result_nullsqlite3_result_textsqlite3_user_datasqlite3_value_blobsqlite3_value_bytessqlite3_value_doublesqlite3_value_int64sqlite3_value_textsqlite3_value_type:请参阅 自定义 SQL 函数和聚合
  • sqlite3_backup_finishsqlite3_backup_initsqlite3_backup_step:请参阅 备份
  • sqlite3_bind_blobsqlite3_bind_doublesqlite3_bind_int64sqlite3_bind_nullsqlite3_bind_parameter_countsqlite3_bind_parameter_namesqlite3_bind_textsqlite3_clear_bindingssqlite3_column_blobsqlite3_column_bytessqlite3_column_countsqlite3_column_doublesqlite3_column_int64sqlite3_column_namesqlite3_column_textsqlite3_column_typesqlite3_execsqlite3_finalizesqlite3_prepare_v2sqlite3_resetsqlite3_step:请参阅 执行更新获取查询预定义语句
  • sqlite3_busy_handlersqlite3_busy_timeout:请参阅 配置.busyMode
  • sqlite3_changessqlite3_total_changes:请参阅 Database.changesCount 和 Database.totalChangesCount
  • sqlite3_closesqlite3_close_v2sqlite3_next_stmtsqlite3_open_v2:参见数据库连接
  • sqlite3_commit_hooksqlite3_rollback_hooksqlite3_update_hook:参见TransactionObserver 协议DatabaseRegionObservationValueObservationFetchedRecordsController
  • sqlite3_config:参见错误日志
  • sqlite3_create_collation_v2:参见字符串比较
  • sqlite3_db_release_memory:参见内存管理
  • sqlite3_errcodesqlite3_errmsgsqlite3_errstrsqlite3_extended_result_codes:参见错误处理
  • sqlite3_keysqlite3_rekey:参见加密
  • sqlite3_last_insert_rowid:参见执行更新
  • sqlite3_preupdate_countsqlite3_preupdate_depthsqlite3_preupdate_hooksqlite3_preupdate_newsqlite3_preupdate_old:参见支持 SQLite Pre-Update Hooks
  • sqlite3_set_authorizer:由 GRDB 保留
  • sqlite3_sql:参见Statement.sql
  • sqlite3_trace:参见Configuration.trace
  • sqlite3_wal_checkpoint_v2:参见DatabasePool.checkpoint

记录

在 SQLite API 之上,GRDB 提供了协议和类,帮助以“记录”的形式操作数据库行

try dbQueue.write { db in
    if var place = try Place.fetchOne(db, key: 1) {
        place.isFavorite = true
        try place.update(db)
    }
}

当然,您需要首先打开数据库连接,并创建数据库表

为了定义您的自定义记录,您可以继承现成的 Record 类,或者使用带有一系列集中功能的协议扩展您的结构和类:获取方法、持久性方法、记录比较...

使用记录协议扩展结构体更符合“Swift”风格。继承 Record 类更符合“经典”风格。您可以选择其中任何一种方法。参见一些记录定义示例记录方法列表以获取概述。

☝️ 注意:如果您熟悉 Core Data 的 NSManagedObject 或 Realm 的 Object,您可能会感到文化冲击:GRDB 记录没有唯一性、不会自动更新,也不会懒加载。这是协议导向编程的目的和结果。您应该阅读如何使用 SQLite 和 GRDB.swift 构建 iOS 应用程序以获得一般介绍。

💡 提示:在阅读本章后,请检查设计记录类型的良好实践指南

💡 提示:查看示例应用程序,其中使用记录。

概述

协议和记录类

记录概览

插入记录

要在数据库中插入记录,请调用 insert 方法

let player = Player(name: "Arthur", email: "[email protected]")
try player.insert(db)

👉 insert 方法适用于 Record 类的子类,以及采用 PersistableRecord 协议的类型。

获取记录

要从数据库中获取记录,请调用 获取方法

let arthur = try Player.fetchOne(db,            // Player?
    "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, key: "ES") // Country?

👉对于采用 Record 类和 FetchableRecord 协议的类型,可以使用原始 SQL 进行获取。

👉对于采用 Record 类、FetchableRecordTableRecord 协议的类型,可以使用 查询接口 而不使用 SQL 进行获取。

更新记录

要更新数据库中的记录,请调用 update 方法

if let player = try Player.fetchOne(db, key: 1) 
    player.score = 1000
    try player.update(db)
}

可以避免 无用的更新

if let player = try Player.fetchOne(db, key: 1) {
    // does not hit the database if score has not changed
    try player.updateChanges(db) {
        $0.score = 1000
    }
}

对于批量更新,可以执行一个 SQL 查询

try db.execute("UPDATE player SET synchronized = 1")

👉更新方法适用于 Record 类的子类,以及采用 PersistableRecord 协议的类型。

删除记录

要删除数据库中的记录,请调用 delete 方法

if let player = try Player.fetchOne(db, key: 1) {
    try player.delete(db)
}

也可以通过主键或任何唯一索引进行删除

try Player.deleteOne(db, key: 1)
try Player.deleteOne(db, key: ["email": "[email protected]"])
try Country.deleteAll(db, keys: ["FR", "US"])

对于批量删除,可以执行一个 SQL 查询,或查看 查询接口

try Player
    .filter(Column("email") == nil)
    .deleteAll(db)

👉删除方法适用于 Record 类的子类,以及采用 PersistableRecord 协议的类型。

记录计数

要计数记录,调用fetchCount方法

let playerCount: Int = try Player.fetchCount(db)

let playerWithEmailCount: Int = try Player
    .filter(Column("email") == nil)
    .fetchCount(db)

👉 fetchCount方法适用于Record类的子类以及采用TableRecord协议的类型。

以下是详细信息

记录协议概述

GRDB自带三种记录协议。根据您想要扩大类型的能力,您的自己的类型将采用其中一种或几种。

  • FetchableRecord能够解码数据库行

    总是可以在没有此协议的情况下解码行

    struct Place { ... }
    try dbQueue.read { db in
        let rows = try Row.fetchAll(db, "SELECT * FROM place")
        let places: [Place] = rows.map { row in
            return Place(
                id: row["id"],
                title: row["title"],
                coordinate: CLLocationCoordinate2D(
                    latitude: row["latitude"],
                    longitude: row["longitude"]))
            )
        }
    }

    但是FetchableRecord允许您编写更易于阅读、更高效的代码,无论是在性能还是内存使用方面

    struct Place: FetchableRecord { ... }
    try dbQueue.read { db in
        let places = try Place.fetchAll(db, "SELECT * FROM place")
    }

    💡 提示:FetchableRecord可以从标准的Decodable协议中获取其实施。有关更多信息,请参阅Codable Records

    FetchableRecord可以解码数据库行,但无法为您生成SQL请求。为此,您还需要TableRecord

  • TableRecord可以生成SQL查询

    struct Place: TableRecord { ... }
    // SELECT * FROM place ORDER BY title
    let request = Place.order(Column("title"))

    当一个类型同时采用TableRecord和FetchableRecord时,它可以加载这些请求

    struct Place: TableRecord, FetchableRecord { ... }
    try dbQueue.read { db in
        let places = try Place.order(Column("title")).fetchAll(db)
        let paris = try Place.fetchOne(key: 1)
    }
  • PersistableRecord能够写入:它可以在数据库中创建、更新和删除行

    struct Place : PersistableRecord { ... }
    try dbQueue.write { db in
        try Place.delete(db, key: 1)
        try Place(...).insert(db)
    }

    持久化记录还可以比较自身与其它记录,并避免不必要的数据库更新。

    💡 提示:PersistableRecord可以从标准的Encodable协议中获取其实施。有关更多信息,请参阅Codable Records

FetchableRecord 协议

FetchableRecord协议为任何可以从数据库行构建的类型授予获取方法

protocol FetchableRecord {
    /// Row initializer
    init(row: Row)
}

要使用FetchableRecord,从Record子类继承或显式采用它。例如

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 Records

// That's all
struct Player: Decodable, FetchableRecord {
    var id: Int64
    var name: String
    var score: Int
}

FetchableRecord允许类型从SQL查询中获取

try Place.fetchCursor(db, "SELECT ...", arguments:...) // A Cursor of Place
try Place.fetchAll(db, "SELECT ...", arguments:...)    // [Place]
try Place.fetchOne(db, "SELECT ...", arguments:...)    // Place?

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

☝️ 注意:出于性能考虑,在fetch查询的迭代过程中,会重复使用同一个init(row:)行参数。如果你想保留这行以供后续使用,请确保保存一个副本:self.row = row.copy()

☝️ 注意FetchableRecord.init(row:)初始化器适合大多数应用程序的需求。但是,有些应用程序的要求比其他应用程序更高。当FetchableRecord不能提供所需的精确支持时,请参阅Beyond FetchableRecord章节。

TableRecord 协议

TableRecord 协议为您生成SQL。要使用TableRecord,可以继承Record类,或显式采用它。

protocol TableRecord {
    static var databaseTableName: String { get }
    static var databaseSelection: [SQLSelectable] { get }
}

databaseSelection类型属性是可选的,并在由请求选择的列章节中进行了说明。

databaseTableName类型属性是数据库表的名字。默认情况下,它来源于类型名。

struct Place: TableRecord { }
print(Place.databaseTableName) // prints "place"

例如

  • 位置:place
  • 国家:country
  • 邮政地址:postalAddress
  • HTTP请求:httpRequest
  • 托福:toefl

您仍然可以提供一个自定义的表名。

struct Place: TableRecord {
    static let databaseTableName = "location"
}
print(Place.databaseTableName) // prints "location"

Record类的子类必须始终覆盖其父类的databaseTableName属性。

class Place: Record {
    override class var databaseTableName: String {
        return "place"
    }
}
print(Place.databaseTableName) // prints "place"

当一个类型同时采用TableRecord和FetchableRecord时,可以使用查询接口进行检索。

// SELECT * FROM place WHERE name = 'Paris'
let paris = try Place.filter(nameColumn == "Paris").fetchOne(db)

TableRecord还可以通过主键进行记录检索。

try Player.fetchOne(db, key: 1)              // Player?
try Player.fetchAll(db, keys: [1, 2, 3])     // [Player]

try Country.fetchOne(db, key: "FR")          // Country?
try Country.fetchAll(db, keys: ["FR", "US"]) // [Country]

当表中没有明确定义的主键时,GRDB使用隐藏的“rowid”列。

// SELECT * FROM document WHERE rowid = 1
try Document.fetchOne(db, key: 1)            // Document?

对于定义在唯一索引中的多列主键和唯一键,请提供字典。

// SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"]) // Citizenship?

PersistableRecord 协议

GRDB提供了两种协议,允许采用类型在数据库中创建、更新和删除行。

protocol MutablePersistableRecord : TableRecord {
    /// Defines the values persisted in the database
    func encode(to container: inout PersistenceContainer)
    
    /// 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(with rowID: Int64, for column: String?)
}
protocol PersistableRecord : MutablePersistableRecord {
    /// Non-mutating version of the optional didInsert(with:for:)
    func didInsert(with rowID: Int64, for column: String?)
}

是的,两种协议而不是一种。它们都提供了完全相同的好处。以下是选择一个或另一个的方式:

  • 如果您的类型是类,请选择PersistableRecord。除此之外,如果数据库表有一个自增主键,请实现didInsert(with:for:)方法。

  • 如果您的类型是结构体,并且数据库表有一个自增主键,请选择MutablePersistableRecord,并实现didInsert(with:for:)

  • 否则,选择PersistableRecord,并忽略didInsert(with:for:)

encode(to:)方法定义了哪些(Bool、Int、String、Date、Swift枚举等)将被分配给数据库列。

didInsert的选项方法允许采用类型在成功插入后存储其行ID,并且仅对具有自增主键的表有用。它在保护队列中调用,并与所有数据库更新序列化。

要使用持久化协议,可以继承Record类,或者显式采用其中一个。例如

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 id upon successful insertion:
    mutating func didInsert(with rowID: Int64, for column: String?) {
        id = 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
    }
}

当您的记录类型采用标准可编码协议时,您无需提供 encode(to:) 的实现。有关更多信息,请参阅 可编码记录

// That's all
struct Player: Encodable, MutablePersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    
    mutating func didInsert(with rowID: Int64, for column: String?) {
        id = rowID
    }
}

持久化方法

采用 持久化记录协议记录 子类和类型将为插入、更新和删除方法提供默认实现

// Instance methods
try place.save(db)                     // INSERT or UPDATE
try place.insert(db)                   // INSERT
try place.update(db)                   // UPDATE
try place.update(db, columns: ...)     // UPDATE
try place.updateChanges(db, from: ...) // Maybe UPDATE
try place.updateChanges(db) { ... }    // Maybe UPDATE
try place.updateChanges(db)            // Maybe UPDATE (Record class only)
try place.delete(db)                   // DELETE
try place.exists(db)

// Type methods
try Place.deleteAll(db)                    // DELETE
try Place.deleteAll(db, keys:...)          // DELETE
try Place.deleteOne(db, key:...)           // DELETE
  • insertupdatesavedelete 可抛出 数据库错误

  • 如果更新失败,因为没有匹配的数据库行,则 updateupdateChanges 还可以抛出 持久化错误

    在保存可能或可能不存在于数据库中的对象时,请首选 save 方法

  • save 确保您的值已存储到数据库中。

    如果记录有一个非空的主键,则它将执行 UPDATE,然后如果一个行没有被修改,将执行 INSERT。如果没有主键或主键为空,则直接执行 INSERT。

    尽管它可能执行两个 SQL 语句,但 save 作为一个原子操作来处理:GRDB 不会允许任何并发线程偷偷进入(请参阅 并发)。

  • delete 返回是否已删除数据库行。

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

自定义持久化方法

当调用持久化方法时,您的自定义类型可能想要执行额外的工作。

例如,它可能希望在其 UUID 插入之前自动设置。或者它可能在保存之前验证其值。

当您从 记录 继承时,只需重写自定义方法并调用 super

class Player : Record {
    var uuid: UUID?
    
    override func insert(_ db: Database) throws {
        if uuid == nil {
            uuid = UUID()
        }
        try super.insert(db)
    }
}

如果您使用原始的 持久化记录协议,请使用以下特殊方法之一:performInsertperformUpdateperformSaveperformDeleteperformExists

struct Link : PersistableRecord {
    var url: URL
    
    func insert(_ db: Database) throws {
        try validate()
        try performInsert(db)
    }
    
    func update(_ db: Database, columns: Set<String>) throws {
        try validate()
        try performUpdate(db, columns: columns)
    }
    
    func validate() throws {
        if url.host == nil {
            throw ValidationError("url must be absolute.")
        }
    }
}

☝️ 注意:特殊的 performInsertperformUpdate 等方法是保留供您自定义实现的。不要在其他地方使用它们。不要为这些方法提供另一个实现。

☝️ 注意:建议您不实现自己的 save 方法版本。它的默认实现将任务转发到 updateinsert:这些是需要自定义的方法,不是 save

编码记录

采用存档协议(可编码、可编码或可解码)的记录类型,只需声明符合所需的记录协议(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)
}

可编码记录根据其自己的可编码和可解码协议的实现来编码和解码其属性。然而,数据库有其特定的要求

  • 当属性有指定的数据库表示时,属性总是根据其首选的数据库表示进行编码(所有采用 DatabaseValueConvertible 协议的值)。
  • 您可以自定义日期和 uuid 的编码和解码。
  • 复杂属性(数组、字典、嵌套结构体等)存储为 JSON。

有关可编码记录的更多信息,请参阅

💡 提示:请参阅演示应用程序,了解使用可编码记录的示例。

JSON 列

可编码记录 包含一个不是简单 (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。默认情况下,使用 .base64 策略处理 Data 值,使用 .millisecondsSince1970 策略处理 Date,对于未遵守的浮点数使用 .throw 策略。

您可以通过实现以下方法来自定义 JSON 格式

protocol FetchableRecord {
    static func databaseJSONDecoder(for column: String) -> JSONDecoder
}

protocol MutablePersistableRecord {
    static func databaseJSONEncoder(for column: String) -> JSONEncoder
}

💡 提示:请确保设置 JSONEncoder 的 `sortedKeys` 选项,该选项从 iOS 11.0+、macOS 10.13+ 和 watchOS 4.0+ 中提供。此选项确保 JSON 输出是稳定的。这种稳定性对于 记录比较 能够按预期工作,对于数据库观察工具(如 ValueObservation)能够准确识别变更的记录至关重要。

日期和 UUID 编码策略

默认情况下,可编码记录 依据《日期和日期组件》和《UUID》部分所述,对日期和 UUID 属性进行编码和解码。

总结如下:日期以“YYYY-MM-DD HH:MM:SS.SSS”格式、UTC 时区编码,并解码各种日期格式和时间戳。UUID 以 16 字节数据块编码,并解码 16 字节数据块和诸如“E621E1F8-C36C-495A-93FC-0C247A3E6E5F”的字符串。

这些行为可以被覆盖

protocol FetchableRecord {
    static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get }
}

protocol MutablePersistableRecord {
    static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
    static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
}

有关所有可用策略的信息,请参阅 DatabaseDateDecodingStrategyDatabaseDateEncodingStrategyDatabaseUUIDEncodingStrategy

☝️ 注意:没有对UUID解码进行自定义,因为UUID已经能解码其所有编码变体(16字节块和UUID字符串)。

userInfo 字典

您的 可编解码记录 可以存储在数据库中,但它们可能有其他用途。在这种情况下,您可能需要根据上下文自定义它们对 Decodable.init(from:)Encodable.encode(to:) 的实现。

提供此类上下文的标准方式是 userInfo 字典。实现这些属性

protocol FetchableRecord {
    static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get }
}

protocol MutablePersistableRecord {
    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 let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "database row"]
}

// prints "Decoded from database row"
let player = try Player.fetchOne(db, ...)

☝️ 注意:确保将 databaseDecodingUserInfodatabaseEncodingUserInfo 属性明确声明为 [CodingUserInfoKey: Any]。如果没有这样做,Swift 编译器可能默默地忽略协议要求,导致出现空白的 userInfo。

提示:使用编码键作为列

如果您声明了一个显式的 CodingKeys 枚举(这是什么?),您可以指示 GRDB 使用这些编码键作为数据库列

struct Player: Codable, FetchableRecord, PersistableRecord {
    var name: String
    var score: Int
    
    // Add ColumnExpression conformance
    private enum CodingKeys: String, CodingKey, ColumnExpression {
        case name, score
    }
}

这允许您的记录使用 查询接口 构建请求

extension Player {
    static func filter(name: String) -> QueryInterfaceRequest<Player> {
        return filter(CodingKeys.name == name)
    }
    
    static var maximumScore: QueryInterfaceRequest<Int> {
        return select(max(CodingKeys.score), as: Int.self)
    }
}

这些请求既可以进行查找...

// Fetch values
try dbQueue.read { db in
    // SELECT * FROM player WHERE name = 'Arthur'
    let arthur = try Player.filter(name: "Arthur").fetchOne(db) // Player?
    
    // SELECT MAX(score) FROM player
    let maxScore = try Player.maximumScore.fetchOne(db)         // Int?
}

... 也可以放入数据库观察工具,如 ValueObservation

// Observe changes
try ValueObservation
    .trackingOne(Player.maximumScore)
    .start(in: dbQueue) { maxScore: Int? in
        print("The maximum score has changed")
    }

记录类

Record 是一个设计成可以继承的类。它从 FetchableRecord, TableRecord, and PersistableRecord 协议继承其功能。除此之外,Record 实例可以通过比较自身的前一个版本来避免不必要的更新。

Record 子类通过覆盖数据库方法自定义其数据库关系。例如

class Place: Record {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
    
    init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) {
        self.id = id
        self.title = title
        self.isFavorite = isFavorite
        self.coordinate = coordinate
        super.init()
    }
    
    /// The table name
    override class var databaseTableName: String {
        return "place"
    }
    
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, favorite, latitude, longitude
    }
    
    /// Creates a record from a database row
    required init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.favorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
        super.init(row: row)
    }
    
    /// The values persisted in the database
    override func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.favorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    /// Update record ID after a successful insertion
    override func didInsert(with rowID: Int64, for column: String?) {
        id = rowID
    }
}

记录比较

采用 PersistableRecord 协议的记录可以与其他记录或自己的前一个版本进行比较。

这有助于在记录未被编辑的情况下避免昂贵的 UPDATE 语句。

《updateChanges` 方法

《updateChanges` 方法仅对更改的列执行数据库更新(如果记录没有更改则不执行任何操作)。

  • updateChanges(_:from:)

    此方法使您能够比较两个记录

    if let oldPlayer = try Player.fetchOne(db, key: 42) {
        var newPlayer = oldPlayer
        newPlayer.score = 100
        if try newPlayer.updateChanges(db, from: oldPlayer) {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }
  • updateChanges(_:with:)

    此方法允许您就地更新记录。

    if var player = try Player.fetchOne(db, key: 42) {
        let modified = player.updateChanges(db) {
            $0.score = 100
        }
        if modified {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }
  • 《updateChanges(_:`(仅 Record 类)

    Record` 类的实例能够与自己比较,并知道自上次检索或保存以来是否是否有未被保存的更改。

    // Record class only
    if let player = try Player.fetchOne(db, key: 42) {
        player.score = 100
        if try player.updateChanges(db) {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }

《databaseEquals` 方法

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

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

☝️ **注意**: 比较是在记录的数据库表示上进行的。只要您的记录类型采用可持久化记录协议,您就无需关心 Equatable。

《databaseChanges` 和 `hasDatabaseChanges` 方法

《databaseChanges(from:)` 返回两个记录之间差异的字典。

let oldPlayer = Player(id: 1, name: "Arthur", score: 100)
let newPlayer = Player(id: 1, name: "Arthur", score: 1000)
for (column, oldValue) in newPlayer.databaseChanges(from: oldPlayer) {
    print("\(column) was \(oldValue)")
}
// prints "score was 100"

Record 类能够与自己比较。

// Record class only
let player = Player(id: 1, name: "Arthur", score: 100)
try player.insert(db)
player.score = 1000
for (column, oldValue) in player.databaseChanges {
    print("\(column) was \(oldValue)")
}
// prints "score was 100"

Record 实例还有一个 hasDatabaseChanges` 属性

// Record class only
player.score = 1000
if player.hasDatabaseChanges {
    try player.save(db)
}

Record.hasDatabaseChanges` 在 Record 实例从一个数据库或保存到数据库中之后为 false。随后的修改可能会设置它,也可能不会:`hasDatabaseChanges` 是根据值比较的。**设置属性为相同的值不会设置更改标记**

let player = Player(name: "Barbara", score: 750)
player.hasDatabaseChanges // true

try player.insert(db)
player.hasDatabaseChanges // false

player.name = "Barbara"
player.hasDatabaseChanges // false

player.score = 1000
player.hasDatabaseChanges // true
player.databaseChanges    // ["score": 750]

有关一个高效算法,该算法可以同步数据库表的内容与 JSON 负载,请参阅 JSONSynchronization.playground

Record 自定义选项

GRDB 的记录具有许多默认行为,这些行为旨在适应大多数情况。许多这些默认值都可以根据您的特定需求进行自定义。

Codable记录有几个额外选项

冲突解决

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

这些冲突通常以错误结束。然而,SQLite 允许你更改默认行为,并使用特定策略处理冲突。例如,INSERT OR REPLACE 语句使用“替换”策略,代替错误替换冲突行。

有五种不同的策略:中止(默认)、替换、回滚、失败和忽略。

SQLite 允许你在两个不同的地方指定冲突策略

  • 在数据库表定义中

    // CREATE TABLE player (
    //     id INTEGER PRIMARY KEY AUTOINCREMENT,
    //     email TEXT UNIQUE ON CONFLICT REPLACE
    // )
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("email", .text).unique(onConflict: .replace) // <--
    }
    
    // Despite the unique index on email, both inserts succeed.
    // The second insert replaces the first row:
    try db.execute("INSERT INTO player (email) VALUES (?)", arguments: ["[email protected]"])
    try db.execute("INSERT INTO player (email) VALUES (?)", arguments: ["[email protected]"])
  • 在每次修改查询中

    // CREATE TABLE player (
    //     id INTEGER PRIMARY KEY AUTOINCREMENT,
    //     email TEXT UNIQUE
    // )
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("email", .text).unique()
    }
    
    // Again, despite the unique index on email, both inserts succeed.
    try db.execute("INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["[email protected]"])
    try db.execute("INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["[email protected]"])

当你想在查询级别处理冲突时,指定采用 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)

☝️ 注意:忽略策略与不会影响通知插入记录的rowID的 didInsert 方法不兼容。选择你的毒药

  • 如果你在数据库表定义中指定了 ignore 策略,不要实现 didInsert 方法:在插入失败的情况下,它将被一些随机的 ID 调用。
  • 如果你在查询级别指定了 ignore 策略,则永远不会调用 didInsert 方法。

☝️ 注意replace 策略可能需要删除行,以便插入和更新可以成功。这些删除不会被报告给 事务观察者(这可能在 SQLite 的未来版本中改变)。

隐式RowID主键

所有SQLite表都有一个主键。即使在主键不是显式的情况下也是。

// No explicit primary key
try db.create(table: "event") { t in
    t.column("message", .text)
    t.column("date", .datetime)
}

// No way to define an explicit primary key
try db.create(virtualTable: "book", using: FTS4()) { t in
    t.column("title")
    t.column("author")
    t.column("body")
}

隐式主键存储在隐藏列 rowid 中。隐藏意味着 SELECT * 不会选取它,但它仍然可以被选择和查询:SELECT *, rowid ... WHERE rowid = 1

一些 GRDB 方法将在表没有显式主键时自动使用此隐藏列

// SELECT * FROM event WHERE rowid = 1
let event = try Event.fetchOne(db, key: 1)

// DELETE FROM book WHERE rowid = 1
try Book.deleteOne(db, key: 1)

暴露 RowID 列

默认情况下,包装不含任何显式主键的表的记录类型不了解隐藏的 rowid 列。

没有主键,记录没有任何标识,持久化方法可能表现出不被期望的行为:update() 抛出错误,save() 总是执行插入,可能会违反约束,exists() 总是 false。

当 SQLite 不允许您提供显式主键时(例如在 全文 表中),您可能希望使您的记录类型完全了解隐藏的 rowid 列

  1. databaseSelection 静态属性(来自 TableRecord 协议)返回隐藏的 rowid 列

    struct Event : TableRecord {
        static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID]
    }
    
    // When you subclass Record, you need an override:
    class Book : Record {
        override class var databaseSelection: [SQLSelectable] {
            return [AllColums(), Column.rowID]
        }
    }

    GRDB 将默认选择 rowid

    // SELECT *, rowid FROM event
    let events = try Event.fetchAll(db)
  2. init(row:)(来自 FetchableRecord 协议)消耗 "rowid" 列

    struct Event : FetchableRecord {
        var id: Int64?
        
        init(row: Row) {
            id = row[Column.rowID]
        }
    }

    然后您的检索到的记录将知道它们的 IDs

    let event = try Event.fetchOne(db)!
    event.id // some value
  3. encode(to:) 中编码 rowid,并在 didInsert(with:for:) 方法中保留它(都来自 PersistableRecordMutablePersistableRecord 协议)

    struct Event : MutablePersistableRecord {
        var id: Int64?
        
        func encode(to container: inout PersistenceContainer) {
            container[Column.rowID] = id
            container["message"] = message
            container["date"] = date
        }
        
        mutating func didInsert(with rowID: Int64, for column: String?) {
            id = rowID
        }
    }

    您将能够跟踪记录 ID,更新它们或检查它们的存在

    let event = Event(message: "foo", date: Date())
    
    // Insertion sets the record id:
    try event.insert(db)
    event.id // some value
    
    // Record can be updated:
    event.message = "bar"
    try event.update(db)
    
    // Record knows if it exists:
    event.exists(db) // true

超越 FetchableRecord

一些 GRDB 用户最终发现,FetchableRecord 协议并不适用于所有情况。 FetchableRecord 无法很好地处理的用例包括

  • 您的应用程序需要多态行解码:它根据数据库行中包含的值解码某种类型或另一种类型。

  • 您的应用程序需要在使用上下文中解码行:每个解码的值都应该使用来自数据库之外的一些额外值进行初始化。

  • 您的应用程序需要一个支持不受信任数据库的记录类型,并且在解码数据库行时可能会失败(当行包含无效值时抛出错误)。

由于这些用例未由 FetchableRecord 处理得很好,因此请不要尝试在此协议之上实现它们:您只是在与框架作对。

相反,请参阅 CustomizedDecodingOfDatabaseRows 游乐场。您将运行一些示例代码,并了解当需要时如何逃离 FetchableRecord。并且记住,离开 FetchableRecord 将不会剥夺您查询接口请求和一般所有 TableRecordPersistableRecord 协议的 SQL 生成功能。

记录定义示例

以下我们将展示如何为以下数据库表声明记录类型

try dbQueue.write { db in
    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()
    }
}

下面三个示例都是正确的。您将根据个人喜好和应用程序的要求选择其中一个

定义一个可编码的结构体,并采用所需的记录协议

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

请参阅记录协议概述可编码记录以获取更多信息。

struct Place: Codable {
    var id: Int64?
    var title: String
    var favorite: Bool
    var latitude: CLLocationDegrees
    var longitude: CLLocationDegrees
    
    var coordinate: CLLocationCoordinate2D {
        get {
            return CLLocationCoordinate2D(
                latitude: latitude,
                longitude: longitude)
        }
        set {
            latitude = newValue.latitude
            longitude = newValue.longitude
        }
    }
}

// SQL generation
extension Place: TableRecord { }

// Fetching methods
extension Place: FetchableRecord { }

// Persistence methods
extension Place: MutablePersistableRecord {
    /// Update record ID after a successful insertion
    mutating func didInsert(with rowID: Int64, for column: String?) {
        id = 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, favorite, 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.favorite]
        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.favorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    /// Update record ID after a successful insertion
    mutating func didInsert(with rowID: Int64, for column: String?) {
        id = rowID
    }
}
继承自Record

请参阅Record类以获取更多信息。

class Place: Record {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
    
    init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) {
        self.id = id
        self.title = title
        self.isFavorite = isFavorite
        self.coordinate = coordinate
        super.init()
    }
    
    /// The table name
    override class var databaseTableName: String {
        return "place"
    }
    
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, favorite, latitude, longitude
    }
    
    /// Creates a record from a database row
    required init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.favorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
        super.init(row: row)
    }
    
    /// The values persisted in the database
    override func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.favorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    /// Update record ID after a successful insertion
    override func didInsert(with rowID: Int64, for column: String?) {
        id = rowID
    }
}

记录方法列表

这是记录方法的列表,包括它们所需的协议。Record类采用了所有这些协议,并增加了一些额外的功能。

方法 协议 注意事项
核心方法
init(row:) FetchableRecord
Type.databaseTableName TableRecord
Type.databaseSelection TableRecord *
Type.persistenceConflictPolicy PersistableRecord *
record.encode(to:) PersistableRecord
record.didInsert(with:for:) PersistableRecord
插入和更新记录
record.insert(db) PersistableRecord
record.save(db) PersistableRecord
record.update(db) PersistableRecord
record.update(db, columns:...) PersistableRecord
record.updateChanges(db, from:...) PersistableRecord *
record.updateChanges(db) { ... } PersistableRecord *
record.updateChanges(db) Record *
删除记录
record.delete(db) PersistableRecord
Type.deleteOne(db, key:...) PersistableRecord ¹
Type.deleteAll(db) PersistableRecord
Type.deleteAll(db, keys:...) PersistableRecord ¹
Type.filter(...).deleteAll(db) PersistableRecord ²
检查记录是否存在
record.exists(db) PersistableRecord
将记录转换为字典
record.databaseDictionary PersistableRecord
统计记录数量
Type.fetchCount(db) TableRecord
Type.filter(...).fetchCount(db) TableRecord ²
获取记录游标
Type.fetchCursor(db) FetchableRecord & TableRecord
Type.fetchCursor(db, keys:...) FetchableRecord & TableRecord ¹
Type.fetchCursor(db, sql) FetchableRecord ³
Type.fetchCursor(statement) FetchableRecord
Type.filter(...).fetchCursor(db) FetchableRecord & TableRecord ²
获取记录数组
Type.fetchAll(db) FetchableRecord & TableRecord
Type.fetchAll(db, keys:...) FetchableRecord & TableRecord ¹
Type.fetchAll(db, sql) FetchableRecord ³
Type.fetchAll(statement) FetchableRecord
Type.filter(...).fetchAll(db) FetchableRecord & TableRecord ²
获取单个记录
Type.fetchOne(db) FetchableRecord & TableRecord
Type.fetchOne(db, key:...) FetchableRecord & TableRecord ¹
Type.fetchOne(db, sql) FetchableRecord ³
Type.fetchOne(statement) FetchableRecord
Type.filter(...).fetchOne(db) FetchableRecord & TableRecord ²
记录比较
record.databaseEquals(...) PersistableRecord
record.databaseChanges(from:...) PersistableRecord
record.updateChanges(db, from:...) PersistableRecord
record.updateChanges(db) { ... } PersistableRecord
record.hasDatabaseChanges Record
record.databaseChanges Record
record.updateChanges(db) Record

¹所有唯一的键支持:主键(单列、组合、隐式RowID)和唯一索引

try Player.fetchOne(db, key: 1)                               // Player?
try Player.fetchOne(db, key: ["email": "[email protected]"]) // Player?
try Country.fetchAll(db, keys: ["FR", "US"])                  // [Country]

²请参阅查询请求

let request = Player.filter(emailColumn != nil).order(nameColumn)
let players = try request.fetchAll(db)  // [Player]
let count = try request.fetchCount(db)  // Int

³ 请参阅 SQL 查询

let player = try Player.fetchOne("SELECT * FROM player WHERE id = ?", arguments: [1]) // Player?

请参阅 预处理语句

let statement = try db.makeSelectStatement("SELECT * FROM player WHERE id = ?")
let player = try Player.fetchOne(statement, arguments: [1])  // Player?

查询接口

查询接口允许您编写纯 Swift 代码而不是 SQL 代码

try dbQueue.write { db in
    // Update database schema
    try db.create(table: "wine") { t in ... }
    
    // Fetch records
    let wines = try Wine.filter(origin == "Burgundy").order(price).fetchAll(db)
    
    // Count
    let count = try Wine.filter(color == Color.red).fetchCount(db)
    
    // Delete
    try Wine.filter(corked == true).deleteAll(db)
}

在查询数据库之前,您需要打开一个 数据库连接

请注意,查询接口无法生成所有可能的 SQL 查询。您可能也 更愿意 编写 SQL 代码,这完全可以接受。从简短片段到完整的查询,您的 SQL 技能都受到欢迎

try dbQueue.write { db in
    // Update database schema (with SQL)
    try db.execute("CREATE TABLE wine (...)")
    
    // Fetch records (with SQL)
    let wines = try Wine.fetchAll(db,
        "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)
    
    // Delete (with SQL)
    try db.execute("DELETE FROM wine WHERE corked")
}

所以,不要错过 SQL API

数据库模式

一旦获得 数据库连接,您就可以设置数据库模式而不必编写 SQL

创建表

// CREATE TABLE place (
//   id INTEGER PRIMARY KEY AUTOINCREMENT,
//   title TEXT,
//   favorite BOOLEAN NOT NULL DEFAULT 0,
//   latitude DOUBLE NOT NULL,
//   longitude DOUBLE NOT NULL
// )
try db.create(table: "place") { t in
    t.autoIncrementedPrimaryKey("id")
    t.column("title", .text)
    t.column("favorite", .boolean).notNull().defaults(to: false)
    t.column("longitude", .double).notNull()
    t.column("latitude", .double).notNull()
}

create(table:) 方法涵盖了几乎所有 SQLite 表创建功能。对于虚拟表,请参阅 全文搜索,或使用原始 SQL。

SQLite 本身有关于表创建的许多参考文档:CREATE TABLESQLite 版本 3 中的数据类型SQLite 外键支持ON CONFLICT无 ROWID 优化

配置表创建:

// CREATE TABLE example ( ... )
try db.create(table: "example") { t in ... }
    
// CREATE TEMPORARY TABLE example IF NOT EXISTS (
try db.create(table: "example", temporary: true, ifNotExists: true) { t in

💡 提示:数据库表名应该是单数,并且使用驼峰命名法。使它们看起来像 Swift 标识符:placecountrypostalAddress,'httpRequest'。

这将在您需要时帮助您使用 关联。遵循其他命名约定数据库表名是完全可以接受的,但您可能需要执行额外的配置。

添加常规列,包括它们的名称和最终类型(文本、整数、双精度、数字、布尔值、二进制大对象、日期和日期时间) - 请参阅 SQLite 数据类型

// CREATE TABLE example (
//   a,
//   name TEXT,
//   creationDate DATETIME,
try db.create(table: "example") { t in
    t.column("a")
    t.column("name", .text)
    t.column("creationDate", .datetime)

定义 非空 列,并设置 默认值

    // email TEXT NOT NULL,
    t.column("email", .text).notNull()
    
    // name TEXT NOT NULL DEFAULT 'Anonymous',
    t.column("name", .text).notNull().defaults(to: "Anonymous")

使用单个列作为 主键唯一键外键。在定义外键时,引用列是引用表的(主)键(除非您指定了其他内容)

    // id INTEGER PRIMARY KEY AUTOINCREMENT,
    t.autoIncrementedPrimaryKey("id")
    
    // uuid TEXT PRIMARY KEY,
    t.column("uuid", .text).primaryKey()
    
    // email TEXT UNIQUE,
    t.column("email", .text).unique()
    
    // countryCode TEXT REFERENCES country(code) ON DELETE CASCADE,
    t.column("countryCode", .text).references("country", onDelete: .cascade)

💡 提示:当您需要一个自动生成唯一值的整数主键时,强烈建议您使用autoIncrementedPrimaryKey方法

try db.create(table: "example") { t in
    t.autoIncrementedPrimaryKey("id")
    ...
}

推荐这个方法的原因是,自动增长的 主键 可以防止id的重用。这可以防止您的应用程序或数据库观察工具认为某行已更新,其实它是被删除后替换的。根据您的应用需求,这可能是可以接受的。但通常情况下并不是。

在列上创建索引

    t.column("score", .integer).indexed()

有关额外的索引选项,请参阅下文创建索引

对单个列执行完整性检查,SQLite只会让符合规范的行通过。在上面的示例中,$0闭包变量是一个列,允许您构建任何SQL表达式

    // name TEXT CHECK (LENGTH(name) > 0)
    // score INTEGER CHECK (score > 0)
    t.column("name", .text).check { length($0) > 0 }
    t.column("score", .integer).check(sql: "score > 0")

其他表约束可能涉及多个列

    // PRIMARY KEY (a, b),
    t.primaryKey(["a", "b"])
    
    // UNIQUE (a, b) ON CONFLICT REPLACE,
    t.uniqueKey(["a", "b"], onConfict: .replace)
    
    // FOREIGN KEY (a, b) REFERENCES parents(c, d),
    t.foreignKey(["a", "b"], references: "parents")
    
    // CHECK (a + b < 10),
    t.check(Column("a") + Column("b") < 10)
    
    // CHECK (a + b < 10)
    t.check(sql: "a + b < 10")
}

修改表

SQLite 允许您重命名表,并向现有表中添加列

// ALTER TABLE referer RENAME TO referrer
try db.rename(table: "referer", to: "referrer")

// ALTER TABLE player ADD COLUMN url TEXT
try db.alter(table: "player") { t in
    t.add(column: "url", .text)
}

☝️ 注意:SQLite 限制了可能的表更改,可能需要您重新创建依赖的触发器或视图。有关ALTER TABLE的详细信息,请参阅文档。有关解除限制的方法,请查阅高级数据库架构更改

删除表

使用drop(table:)方法删除表

try db.drop(table: "obsolete")

创建索引

使用create(index:)方法创建索引

// CREATE UNIQUE INDEX byEmail ON users(email)
try db.create(index: "byEmail", on: "users", columns: ["email"], unique: true)

相关SQLite文档

请求

查询接口请求使您可以从数据库中检索值

let request = Player.filter(emailColumn != nil).order(nameColumn)
let players = try request.fetchAll(db)  // [Player]
let count = try request.fetchCount(db)  // Int

所有请求都从采用TableRecord协议的类型开始,例如Record子类(见记录

class Player : Record { ... }

声明您要用于过滤或排序的表列

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
}

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

  • all()none():所有行或没有行的请求。

    // SELECT * FROM player
    Player.all()

    当需要时,还可以选择隐藏的rowid(请参阅此处说明)

  • select(...)select(..., as:)定义所选列。请参阅由请求选择的列

    // SELECT name FROM player
    Player.select(nameColumn, as: String.self)
  • annotated(with: ...)通过关联聚合扩展选集(请参阅此处说明)

    // SELECT team.*, COUNT(DISTINCT player.rowid) AS playerCount
    // FROM team
    // LEFT JOIN player ON player.teamId = team.id
    // GROUP BY team.id
    Team.annotated(with: Team.players.count)
  • distinct()执行唯一化。

    // SELECT DISTINCT name FROM player
    Player.select(nameColumn, as: String.self).distinct()
  • filter(expression)应用条件。

    // SELECT * FROM player WHERE id IN (1, 2, 3)
    Player.filter([1,2,3].contains(idColumn))
    
    // SELECT * FROM player WHERE (name IS NOT NULL) AND (height > 1.75)
    Player.filter(nameColumn != nil && heightColumn > 1.75)
  • filter(key:)filter(keys:)在主键和唯一键上应用条件。

    // SELECT * FROM player WHERE id = 1
    Player.filter(key: 1)
    
    // SELECT * FROM country WHERE isoCode IN ('FR', 'US')
    Country.filter(keys: ["FR", "US"])
    
    // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
    Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])
    
    // SELECT * FROM player WHERE email = '[email protected]'
    Player.filter(key: ["email": "[email protected]"])
  • matching(pattern)执行全文搜索(请参阅此处说明)

    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
    Document.matching(pattern)

    当模式为nil时,不会匹配任何行。

  • group(expression, ...)对行进行分组。

    // SELECT name, MAX(score) FROM player GROUP BY name
    Player
        .select(nameColumn, max(scoreColumn))
        .group(nameColumn)
  • having(expression)对分组行应用条件。

    // SELECT team, MAX(score) FROM player GROUP BY team HAVING MIN(score) >= 1000
    Player
        .select(teamColumn, max(scoreColumn))
        .group(teamColumn)
        .having(min(scoreColumn) >= 1000)
  • having(aggregate)根据关联聚合对分组行应用条件(请参阅此处说明)

    // SELECT team.*
    // FROM team
    // LEFT JOIN player ON player.teamId = team.id
    // GROUP BY team.id
    // HAVING COUNT(DISTINCT player.rowid) >= 5
    Team.having(Team.players.count >= 5)
  • order(ordering, ...)排序。

    // SELECT * FROM player ORDER BY name
    Player.order(nameColumn)
    
    // SELECT * FROM player ORDER BY score DESC, name
    Player.order(scoreColumn.desc, nameColumn)

    每次order调用都会清除之前的排序。

    // SELECT * FROM player ORDER BY name
    Player.order(scoreColumn).order(nameColumn)
  • orderByPrimaryKey()按主键排序

    // SELECT * FROM player ORDER BY id
    Player.orderByPrimaryKey()
    
    // SELECT * FROM country ORDER BY code
    Country.orderByPrimaryKey()
    
    // SELECT * FROM citizenship ORDER BY citizenId, countryCode
    Citizenship.orderByPrimaryKey()
  • reversed()反转最终排序。

    // SELECT * FROM player ORDER BY score ASC, name DESC
    Player.order(scoreColumn.desc, nameColumn).reversed()

    如果没有指定排序,则此方法没有效果。

    // SELECT * FROM player
    Player.all().reversed()
  • limit(limit, offset: offset)限制和分页结果。

    // SELECT * FROM player LIMIT 5
    Player.limit(5)
    
    // SELECT * FROM player LIMIT 5 OFFSET 10
    Player.limit(5, offset: 10)
  • joining(...)including(...)通过关联获取和连接记录。

    // SELECT player.*, team.*
    // FROM player
    // JOIN team ON team.id = player.teamId
    Player.including(required: Player.team)

通过级联这些方法来细化请求。

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

selectordergrouplimit方法忽略并替换之前应用的选取、排序、分组和限制。相反,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
let request = Player.all()

可以为每个单独的请求或为从给定类型构建的所有请求更改选择。

select(...)select(..., as:)方法更改单个请求的选择(请参阅从请求中获取以获取详细信息)

let request = Player.select(max(Column("score")))
let maxScore: Int? = try Int.fetchOne(db, request)

记录类型的默认选择由databaseSelection属性控制

struct RestrictedPlayer : TableRecord {
    static let databaseTableName = "player"
    static let databaseSelection: [SQLSelectable] = [Column("id"), Column("name")]
}

struct ExtendedPlayer : TableRecord {
    static let databaseTableName = "player"
    static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID]
}

// SELECT id, name FROM player
let request = RestrictedPlayer.all()

// SELECT *, rowid FROM player
let request = ExtendedPlayer.all()

☝️ 注意:请确保将databaseSelection属性显式声明为[SQLSelectable]。如果不是,Swift编译器可能会静默忽略协议要求,导致粘性的SELECT *请求。要验证您的配置,请参阅如何将请求打印为SQL常见问题解答。

表达式

使用从Swift代码构建的SQL表达式提供请求(请参阅此处说明)

SQL 运算符

  • =<><<=>>=ISIS NOT

    比较运算符基于 Swift 运算符 ==!====!==<<=>>=

    // SELECT * FROM player WHERE (name = 'Arthur')
    Player.filter(nameColumn == "Arthur")
    
    // SELECT * FROM player WHERE (name IS NULL)
    Player.filter(nameColumn == nil)
    
    // SELECT * FROM player WHERE (score IS 1000)
    Player.filter(scoreColumn === 1000)
    
    // SELECT * FROM rectangle WHERE width < height
    Rectangle.filter(widthColumn < heightColumn)

    ☝️ 注意:SQLite 字符串比较,默认是大小写敏感的,并且不支持 Unicode。如果需要更多控制,请参阅 字符串比较

  • *, /, +, -

    SQLite 算术运算符与他们的 Swift 等效符号相同

    // SELECT ((temperature * 1.8) + 32) AS farenheit FROM planet
    Planet.select((temperatureColumn * 1.8 + 32).aliased("farenheit"))

    ☝️ 注意:类似 nameColumn + "rrr" 的表达式将由 SQLite 作为一个数值加法(结果可能古怪)而不是字符串连接来解析。

  • ANDORNOT

    SQL 逻辑运算符基于 Swift 的 &&||!

    // SELECT * FROM player WHERE ((NOT verified) OR (score < 1000))
    Player.filter(!verifiedColumn || scoreColumn < 1000)

    当你想用 ANDOR 运算符连接一系列表达式时,请使用 joined(operator:)

    // SELECT * FROM player WHERE (verified AND (score >= 1000) AND (name IS NOT NULL))
    let conditions = [
        verifiedColumn,
        scoreColumn >= 1000,
        nameColumn != nil]
    Player.filter(conditions.joined(operator: .and))

    如果序列为空,则 joined(operator: .and) 返回 true,而 joined(operator: .or) 返回 false

    // SELECT * FROM player WHERE 1
    Player.filter([].joined(operator: .and))
    
    // SELECT * FROM player WHERE 0
    Player.filter([].joined(operator: .or))
  • BETWEENINNOT IN

    为了检查 Swift 序列(数组、集合、范围等)中是否包含某个值,请调用 contains 方法

    // SELECT * FROM player WHERE id IN (1, 2, 3)
    Player.filter([1, 2, 3].contains(idColumn))
    
    // SELECT * FROM player WHERE id NOT IN (1, 2, 3)
    Player.filter(![1, 2, 3].contains(idColumn))
    
    // SELECT * FROM player WHERE score BETWEEN 0 AND 1000
    Player.filter((0...1000).contains(scoreColumn))
    
    // SELECT * FROM player WHERE (score >= 0) AND (score < 1000)
    Player.filter((0..<1000).contains(scoreColumn))
    
    // SELECT * FROM player WHERE initial BETWEEN 'A' AND 'N'
    Player.filter(("A"..."N").contains(initialColumn))
    
    // SELECT * FROM player WHERE (initial >= 'A') AND (initial < 'N')
    Player.filter(("A"..<"N").contains(initialColumn))

    ☝️ 注意:SQLite 字符串比较,默认是大小写敏感的,并且不支持 Unicode。如果需要更多控制,请参阅 字符串比较

  • LIKE

    SQLite LIKE 操作符作为 like 方法可用

    // SELECT * FROM player WHERE (email LIKE '%@example.com')
    Player.filter(emailColumn.like("%@example.com"))

    ☝️ 注意:SQLite LIKE 操作符不区分大小写,但也不支持 Unicode。例如,表达式 'a' LIKE 'A' 为真,但 'æ' LIKE 'Æ' 为假。

  • MATCH

    全文搜索 MATCH 操作符通过 FTS3Pattern(适用于 FTS3 和 FTS4 表)和 FTS5Pattern(适用于 FTS5)提供

    FTS3 和 FTS4

    let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
    
    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    Document.matching(pattern)
    
    // SELECT * FROM document WHERE content MATCH 'sqlite database'
    Document.filter(contentColumn.match(pattern))

    FTS5

    let pattern = FTS5Pattern(matchingAllTokensIn: "SQLite database")
    
    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    Document.matching(pattern)

SQL 函数

  • ABSAVGCOUNTLENGTHMAXMINSUM

    它们基于 absaveragecountlengthmaxminsum Swift 函数

    // SELECT MIN(score), MAX(score) FROM player
    Player.select(min(scoreColumn), max(scoreColumn))
    
    // SELECT COUNT(name) FROM player
    Player.select(count(nameColumn))
    
    // SELECT COUNT(DISTINCT name) FROM player
    Player.select(count(distinct: nameColumn))
  • IFNULL

    使用 Swift 的 ?? 运算符

    // SELECT IFNULL(name, 'Anonymous') FROM player
    Player.select(nameColumn ?? "Anonymous")
    
    // SELECT IFNULL(name, email) FROM player
    Player.select(nameColumn ?? emailColumn)
  • LOWERUPPER

    查询界面不提供对这些 SQLite 函数的访问。虽然对它们没有意见,但它们不识别 Unicode。

    相反,GRDB 将 SQLite 扩展到调用 Swift 内置字符串函数(capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercased)的 SQL 函数上

    Player.select(nameColumn.uppercased())

    ☝️ 注意:在 比较 字符串时,你最好使用 排序规则

    let name: String = ...
    
    // Not recommended
    nameColumn.uppercased() == name.uppercased()
    
    // Better
    nameColumn.collating(.caseInsensitiveCompare) == name
  • 自定义 SQL 函数和聚合

    你可以应用你自己的 自定义 SQL 函数和聚合

    let f = DatabaseFunction("f", ...)
    
    // SELECT f(name) FROM player
    Player.select(f.apply(nameColumn))

从请求中获取

一旦你有了请求,就可以从请求的源头获取记录

// 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.fetchOne(db)    // Player?

例如

let allPlayers = try Player.fetchAll(db)                            // [Player]
let arthur = try Player.filter(nameColumn == "Arthur").fetchOne(db) // Player?

查看获取方法,了解fetchCursorfetchAllfetchOne方法的信息。

有时你想要获取其他值.

最简单的方法是将请求作为参数传递给所需的类型获取方法

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

当你还想使用数据库观测工具,如值观测时,你需要更进一步,更改请求的类型

  • 当你更改选择时,首选select(..., as:)方法

    // A request of Int
    let request = Player.select(max(scoreColumn), as: Int.self)
    
    // Simple fetch
    let maxScore = try dbQueue.read { db in
        try request.fetchOne(db) // Int?
    }
    
    // Observe with ValueObservation
    try ValueObservation
        .trackingOne(request)
        .start(in: dbQueue) { maxScore: Int? in
            print("The maximum score has changed")
        }
  • 否则,使用asRequest(of:)。下面是一个使用关联的示例

    struct BookInfo: FetchableRecord, Decodable {
        var book: Book
        var author: Author
    }
    
    // A request of BookInfo
    let request = Book
        .including(required: Book.author)
        .asRequest(of: BookInfo.self)
    
    // Simple fetch
    let bookInfos = try dbQueue.read { db in
        try request.fetchAll(db) // [BookInfo]
    }
    
    // Observe with ValueObservation
    try ValueObservation
        .trackingAll(request)
        .start(in: dbQueue) { bookInfos: [BookInfo] in
            print("Books have changed")
        }

根据键进行获取

根据主键获取记录是一个非常常见的任务。它有一个快捷方式,可以接受任何单列主键

// SELECT * FROM player WHERE id = 1
try Player.fetchOne(db, key: 1)              // Player?

// SELECT * FROM player WHERE id IN (1, 2, 3)
try Player.fetchAll(db, keys: [1, 2, 3])     // [Player]

// SELECT * FROM country WHERE isoCode = 'FR'
try Country.fetchOne(db, key: "FR")          // Country?

// SELECT * FROM country WHERE isoCode IN ('FR', 'US')
try Country.fetchAll(db, keys: ["FR", "US"]) // [Country]

当表中没有明确定义的主键时,GRDB使用隐藏的“rowid”列。

// SELECT * FROM document WHERE rowid = 1
try Document.fetchOne(db, key: 1)            // Document?

对于定义在唯一索引中的多列主键和唯一键,请提供字典。

// SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"]) // Citizenship?

// SELECT * FROM player WHERE email = '[email protected]'
try Player.fetchOne(db, key: ["email": "[email protected]"])              // Player?

当你想构建请求并计划稍后从它获取数据时,请使用filter(key:)filter(keys:)方法

// SELECT * FROM player WHERE id = 1
let request = Player.filter(key: 1)
let player = try request.fetchOne(db)    // Player?

// SELECT * FROM player WHERE id IN (1, 2, 3)
let request = Player.filter(keys: [1, 2, 3])
let players = try request.fetchAll(db)   // [Player]

// SELECT * FROM country WHERE isoCode = 'FR'
let request = Country.filter(key: "FR")
let country = try request.fetchOne(db)   // Country?

// SELECT * FROM country WHERE isoCode IN ('FR', 'US')
let request = Country.filter(keys: ["FR", "US"])
let countries = try request.fetchAll(db) // [Country]

// SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
let request = Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])
let citizenship = request.fetchOne(db)   // Citizenship?

// SELECT * FROM player WHERE email = '[email protected]'
let request = Player.filter(key: ["email": "[email protected]"])
let player = try request.fetchOne(db)    // Player?

这些请求可以为值观测提供数据

try ValueObservation.
    .trackingOne(Player.filter(key: 1))
    .start(in: dbQueue) { player: Player? in
        print("Player 1 has changed")
    }

获取聚合值

请求可以计数。 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 WHERE email IS NULL
let request = Player.filter(emailColumn == nil)
try request.deleteAll(db)

☝️ 注意 删除方法仅适用于采用持久化记录协议的记录。

根据其主键删除记录也很常见。它有一个快捷方式,可以接受任何单列主键

// DELETE FROM player WHERE id = 1
try Player.deleteOne(db, key: 1)

// DELETE FROM player WHERE id IN (1, 2, 3)
try Player.deleteAll(db, keys: [1, 2, 3])

// DELETE FROM country WHERE isoCode = 'FR'
try Country.deleteOne(db, key: "FR")

// DELETE FROM country WHERE isoCode IN ('FR', 'US')
try Country.deleteAll(db, keys: ["FR", "US"])

当表中没有明确定义的主键时,GRDB使用隐藏的“rowid”列。

// DELETE FROM document WHERE rowid = 1
try Document.deleteOne(db, key: 1)

对于定义在唯一索引中的多列主键和唯一键,请提供字典。

// DELETE FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
try Citizenship.deleteOne(db, key: ["citizenId": 1, "countryCode": "FR"])

// DELETE FROM player WHERE email = '[email protected]'
Player.deleteOne(db, key: ["email": "[email protected]"])

自定义请求

到目前为止,我们已经看到了从采用TableRecord协议的任何类型创建的请求

let request = Player.all()  // QueryInterfaceRequest<Player>

那些类型为QueryInterfaceRequest的请求可以获取和计数

try request.fetchCursor(db) // A Cursor of Player
try request.fetchAll(db)    // [Player]
try request.fetchOne(db)    // Player?
try request.fetchCount(db)  // Int

当查询接口无法生成你需要的SQL时,你仍然可以回退到原始SQL

// Custom SQL is always welcome
try Player.fetchAll(db, "SELECT ...")   // [Player]

但也许你更喜欢在自定义请求中恢复一些优雅,并构建自定义请求

// No custom SQL in sight
try Player.customRequest().fetchAll(db) // [Player]

自定义请求还可以提供ValueObservation

try ValueObservation.
    .trackingAll(Player.customRequest(...))
    .start(in: dbQueue) { players: [Player] in
        print("Players have changed")
    }

FetchRequest协议

FetchRequest是执行单个SELECT语句并知道如何解释所获取行的所有请求的协议。

protocol FetchRequest: DatabaseRegionConvertible {
    /// The type that tells how fetched rows should be decoded
    associatedtype RowDecoder
    
    /// A tuple that contains a prepared statement, and an eventual row adapter.
    func prepare(_ db: Database) throws -> (SelectStatement, RowAdapter?)
    
    /// The number of rows fetched by the request.
    func fetchCount(_ db: Database) throws -> Int
}

当关联的RowDecoder类型是或符合FetchableRecord的类型时,请求可以获取:请参见下文从自定义请求获取

prepare方法返回一个预处理语句和可选的行适配器。预处理语句指示应执行哪个SQL查询。行适配器帮助以行解码器期望的方式显示所获取的行(请参阅行适配器)。

fetchCount方法有一个默认实现,该实现从prepare返回的语句中构建了一个正确但简单的SQL查询:SELECT COUNT(*) FROM (...)。采用类型可以通过自定义它们的fetchCount实现来完善计数SQL。

基本DatabaseRegionConvertible协议涉及数据库观察。有关更多信息,请参阅DatabaseRegionDatabaseRegionObservationValueObservation

FetchRequest协议被查询接口请求采用

// A FetchRequest whose RowDecoder associated type is Player:
let request = Player.all()

构建自定义请求

要构建自定义请求,您可以使用内置请求之一,从其他请求中派生请求,或者创建自己的请求类型,该类型采用FetchRequest协议。

  • SQLRequest是从原始SQL构建的请求。例如

    extension Player {
        static func filter(color: Color) -> SQLRequest<Player> {
            return SQLRequest<Player>(
                "SELECT * FROM player WHERE color = ?"
                arguments: [color])
        }
    }
    
    // [Player]
    try Player.filter(color: .red).fetchAll(db)
  • asRequest(of:)方法更改请求获取的类型。这在您使用关联时非常有用

    struct BookInfo: FetchableRecord, Decodable {
        var book: Book
        var author: Author
    }
    
    let request = Book
        .including(required: Book.author)
        .asRequest(of: BookInfo.self)
    
    // [BookInfo]
    try request.fetchAll(db)
  • adapted(_:)方法简化了对具有行适配器的复杂行的处理。有关使用此方法的示例代码,请参阅连接查询支持

  • AnyFetchRequest:一种类型擦除请求。

从自定义请求中获取数据

采用FetchRequest类型时,当其关联的RowDecoder类型可以解码数据库行(本身,FetchableRecord)时,它知道必须做什么。

let rowRequest = ...        // Some FetchRequest that fetches Row
try request.fetchCursor(db) // A cursor of rows

let playerRequest = ...     // Some FetchRequest that fetches Player
try request.fetchAll(db)    // [Player]

let intRequest = ...        // Some FetchRequest that fetches Int
try request.fetchOne(db)    // Int?

例如

let playerRequest = SQLRequest<Player>(
    "SELECT * FROM player WHERE color = ?"
    arguments: [color])
try request.fetchAll(db)    // [Player]

查看获取方法,了解fetchCursorfetchAllfetchOne方法的信息。

与FetchRequest关联的RowDecoder类型不一定是Row、DatabaseValueConvertible或FetchableRecord。有关更多信息,请参阅超越FetchableRecord章节。

迁移

迁移是一种方便的方式来统一和容易地修改数据库模式。

迁移按顺序运行,仅运行一次。当用户升级应用程序时,只会运行尚未应用的迁移。

在每次迁移中,您通常会根据不断发展的应用程序需求定义和更新数据库表

var migrator = DatabaseMigrator()

// 1st migration
migrator.registerMigration("v1") { db in
    try db.create(table: "author") { t in ... }
    try db.create(table: "book") { t in ... }
    try db.create(index: ...)
}

// 2nd migration
migrator.registerMigration("v2") { db in
    try db.alter(table: "author") { t in ... }
}

// Migrations for future versions will be inserted here:
//
// // 3rd migration
// migrator.registerMigration("...") { db in
//     ...
// }

每次迁移都在单独的事务中运行。如果发生错误,则回滚其事务,后续迁移不会运行,并最终通过migrator.migrate(dbQueue)抛出错误。

已应用迁移的内存存储在数据库本身(在一个保留表中)。

使用migrate(_:)方法将数据库迁移到最新版本。

try migrator.migrate(dbQueue) // or migrator.migrate(dbPool)

要迁移数据库到特定版本,请使用migrate(_:upTo:)

try migrator.migrate(dbQueue, upTo: "v2")

迁移只能向前运行

try migrator.migrate(dbQueue, upTo: "v2")
try migrator.migrate(dbQueue, upTo: "v1")
// fatal error: database is already migrated beyond migration "v1"

检查是否已应用迁移

let appliedMigrations = try migrator.appliedMigrations(in: dbQueue)
if appliedMigrations.contains("v2") {
    // "v2" migration has been applied
}

eraseDatabaseOnSchemaChange选项

如果数据库迁移更改了其定义,DatabaseMigrator可以自动擦除整个数据库内容,并从头开始重新创建整个数据库。

var migrator = DatabaseMigrator()
migrator.eraseDatabaseOnSchemaChange = true

小心!此标志可能会破坏您宝贵的数据!

但在以下两种情况下可能很有用

  1. 在应用程序开发期间,因为您仍在设计迁移,并且模式更改频繁。

    在这种情况下,建议不要将此标志包含在分布式应用程序中

    var migrator = DatabaseMigrator()
    #if DEBUG
    // Speed up development by nuking the database when migrations change
    migrator.eraseDatabaseOnSchemaChange = true
    #endif
  2. 当数据库内容可以轻松重新创建时,例如某些下载数据的缓存。

eraseDatabaseOnSchemaChange 选项在数据库迁移器检测到 模式变更 时触发数据库的重建。模式变更是指 sqlite_master 表中包含创建数据库表、索引、触发器和视图所用的 SQL 的任何差异。

高级数据库模式变更

SQLite 不支持许多模式变更,例如,不允许使用 "ALTER TABLE ... DROP COLUMN ..." 语法删除表列。

尽管如此,任何类型的模式变更都是可能的。SQLite 文档详细说明了如何进行修改:https://www.sqlite.org/lang_altertable.html#otheralter。这项技术需要暂时禁用外键检查,并由 registerMigrationWithDeferredForeignKeyCheck 函数支持。

// Add a NOT NULL constraint on player.name:
migrator.registerMigrationWithDeferredForeignKeyCheck("AddNotNullCheckOnName") { db in
    try db.create(table: "new_player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
    }
    try db.execute("INSERT INTO new_player SELECT * FROM player")
    try db.drop(table: "player")
    try db.rename(table: "new_player", to: "player")
}

在您的迁移代码运行且外键检查被禁用的情况下,无论是否出现错误,外键检查都会在迁移结束时重新启用并检查。

全文搜索

全文搜索是搜索文本文本文档的高效方式。

// Create full-text tables
try db.create(virtualTable: "book", using: FTS4()) { t in // or FTS3(), or FTS5()
    t.column("author")
    t.column("title")
    t.column("body")
}

// Populate full-text table with records or SQL
try Book(...).insert(db)
try db.execute(
    "INSERT INTO book (author, title, body) VALUES (?, ?, ?)",
    arguments: [...])

// Build search patterns
let pattern = FTS3Pattern(matchingPhrase: "Moby-Dick")

// Search with the query interface or SQL
let books = try Book.matching(pattern).fetchAll(db)
let books = try Book.fetchAll(db,
    "SELECT * FROM book WHERE book MATCH ?",
    arguments: [pattern])

选择全文引擎

SQLite 支持 FTS3、FTS4 和 FTS5 三个全文引擎。

一般来说,FTS5 比FTS4要好,FTS4又比FTS3好。但这并不能真正告诉您应该为应用程序选择哪个引擎。相反,您的选择应取决于以下因素

  • 应用程序需要的全文功能:

    全文需求 FTS3 FTS4 FTS5
    查询
    词汇搜索(包含“数据库”的文档) X X X
    前缀搜索(包含以“data”开头的单词的文档) X X X
    短语搜索(包含短语“SQLite数据库”的文档) X X X
    布尔搜索(包含“SQLite”或“database”的文档) X X X
    邻近搜索(包含“SQLite”靠近“database”的文档) X X X
    ✂️分词
    ASCII大小写不敏感(使得“DATABASE”匹配“database”) X X X
    Unicode大小写不敏感(使得“ÉLÉGANCE”匹配“élégance”) X X X
    拉丁符号不敏感(使得“elegance”匹配“élégance”) X X X
    英语词干处理(使得“frustration”匹配“frustrated”) X X X
    英语词干处理和ASCII大小写不敏感 X X X
    英语词干处理和Unicode大小写不敏感 X
    英语词干处理和拉丁符号不敏感 X
    同义词(使得“1st”匹配“first”) ¹ ¹ X ²
    拼音和罗马字(使得“romaji”匹配“ローマ字”) ¹ ¹ X ²
    停用词(不索引,不匹配诸如“and”和“the”之类的单词) ¹ ¹ X ²
    拼写检查(使得“alamaba”匹配“alabama”) ¹ ¹ ¹
    :bowtie:其他功能
    排名(按相关性排序结果) ¹ ¹ X
    片段(显示匹配的几个单词) X X X

    ¹ 需要额外设置,可能难以实现。

    ² 需要一个自定义分词器(custom tokenizer)

    有关完整功能列表,请参阅SQLite文档。一些缺失的功能可以通过额外的应用程序代码实现。

  • 速度与磁盘空间限制。粗略地说,FTS4和FTS5比FTS3更快,但使用更多空间。FTS4仅支持内容压缩。

  • 索引文本在您的数据库模式中的位置。只有FTS4和FTS5支持“无内容”和“外部内容”表。

  • 集成到您的应用程序中的SQLite库。iOS、macOS和watchOS附带SQLite版本支持FTS3和FTS4,但不总是FTS5。要使用FTS5,请参阅启用FTS5支持

  • 请参阅FTS3与FTS4FTS5与FTS3/4以获取更多差异。

☝️ 注意:如果您仍然有所疑问,建议您阅读SQLite文档:FTS3 & FTS4FTS5

创建FTS3和FTS4虚拟表

FTS3和FTS4全文表存储和索引文本内容。

使用create(virtualTable:using:)方法创建表

// CREATE VIRTUAL TABLE document USING fts3(content)
try db.create(virtualTable: "document", using: FTS3()) { t in
    t.column("content")
}

// CREATE VIRTUAL TABLE document USING fts4(content)
try db.create(virtualTable: "document", using: FTS4()) { t in
    t.column("content")
}

全文表中的所有列都包含文本。如果您需要索引包含其他类型值的表,则需要一个“外部内容”全文表

您可以指定一个分词器

// CREATE VIRTUAL TABLE book USING fts4(
//   tokenize=porter,
//   author,
//   title,
//   body
// )
try db.create(virtualTable: "book", using: FTS4()) { t in
    t.tokenizer = .porter
    t.column("author")
    t.column("title")
    t.column("body")
}

FTS4支持选项

// CREATE VIRTUAL TABLE book USING fts4(
//   content,
//   uuid,
//   content="",
//   compress=zip,
//   uncompress=unzip,
//   prefix="2,4",
//   notindexed=uuid,
//   languageid=lid
// )
try db.create(virtualTable: "document", using: FTS4()) { t in
    t.content = ""
    t.compress = "zip"
    t.uncompress = "unzip"
    t.prefixes = [2, 4]
    t.column("content")
    t.column("uuid").notIndexed()
    t.column("lid").asLanguageId()
}

“content”选项涉及“无内容”和“外部内容”全文表。GRDB可以帮助您定义自动与其内容表同步的全文表。请参阅外部内容全文表

参见 SQLite 文档 获取更多信息。

FTS3 和 FTS4 词法分析器

词法分析器定义了“匹配”的含义。根据您选择的词法分析器,全文搜索将不会返回相同的结果。

SQLite 随带三种内置的 FTS3/4 词法分析器: simpleporterunicode61,它们使用不同的算法将查询与索引内容相匹配

try db.create(virtualTable: "book", using: FTS4()) { t in
    // Pick one:
    t.tokenizer = .simple // default
    t.tokenizer = .porter
    t.tokenizer = .unicode61(...)
}

以下是一些匹配示例

内容 查询 simple porter unicode61
Foo Foo X X X
Foo FOO X X X
Jérôme Jérôme
Jérôme JÉRÔME
Jérôme Jerome
Database Databases X
Frustration Frustrated X

¹ 不要错过 Unicode 全文搜索陷阱

  • simple

    try db.create(virtualTable: "book", using: FTS4()) { t in
        t.tokenizer = .simple   // default
    }

    默认的 "simple" 词法分析器在 ASCII 字符中对大小写不敏感。它将 "foo" 与 "FOO" 匹配,但不会将 "Jérôme" 与 "JÉRÔME" 匹配。

    它不提供词干提取,因此不会将 "databases" 与 "database" 匹配。

    它不会从拉丁字母脚本字符中去掉变音符号,因此不会将 "jérôme" 与 "jerome" 匹配。

  • porter

    try db.create(virtualTable: "book", using: FTS4()) { t in
        t.tokenizer = .porter
    }

    "porter" 词法分析器比较英文单词的词根:它将 "database" 与 "databases" 匹配,以及 "frustration" 与 "frustrated" 匹配。

    它不会从拉丁字母脚本字符中去掉变音符号,因此不会将 "jérôme" 与 "jerome" 匹配。

  • unicode61

    try db.create(virtualTable: "book", using: FTS4()) { t in
        t.tokenizer = .unicode61()
        t.tokenizer = .unicode61(removeDiacritics: false)
    }

    "unicode61" 词法分析器对 uni-code 字符对大小写不敏感。它与 "Jérôme" 匹配 "JÉRÔME"。

    默认情况下,它会从拉丁字母脚本字符中去掉变音符号,与 "jérôme" 匹配 "jerome"。此行为可以禁用,如上例所示。

    它不提供词干提取,因此不会将 "databases" 与 "database" 匹配。

参见 SQLite 词法分析器 获取更多信息。

FTS3Pattern

在 FTS3 和 FTS4 表中执行全文搜索时使用搜索模式

  • database 匹配包含 "database" 的所有文档
  • data* 匹配以 "data" 开头的所有文档
  • SQLite database 匹配包含 "SQLite" 和 "database" 的所有文档
  • SQLite OR database 匹配包含 "SQLite" 或 "database" 的所有文档
  • "SQLite database" 匹配包含 "SQLite database" 短语的所有文档

并非所有搜索模式都有效:它们必须遵循 全文索引查询语法

FTS3Pattern 类型可以帮助您验证模式,并从受信任的字符串(例如用户输入的字符串)构建有效的模式

struct FTS3Pattern {
    init(rawPattern: String) throws
    init?(matchingAnyTokenIn string: String)
    init?(matchingAllTokensIn string: String)
    init?(matchingPhrase string: String)
}

第一个初始化器将您的原始模式与查询语法进行比较,并且可能会抛出一个 DatabaseError

// OK: FTS3Pattern
let pattern = try FTS3Pattern(rawPattern: "sqlite AND database")
// DatabaseError: malformed MATCH expression: [AND]
let pattern = try FTS3Pattern(rawPattern: "AND")

其他三个初始化器不会抛出错误。它们从任何字符串构建一个有效的模式,包括您的应用程序用户提供的数据。它们允许您查找匹配所有给定单词、任意给定单词或完整短语的文档,具体取决于您的应用程序需求。

let query = "SQLite database"
// Matches documents that contain "SQLite" or "database"
let pattern = FTS3Pattern(matchingAnyTokenIn: query)
// Matches documents that contain both "SQLite" and "database"
let pattern = FTS3Pattern(matchingAllTokensIn: query)
// Matches documents that contain "SQLite database"
let pattern = FTS3Pattern(matchingPhrase: query)

当无法从输入字符串构建模式时,它们返回 nil。

let pattern = FTS3Pattern(matchingAnyTokenIn: "")  // nil
let pattern = FTS3Pattern(matchingAnyTokenIn: "*") // nil

FTS3Pattern 是常规的 。您可以用作查询 参数

let documents = try Document.fetchAll(db,
    "SELECT * FROM document WHERE content MATCH ?",
    arguments: [pattern])

查询接口 中使用它们。

// Search in all columns
let documents = try Document.matching(pattern).fetchAll(db)

// Search in a specific column:
let documents = try Document.filter(Column("content").match(pattern)).fetchAll(db)

启用 FTS5 支持

当 FTS3 和 FTS4 全文引擎不能满足您的需求时,您可能想使用 FTS5。请参阅 选择全文引擎 以帮助您做出决定。

随 iOS、macOS 和 watchOS 一起发布的 SQLite 版本并不总是支持 FTS5 引擎。要启用 FTS5 支持,您需要使用以下安装技术之一安装 GRDB。

  1. 使用以下自定义编译选项的 GRDB.swift CocoaPod。它使用系统 SQLite,该 SQLite 已编译为支持 FTS5,但仅限于 iOS 11.4+ / macOS 10.13+ / watchOS 4.3+。

    pod 'GRDB.swift'
    platform :ios, '11.4' # or above
    
    post_install do |installer|
      installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target|
        target.build_configurations.each do |config|
          config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_FTS5"
        end
      end
    end

    ⚠️ 警告:请确保使用正确的平台版本!在较低版本的设备上运行时,您将遇到运行时错误。

    ☝️ 注意:曾经有一个预置 FTS5 支持的 GRDBPlus CocoaPod。该 CocoaPod 已废弃:请切换到上面的方法。

  2. 使用 GRDBCipher CocoaPod。它使用 SQLCipher(请参阅 加密),并要求 iOS 8.0+ / macOS 10.9+ / watchOS 2.0+。

    pod 'GRDBCipher'
  3. 使用 自定义 SQLite 构建 并激活 SQLITE_ENABLE_FTS5 编译选项。

创建 FTS5 虚拟表

FTS5 全文表用于存储和索引文本内容。

要使用 FTS5,您需要一个已激活 SQLITE_ENABLE_FTS5 编译选项的 自定义 SQLite 构建

使用 create(virtualTable:using:) 方法创建 FTS5 表

// CREATE VIRTUAL TABLE document USING fts5(content)
try db.create(virtualTable: "document", using: FTS5()) { t in
    t.column("content")
}

全文表中的所有列都包含文本。如果您需要索引包含其他类型值的表,则需要一个“外部内容”全文表

您可以选择一个 分词器

// CREATE VIRTUAL TABLE book USING fts5(
//   tokenize='porter',
//   author,
//   title,
//   body
// )
try db.create(virtualTable: "book", using: FTS5()) { t in
    t.tokenizer = .porter()
    t.column("author")
    t.column("title")
    t.column("body")
}

FTS5 支持以下 选项

// CREATE VIRTUAL TABLE book USING fts5(
//   content,
//   uuid UNINDEXED,
//   content='table',
//   content_rowid='id',
//   prefix='2 4',
//   columnsize=0,
//   detail=column
// )
try db.create(virtualTable: "document", using: FTS5()) { t in
    t.column("content")
    t.column("uuid").notIndexed()
    t.content = "table"
    t.contentRowID = "id"
    t.prefixes = [2, 4]
    t.columnSize = 0
    t.detail = "column"
}

在 "无内容" 和 "外部内容" 全文表中,contentcontentRowID 选项涉及到。GRDB 可以帮助您定义与内容表自动同步的全文表。请参阅 外部内容全文表

有关更多信息,请参阅 SQLite 文档

FTS5 分词器

词法分析器定义了“匹配”的含义。根据您选择的词法分析器,全文搜索将不会返回相同的结果。

SQLite 随带三个内置的 FTS5 分词器:asciiporterunicode61,它们使用不同的算法来匹配查询与索引内容。

try db.create(virtualTable: "book", using: FTS5()) { t in
    // Pick one:
    t.tokenizer = .unicode61() // default
    t.tokenizer = .unicode61(...)
    t.tokenizer = .ascii
    t.tokenizer = .porter(...)
}

以下是一些匹配示例

内容 查询 ascii unicode61 porter on ascii porter on unicode61
Foo Foo X X X X
Foo FOO X X X X
Jérôme Jérôme
Jérôme JÉRÔME
Jérôme Jerome
Database Databases X X
Frustration Frustrated X X

¹ 不要错过 Unicode 全文搜索陷阱

  • unicode61

    try db.create(virtualTable: "book", using: FTS5()) { t in
        t.tokenizer = .unicode61()
        t.tokenizer = .unicode61(removeDiacritics: false)
    }

    默认的 "unicode61" 分词器对于 Unicode 字符不区分大小写。它与 "Jérôme" 匹配 "JÉRÔME"。

    默认情况下,它会从拉丁字母脚本字符中去掉变音符号,与 "jérôme" 匹配 "jerome"。此行为可以禁用,如上例所示。

    它不提供词干提取,因此不会将 "databases" 与 "database" 匹配。

  • ascii

    try db.create(virtualTable: "book", using: FTS5()) { t in
        t.tokenizer = .ascii
    }

    "ascii" 分词器对于 ASCII 字符不区分大小写。它与 "foo" 匹配 "FOO",但不匹配 "Jérôme" 和 "JÉRÔME"。

    它不提供词干提取,因此不会将 "databases" 与 "database" 匹配。

    它不会从拉丁字母脚本字符中去掉变音符号,因此不会将 "jérôme" 与 "jerome" 匹配。

  • porter

    try db.create(virtualTable: "book", using: FTS5()) { t in
        t.tokenizer = .porter()       // porter wrapping unicode61 (the default)
        t.tokenizer = .porter(.ascii) // porter wrapping ascii
        t.tokenizer = .porter(.unicode61(removeDiacritics: false)) // porter wrapping unicode61 without diacritics stripping
    }

    Porter分词器是一个包装器分词器,它根据单词的词根比较英语单词:它将“database”与“databases”相匹配,将“frustration”与“frustrated”相匹配。

    如果它包装Unicode61中的拉丁字母字符,则从字符中清除变音符号;如果它包装ASCII,则不清除(参见上面示例)。

有关更多信息,请参见SQLite分词器,以及要添加您自己的分词器,请参见自定义FTS5分词器

FTS5Pattern

在FTS5表中执行全文搜索时,使用搜索模式

  • database 匹配包含 "database" 的所有文档
  • data* 匹配以 "data" 开头的所有文档
  • SQLite database 匹配包含 "SQLite" 和 "database" 的所有文档
  • SQLite OR database 匹配包含 "SQLite" 或 "database" 的所有文档
  • "SQLite database" 匹配包含 "SQLite database" 短语的所有文档

并非所有搜索模式都是有效的:它们必须遵循全文查询语法

FTS5Pattern类型可以帮助您验证模式,并从不受信任的字符串(如用户输入)构建有效模式

extension Database {
    func makeFTS5Pattern(rawPattern: String, forTable table: String) throws -> FTS5Pattern
}

struct FTS5Pattern {
    init?(matchingAnyTokenIn string: String)
    init?(matchingAllTokensIn string: String)
    init?(matchingPhrase string: String)
}

Database.makeFTS5Pattern(rawPattern:forTable:)方法验证您的原始模式与查询语法和目标表的列,可能会抛出DatabaseError

// OK: FTS5Pattern
try db.makeFTS5Pattern(rawPattern: "sqlite", forTable: "book")
// DatabaseError: syntax error near \"AND\"
try db.makeFTS5Pattern(rawPattern: "AND", forTable: "book")
// DatabaseError: no such column: missing
try db.makeFTS5Pattern(rawPattern: "missing: sqlite", forTable: "book")

FTS5Pattern初始化器不会抛出异常。从任何字符串构建有效模式,包括您的应用程序用户提供的字符串。根据应用程序的需求,让您找到所有给出单词的文档、任何给出单词的文档或完整短语的文档。

let query = "SQLite database"
// Matches documents that contain "SQLite" or "database"
let pattern = FTS5Pattern(matchingAnyTokenIn: query)
// Matches documents that contain both "SQLite" and "database"
let pattern = FTS5Pattern(matchingAllTokensIn: query)
// Matches documents that contain "SQLite database"
let pattern = FTS5Pattern(matchingPhrase: query)
// Matches documents that start with "SQLite database"
let pattern = FTS5Pattern(matchingPrefixPhrase: query)

当无法从输入字符串构建模式时,它们返回 nil。

let pattern = FTS5Pattern(matchingAnyTokenIn: "")  // nil
let pattern = FTS5Pattern(matchingAnyTokenIn: "*") // nil

FTS5Pattern是常规。您可以将它们用作查询参数

let documents = try Document.fetchAll(db,
    "SELECT * FROM document WHERE document MATCH ?",
    arguments: [pattern])

查询接口 中使用它们。

let documents = try Document.matching(pattern).fetchAll(db)

FTS5: 根据相关性排序

FTS5可以根据相关性排序结果(从最相关到最不相关)

// SQL
let documents = try Document.fetchAll(db,
    "SELECT * FROM document WHERE document MATCH ? ORDER BY rank",
    arguments: [pattern])

// Query Interface
let documents = try Document.matching(pattern).order(Column.rank).fetchAll(db)

有关排名算法的更多信息以及额外选项,请阅读根据辅助函数结果排序

GRDB不提供FTS3和FTS4的排名。如果您真的需要排名,请参阅SQLite的搜索应用提示

外部内容全文表

外部内容表不存储索引的文本。而是索引存储在另一个表中的文本。

如果您想索引一个无法声明为全文表的表(因为它包含非文本值,例如),这将非常方便。您只需定义一个指向常规表的引用的外部内容全文表即可。

两个表必须保持最新,以确保全文索引与常规表的正文内容匹配。使用全文表定义中的synchronize(withTable:)方法,这种同步将自动发生。

// A regular table
try db.create(table: "book") { t in
    t.column("author", .text)
    t.column("title", .text)
    t.column("content", .text)
    ...
}

// A full-text table synchronized with the regular table
try db.create(virtualTable: "book_ft", using: FTS4()) { t in // or FTS5()
    t.synchronize(withTable: "book")
    t.column("author")
    t.column("title")
    t.column("content")
}

常规表中已经存在的最终内容会被索引,而在常规表中发生的每次插入、更新或删除操作都会自动应用于全文索引。

有关更多信息,请参阅SQLite关于外部内容表的文档:FTS4FTS5

还可以参考WWDC Companion,这是一个示例应用,它使用外部内容表来存储、显示并允许用户搜索WWDC会议。

删除同步全文表

全文表与内容表的同步是通过SQL触发器实现的。

SQLite在丢弃内容(非全文)表时会自动删除这些触发器。

然而,在丢弃全文表后,这些触发器仍然存在。除非它们也被删除,否则将阻止在内容表中未来的插入、更新和删除操作,以及创建新的全文表。

要删除这些触发器,请使用dropFTS4SynchronizationTriggersdropFTS5SynchronizationTriggers方法

// Create tables
try db.create(table: "book") { t in
    ...
}
try db.create(virtualTable: "book_ft", using: FTS4()) { t in
    t.synchronize(withTable: "book")
    ...
}

// Drop full-text table
try db.drop(table: "book_ft")
try db.dropFTS4SynchronizationTriggers(forTable: "book_ft")

⚠️ 警告:GRDB版本2.3.1及以下存在一个错误,创建的触发器名称不正确。如果可能全文表是由旧版本的GRDB创建的,那么请删除同步触发器两次:一次使用已删除的全文表名称,一次使用内容表名称

// Drop full-text table
try db.drop(table: "book_ft")
try db.dropFTS4SynchronizationTriggers(forTable: "book_ft")
try db.dropFTS4SynchronizationTriggers(forTable: "book") // Support for GRDB <= 2.3.1

查询外部内容全文表

当您需要执行全文搜索,并且外部内容表包含所需的所有数据时,您可以简单地对全文表进行查询。

但是,如果您需要加载常规表中的列,并且同时执行全文搜索,那么您将需要同时查询这两个表。

这是因为当您尝试在常规表上执行全文搜索时,SQLite会抛出一个错误

// SQLite error 1: unable to use function MATCH in the requested context
// SELECT * FROM book WHERE book MATCH '...'
let books = Book.matching(pattern).fetchAll(db)

解决方案是使用原始SQL进行联合请求

let sql = """
    SELECT book.*
    FROM book
    JOIN book_ft
        ON book_ft.rowid = book.rowid
        AND book_ft MATCH ?
    """
let books = Book.fetchAll(db, sql, arguments: [pattern])

全文记录

您可以在全文虚拟表周围定义记录类型。

然而,这些表没有明确的 主键。相反,它们使用隐式rowid主键:一个名为rowid的特殊隐藏列。

为了通过主键获取、删除和更新全文记录,您必须暴露此隐藏列

Unicode 全文搜索注意事项

SQLite 内置的 FTS3、FTS4 和 FTS5 分词器通常都支持 Unicode,但也有一些注意事项和限制。

一般来说,当内容与查询使用不同的Unicode 标准化时,匹配可能会失败。SQLite 在这方面实际上表现不一致。

例如,“aimé”与“aimé”匹配时,它们最好有相同的标准化:NFC 形式的“aim\u{00E9}”可能不会与其 NFD 等价词“aime\u{0301}”匹配。大多数从 Swift、UIKit 和 Cocoa 获取的字符串都使用 NFC,所以请小心使用 NFD 输入(如来自 HFS+ 文件系统的字符串或不可信的网络输入)。使用String.precomposedStringWithCanonicalMapping将字符串转换为 NFC。

此外,如果想要“fi”匹配连字符“fi”(U+FB01),则需要将索引内容和输入标准化为 NFKC 或 NFKD。使用String.precomposedStringWithCompatibilityMapping将字符串转换为 NFKC。

Unicode 标准化并非故事的终结,因为它不会帮助“Encyclopaedia”匹配“Encyclopædia”、“Mueller”、“Müller”、“Grossmann”、“Großmann”或“Diyarbakır”、“DIYARBAKIR”。可以使用String.applyingTransform方法来帮助。

GRDB 允许您编写自定义 FTS5 分词器,这些分词器可以透明地处理所有这些问题。对于 FTS3 和 FTS4,您需要在将它们注入全文引擎之前预先处理字符串。

快乐的索引!

联合查询支持

GRDB 帮助消耗具有复杂选择的联合查询。

在本章中,我们将重点关注从复杂行中提取信息,例如以下查询获取的行

-- How to consume the left, middle, and right parts of those rows?
SELECT player.*, team.*, MAX(round.score) AS maxScore
FROM player
LEFT JOIN team ON ...
LEFT JOIN round ON ...
GROUP BY ...

我们将不讨论联合查询的生成,这在关联中已概述。

那么我们究竟在讨论什么?

消耗从复杂联合查询中获取的行有难度,因为这些行经常包含多个具有相同名称的列:例如来自 player 表的 id,来自 team 表的 id 等。

当发生这种歧义时,GRDB 行访问器始终偏好最左边的匹配列。这意味着 row["id"] 会给出一个玩家 id,没有明显的访问团队 id 的方法。

避免这种歧义的一种经典技术是给每个列一个唯一名称。例如

-- A classical technique
SELECT player.id AS player_id, player.name AS player_name, team.id AS team_id, team.name AS team_name, team.color AS team_color, MAX(round.score) AS maxScore
FROM player
LEFT JOIN team ON ...
LEFT JOIN round ON ...
GROUP BY ...

这项技术效果很好,但有三点不足

  1. 选择变得难以阅读和理解。
  2. 此类查询难以手工编写。
  3. 乱序的名称对于需要特定列名的可获取记录类型来说非常不适合。毕竟,如果Team记录类型可以读取SELECT * FROM team ...,那么它也应该能够读取SELECT ..., team.*, ...

因此,我们需要另一种技术。以下是我们将如何分割行并保留列名的示例。

SELECT player.*, team.*, MAX(round.score) AS maxScore FROM ... 将被分割成三个部分:一个包含玩家列的部分,一个包含队伍列的部分,以及一个包含剩余列的部分。玩家记录类型将能够读取包含Player.init(row:)初始化器所期望列的第一个部分。同样,队伍记录类型可以读取第二个部分。

与名称混排技术不同,分割行保持了SQL的可读性,接受您手写的SQL查询,并与现有的记录类型良好地协作。

分割行,入门介绍

让我们先写一些入门代码,希望这一章能让你理解各个部分是如何组合在一起的。我们将在稍后看到记录如何帮助我们整合初始方法,如何跟踪在一起请求时的更改,以及我们如何使用标准的Decodable协议。

为了分割行,我们将使用行适配器。行适配器通过适配行,使行消费者能够看到它们想要的恰好列。行适配器可以做很多其他事情,定义多个行作用域以访问尽可能多的行切片。听起来很合适。

在开始时,有一个SQL查询

try dbQueue.read { db in
    let sql = """
        SELECT player.*, team.*, MAX(round.score) AS maxScore
        FROM player
        LEFT JOIN team ON ...
        LEFT JOIN round ON ...
        GROUP BY ...
        """

我们需要一个适配器来提取玩家列,在一个切片中包含和玩家表中的列一样多的列。这就是RangeRowAdapter

    // SELECT player.*, team.*, ...
    //        <------>
    let playerWidth = try db.columns(in: "player").count
    let playerAdapter = RangeRowAdapter(0 ..< playerWidth)

我们还需要一个适配器来提取队伍列

    // SELECT player.*, team.*, ...
    //                  <---->
    let teamWidth = try db.columns(in: "team").count
    let teamAdapter = RangeRowAdapter(playerWidth ..< (playerWidth + teamWidth))

我们将这些适配器合并到一个单一的ScopeAdapter中,这将使我们能够访问这些切片行

    let playerScope = "player"
    let teamScope = "team"
    let adapter = ScopeAdapter([
        playerScope: playerAdapter,
        teamScope: teamAdapter])

现在我们可以获取,并开始消费我们的行。您已经知道行游标

    let rows = try Row.fetchCursor(db, sql, adapter: adapter)
    while let row = try rows.next() {

从获取的行中,我们可以构建一个玩家

        let player: Player = row[playerScope]

在SQL查询中,队伍是通过LEFT JOIN操作符与团队连接的。这意味着队伍可能缺失:其切片可能包含队伍的值,或者它可能只包含NULL值。在这种情况下,我们不想构建一个队伍记录,因此加载一个可选的队伍

        let team: Team? = row[teamScope]

最后,我们可以加载最大分数,假设“maxScore”列不具有歧义

        let maxScore: Int = row["maxScore"]
        
        print("player: \(player)")
        print("team: \(team)")
        print("maxScore: \(maxScore)")
    }
}

💡在这一章中,我们学习了以下内容

  • 如何使用RangeRowAdapter将特定表的列提取到行切片中。
  • 如何使用ScopeAdapter通过命名范围访问多个行切片。
  • 如何使用行子脚本从行中提取记录,或可选记录以处理左侧连接。

按记录方式划分行

我们上面的介绍引入了重要的技术。它使用行适配器来划分行。它使用行下标从行切片中提取记录。

但我们可能想让它更易于使用和更健壮

  1. 通常消费记录比消费原始行要容易。
  2. 连接的记录不一定需要表中所有列(参见请求选择的列TableRecord.databaseSelection))。
  3. 构建行适配器既耗时又容易出错。

为了解决第一个要点,让我们定义一个包含我们的玩家、可选的球队和最高分的记录。因为它可以解码数据库行,所以它采用FetchableRecord协议

struct PlayerInfo {
    var player: Player
    var team: Team?
    var maxScore: Int
}

/// PlayerInfo can decode rows:
extension PlayerInfo: FetchableRecord {
    private enum Scopes {
        static let player = "player"
        static let team = "team"
    }
    
    init(row: Row) {
        player = row[Scopes.player]
        team = row[Scopes.team]
        maxScore = row["maxScore"]
    }
}

现在让我们编写一个获取PlayerInfo记录的方法

extension PlayerInfo {
    static func fetchAll(_ db: Database) throws -> [PlayerInfo] {

为了承认Player和Team记录都可能自定义它们的"玩家"和"球队"列的选择,我们将以稍微不同的方式编写我们的SQL

        // Let Player and Team customize their selection:
        let sql = """
            SELECT
                \(Player.selectionSQL()), -- instead of player.*
                \(Team.selectionSQL()),   -- instead of team.*
                MAX(round.score) AS maxScore
            FROM player
            LEFT JOIN team ON ...
            LEFT JOIN round ON ...
            GROUP BY ...
            """

Player.selectionSQL()将输出player.*,除非Player定义了自定义选择

☝️ 注意:您还可以使用SQL表别名

let sql = """
    SELECT
        \(Player.selectionSQL(alias: "p")),
        \(Team.selectionSQL(alias: "t")),
        MAX(r.score) AS maxScore
    FROM player p
    LEFT JOIN team t ON ...
    LEFT JOIN round r ON ...
    GROUP BY ...
    """

现在是我们构建适配器的时刻(考虑到玩家和球队的自定义选择)。我们使用全局函数splittingRowAdapters,该函数构建所需的宽度行适配器

        let adapters = try splittingRowAdapters(columnCounts: [
            Player.numberOfSelectedColumns(db),
            Team.numberOfSelectedColumns(db)])
    
        let adapter = ScopeAdapter([
            Scopes.player: adapters[0],
            Scopes.team: adapters[1]])

☝️ 注意splittingRowAdapters根据需要返回适配器数量,以完全划分行。在上面的示例中,它返回三个适配器:一个用于玩家,一个用于球队,一个用于剩余的列。

然后,我们可以获取玩家信息

        return try PlayerInfo.fetchAll(db, sql, adapter: adapter)
    }
}

当您的应用程序需要获取玩家信息时,现在读取

// Fetch player infos
let playerInfos = try dbQueue.read { db in
    try PlayerInfo.fetchAll(db)
}

💡在这一章中,我们学习了以下内容

  • 如何定义一个FetchableRecord记录,以消费从连接查询中检索到的行。
  • 如何使用selectionSQLnumberOfSelectedColumns来处理定义为自定义选择的嵌套记录类型。
  • 如何使用splittingRowAdapters来简化行切片的定义。
  • 如何收集与数据库相关的所有相关方法和常量在一个记录类型中,完全负责其与数据库的关系。

按请求方式划分行

上面的PlayerInfo.fetchAll方法直接检索记录。这是好的,但为了从数据库观察中获益,您需要一个定义数据库查询的自定义请求

在深入这些示例代码之前,建议您先阅读前面的段落。我们以与上面相同的PlayerInfo记录开始

struct PlayerInfo {
    var player: Player
    var team: Team?
    var maxScore: Int
}

/// PlayerInfo can decode rows:
extension PlayerInfo: FetchableRecord {
    private enum Scopes {
        static let player = "player"
        static let team = "team"
    }
    
    init(row: Row) {
        player = row[Scopes.player]
        team = row[Scopes.team]
        maxScore = row["maxScore"]
    }
}

现在我们编写一个返回请求的方法,然后在那个请求的基础上构建检索方法

extension PlayerInfo {
    /// The request for all player infos
    static func all() -> AdaptedFetchRequest<SQLRequest<PlayerInfo>> {
        let sql = """
            SELECT
                \(Player.selectionSQL()),
                \(Team.selectionSQL()),
                MAX(round.score) AS maxScore
            FROM player
            LEFT JOIN team ON ...
            LEFT JOIN round ON ...
            GROUP BY ...
            """
        return SQLRequest<PlayerInfo>(sql).adapted { db in
            let adapters = try splittingRowAdapters(columnCounts: [
                Player.numberOfSelectedColumns(db),
                Team.numberOfSelectedColumns(db)])
            return ScopeAdapter([
                Scopes.player: adapters[0],
                Scopes.team: adapters[1]])
        }
    }
    
    /// Fetches all player infos
    static func fetchAll(_ db: Database) throws -> [PlayerInfo] {
        return try all().fetchAll(db)
    }
}

现在是开始使用我们的请求的时间了

// Fetch player infos
let playerInfos = try dbQueue.read { db in
    try PlayerInfo.fetchAll(db)
}

// Track player infos with RxRGDB:
PlayerInfo.all()
    .rx.fetchAll(in: dbQueue)
    .subscribe(onNext: { playerInfos: [PlayerInfo] in
        print("Player infos have changed")
    })

💡在本章中,我们学习了如何定义一个自定义请求,该请求既可以从连接查询中检索记录,还可以喂养数据库观察工具。

按可编码方式拆分行

可编码记录在标准可解编码协议的基础上构建,用于解码数据库行。

您还可以使用可编码记录消费复杂的连接查询。作为演示,我们将重写上面的样本代码

struct Player: Decodable, FetchableRecord, TableRecord {
    var id: Int64
    var name: String
}
struct Team: Decodable, FetchableRecord, TableRecord {
    var id: Int64
    var name: String
    var color: Color
}
struct PlayerInfo: Decodable, FetchableRecord {
    var player: Player
    var team: Team?
    var maxScore: Int
}

extension PlayerInfo {
    /// The request for all player infos
    static func all() -> AdaptedFetchRequest<SQLRequest<PlayerInfo>> {
        let sql = """
            SELECT
                \(Player.selectionSQL()),
                \(Team.selectionSQL()),
                MAX(round.score) AS maxScore
            FROM player
            LEFT JOIN team ON ...
            LEFT JOIN round ON ...
            GROUP BY ...
            """
        return SQLRequest<PlayerInfo>(sql).adapted { db in
            let adapters = try splittingRowAdapters(columnCounts: [
                Player.numberOfSelectedColumns(db),
                Team.numberOfSelectedColumns(db)])
            return ScopeAdapter([
                CodingKeys.player.stringValue: adapters[0],
                CodingKeys.team.stringValue: adapters[1]])
        }
    }
    
    /// Fetches all player infos
    static func fetchAll(_ db: Database) throws -> [PlayerInfo] {
        return try all().fetchAll(db)
    }
}

// Fetch player infos
let playerInfos = try dbQueue.read { db in
    try PlayerInfo.fetchAll(db)
}

// Track player infos with RxRGDB:
PlayerInfo.all()
    .rx.fetchAll(in: dbQueue)
    .subscribe(onNext: { playerInfos: [PlayerInfo] in
        print("Player infos have changed")
    })

💡在这一章中,我们学习了如何使用Decodable协议及其相关的CodingKeys枚举以简化我们的代码。

数据库更改观察

SQLite会通知其主机应用程序对数据库进行的更改,以及事务提交和回滚。

GRDB将此SQLite功能用于一些很好的用途,并允许您以各种方式观察数据库

数据库观察需要在整个数据库使用期间保持单个 数据库队列 开启。

提交后挂钩

当您的应用程序需要在执行某些工作之前确保特定的数据库事务已经成功提交时,请使用Database.afterNextTransactionCommit(_:)方法。

其闭包参数会在数据库更改成功写入磁盘后立即被调用

try dbQueue.write { db in
    db.afterNextTransactionCommit { db in
        print("success")
    }
    ...
} // prints "success"

闭包在受保护的分发队列中运行,以序列化所有数据库更新。

这个“提交后挂钩”有助于将数据库与其他资源,如文件或系统传感器进行同步。

在下面示例中,一个位置管理器仅在成功存储在数据库中时会开始监测一个CLRegion

/// Inserts a region in the database, and start monitoring upon
/// successful insertion.
func startMonitoring(_ db: Database, region: CLRegion) throws {
    // Make sure database is inside a transaction
    try db.inSavepoint {
        
        // Save the region in the database
        try insert(...)
        
        // Start monitoring if and only if the insertion is
        // eventually committed
        db.afterNextTransactionCommit { _ in
            // locationManager prefers the main queue:
            DispatchQueue.main.async {
                locationManager.startMonitoring(for: region)
            }
        }
        
        return .commit
    }
}

上面的方法如果事务最终回滚(显式或因为错误)则不会触发位置管理器,如下面的样本代码所示

try dbQueue.write { db in
    // success
    try startMonitoring(db, region)
    
    // On error, the transaction is rollbacked, the region is not inserted, and
    // the location manager is not invoked.
    try failableMethod(db)
}

值观察和数据区域观察

值观察数据区域观察是两种数据库观察工具,用于跟踪数据库请求的变化。

// Let's observe all players!
let request = Player.all()

值观察向您的应用程序提供最新的值(这是大多数应用程序所需要的内容👍):

let observation = ValueObservation.trackingAll(request)
let observer = observation.start(in: dbQueue) { players: [Player] in
    let names = players.map { $0.name }.joined(separator: ", ")
    print("Fresh players: \(names)")
}

try dbQueue.write { db in
    try Player(name: "Arthur").insert(db)
}
// Prints "Fresh players: Arthur, ..."

数据区域观察在关键数据库事务提交后立即向您的应用程序提供数据库连接(适用于更复杂的用例🤓):

let observation = DatabaseRegionObservation(tracking: request)
let observer = observation.start(in: dbQueue) { db: Database in
    print("Players have changed.")
}

try dbQueue.write { db in
    try Player(name: "Barbara").insert(db)
}
// Prints "Players have changed."

值观察

值观察跟踪数据库请求的结果变化,并在数据库发生变化时通知最新的值。

仅在有更改在数据库中提交后才通知这些更改。不会错过任何被跟踪表的插入、更新或删除。这包括由外键SQL触发器触发的间接变化。

值观察使用方法

以下是一个典型的UIViewController,用于观察数据库,以保持其视图的最新状态

class PlayerViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    private var observer: TransactionObserver?
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Define a ValueObservation which tracks a player
        let request = Player.filter(key: 42)
        let observation = ValueObservation.trackingOne(request)
        
        // Start observing the database
        observer = try! observation.start(
            in: dbQueue,
            onChange: { [unowned self] player: Player? in
                // Player has changed: update view
                self.nameLabel.text = player?.name
            })
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Stop observing the database
        observer = nil
    }
}

默认情况下,所有值都在主队列上通知。视图可以直接从onChange回调中更新。

当观察开始时立即执行初始获取操作:当viewWillAppear方法返回时,视图已设置并准备就绪。

通过start方法返回的观察者存储在视图控制器的属性中。这使得视图控制器可以控制观察的持续时间。当观察者被释放时,观察结束。同时,所有修改被观察玩家的交易都会通知,并且nameLabel保持最新。

💡 提示:请参阅示例应用程序,了解使用值观察的示例。

ValueObservation.trackingCount, trackingOne, trackingAll

给定一个 请求,您可追踪其结果数量、第一个或所有结果

ValueObservation.trackingCount(request)
ValueObservation.trackingOne(request)
ValueObservation.trackingAll(request)

这些观察匹配 fetchCountfetchOnefetchAll 请求方法

  • trackingCount 通知计数

    // Observe number of players
    let observer = ValueObservation
        .trackingCount(Player.all())
        .start(in: dbQueue) { count: Int in
            print("Number of players have changed: \(count)")
        }
  • trackingOne 通知可选值,从单个数据库行(如有)构建

    // Observe a single player
    let observer = ValueObservation
        .trackingOne(Player.filter(key: 1))
        .start(in: dbQueue) { player: Player? in
            print("Player has changed: \(player)")
        }
    
    // Observe the maximum score
    let request = Player.select(max(Column("score")), as: Int.self)
    let observer = ValueObservation
        .trackingOne(request)
        .start(in: dbQueue) { maximumScore: Int? in
            print("Maximum score has changed: \(maximumScore)")
        }
  • trackingAll 通知数组

    // Observe all players
    let observer = ValueObservation
        .trackingAll(Player.all())
        .start(in: dbQueue) { players: [Player] in
            print("Players have changed: \(players)")
        }
    
    // Observe all player names
    let request = SQLRequest<String>("SELECT name FROM player")
    let observer = ValueObservation
        .trackingAll(request)
        .start(in: dbQueue) { names: [String] in
            print("Player names have changed: \(names)")
        }

☝️ 注意:ValueObservation.trackingCount, trackingOne, 和 trackingAll 方法的返回观察执行了对基于原始数据库值的连续相同值的过滤。

ValueObservation.tracking(_:fetch:)

像上面那样,使用简单的请求来表示观察数据库并不总是容易的。

例如,假设我们有一个定义了“名人堂”的 struct。

struct HallOfFame {
    var totalPlayerCount: Int
    var bestPlayers: [Player]

    /// Fetch a HallOfFame
    static fetch(_ db: Database) throws -> HallOfFame {
        let totalPlayerCount = try Player.fetchCount(db)
        let bestPlayers = try Player
            .order(Column("score").desc)
            .limit(10)
            .fetchAll(db)
        return HallOfFame(
            totalPlayerCount: totalPlayerCount,
            bestPlayers: bestPlayers)
    }
}

let hallOfFame = try dbQueue.read { db in try HallOfFame.fetch(db) }
print("""
    Best players out of \(hallOfFame.totalPlayerCount):
    \(hallOfFame.bestPlayers)
    """)

为了跟踪名人堂的改变,我们将使用 ValueObservation.tracking(_:fetch:) 方法。它接受两个参数

  1. 一系列观察请求。
  2. 一个闭包,当任何一个观察请求被修改时,它都会获取新的值。

在我们的例子中,任何对 player 表的改变都可能影响名人堂。因此,我们跟踪所有玩家请求 Player.all(),并在球员更改时获取新的名人堂。

let observation = ValueObservation.tracking(Player.all(), fetch: { db in
    try HallOfFame.fetch(db)
})

let observer = observation.start(in: dbQueue) { hallOfFame: HallOfFame in
    print("""
        Best players out of \(hallOfFame.totalPlayerCount):
        \(hallOfFame.bestPlayers)
        """)
}

过滤连续相同值

有时候,数据库的变化并不一定会改变观察到的值。例如,名人堂不会被最差玩家发生的变化所影响。

当这样的数据库变化发生时,ValueObservation.tracking(_:fetch:) 被触发,以防最佳玩家的修改,并最终通知相同连续的值。

您可以使用 ValueObservation.distinctUntilChanged 方法来过滤这些重复项。它要求观察的值实现 Equatable 协议

extension HallOfFame: Equatable { ... }

let observation = ValueObservation
    .tracking(Player.all(), fetch: HallOfFame.fetch)
    .distinctUntilChanged()

let observer = observation.start(in: dbQueue) { hallOfFame: HallOfFame in
    print("""
        Best players out of \(hallOfFame.totalPlayerCount):
        \(hallOfFame.bestPlayers)
        """)
}

DatabaseRegionConvertible 观察结果

ValueObservation.tracking(_:fetch:) 方法的初始参数可以传入请求,通常是采用 DatabaseRegionConvertible 协议的值。

得益于 DatabaseRegionConvertible,下面的 TeamInfoRequest 不仅能够请求团队和其球员,还能够被观察。

/// A team and its players
struct TeamInfo {
    var team: Team
    var players: [Player]
}

/// A request that can fetch TeamInfo, given a team id.
struct TeamInfoRequest {
    var teamId: Int64
    
    /// The request for the team
    private var teamRequest: QueryInterfaceRequest<Team> {
        return Team.filter(key: teamId)
    }
    
    /// The request for the players
    private var playersRequest: QueryInterfaceRequest<Player> {
        return Player.filter(Column("teamId") == teamId)
    }
    
    /// Fetch a TeamInfo
    func fetch(_ db: Database) throws -> TeamInfo? {
        guard let team = try teamRequest.fetchOne(db) else {
            return nil
        }
        let players = try playersRequest.fetchAll(db)
        return TeamInfo(team: team, players: players)
    }
}

/// Make TeamInfoRequest observable
extension TeamInfoRequest: DatabaseRegionConvertible {
    func databaseRegion(_ db: Database) throws -> DatabaseRegion {
        // Returns the union of the team region and the players region
        let teamRegion = try teamRequest.databaseRegion(db)
        let playersRegion = try playersRequest.databaseRegion(db)
        return teamRegion.union(playersRegion)
    }
}

let request = TeamInfoRequest(teamId: 1)

// Simple fetch
let teamInfo: TeamInfo? = try dbQueue.read(request.fetch)

// Observation
let observer = ValueObservation
    .tracking(request, fetch: request.fetch)
    .start(in: dbQueue) { teamInfo: TeamInfo? in
        print("Team and its players have hanged.")
    }

ValueObservation 转换

ValueObservation.map

map 方法允许您转换由 ValueObservation 通知的值。

例如

// Observe a player's profile image
let observation = ValueObservation
    .trackingOne(Player.filter(key: 42))
    .map { player in player?.loadBigProfileImage() }

let observer = observation.start(in: dbQueue) { image: UIImage? in
    print("Player picture has changed")
}

转换闭包不会在主线程上运行,适合执行重计算。

ValueObservation.compactMap

compactMap 方法允许您转换和过滤由 ValueObservation 通知的值。只有非 nil 的新转换值才会被通知。

例如

// Observe a player
let observation = ValueObservation
    .trackingOne(Player.filter(key: 42))
    .compactMap { $0 }
    
let observer = observation.start(in: dbQueue) { player: Player in
    print("Player name: \(player.name)")
}

转换闭包不会在主线程上运行,适合执行重计算。

ValueObservation.distinctUntilChanged

distinctUntilChanged 方法过滤出由 ValueObservation 通知的连续相等值。观察的值必须遵守 Equatable 协议。

例如

let observation = ValueObservation
    .trackingOne(Player.filter(key: 42))
    .map { player in player != nil } // existence test
    .distinctUntilChanged()

let observer = observation.start(in: dbQueue) { exists: Bool in
    if exists {
        print("Player 42 exists.")
    } else {
        print("Player 42 does not exist.")
    }
}

☝️ 注意:由 ValueObservation.trackingCount、trackingOne 和 trackingAll 方法返回的观察结果已经根据原始数据库值进行过类似的筛选。

ValueObservation.combine(...)

有时你需要同时观察多个请求。例如,你需要同时观察一个团队和它的队员。

在这种情况下,使用 ValueObservation.combine(...) 方法将多个观察结果合并在一起。

// The two observed requests (the team and its players)
let teamRequest = Team.filter(key: 1)
let playersRequest = Player.filter(Column("teamId") == 1)

// Two observations
let teamObservation = ValueObservation.trackingOne(teamRequest)
let playersObservation = ValueObservation.trackingAll(playersRequest)

// The combined observation
let observation = ValueObservation.combine(teamObservation, playersObservation)

// Start tracking players and teams
let observer = observation.start(in: dbQueue) { (team: Team?, players: [Player]) in
    print("Team or players have changed.")
}

合并观察结果可以保证通知的值是 一致的

☝️ 注意:可以合并多达五个观察结果。如有需要更多数量的请求,请提交一个 pull 请求。

☝️ 注意:熟悉反应式编程的读者会认出 ValueObservation.combine 方法中的 CombineLatest 操作符。尽管反应式操作符不考虑数据一致性,但如果使用类似 RxGRDB 的反应式层,请使用 ValueObservation.combine 而不是 CombineLatest 操作符来组合观察结果。

ValueObservation 错误处理

当你开始一个观察时,你可以提供一个 onError 回调。每当在数据库更改后获取新鲜值时发生错误时,就会调用此回调。它像值一样调度(参看 ValueObservation.scheduling)。

let observer = try observation.start(
    in: dbQueue,
    onError: { error in
        print("fresh value could not be fetched")
    },
    onChange: { value in
        print("fresh value: \(value)")
    })

ValueObservation 选项

可以配置某些观察结果的包装行为。

ValueObservation.extent

extent 属性允许你指定观察的持续时间。

默认作用域是 .observerLifetime:当通过 start 返回的观察者被释放时,观察停止。

您可以使用 .databaseLifetime 作用域来指定观察持续到数据库连接关闭。

// No need to retain the observer returned by the start method:
var observation = ValueObservation...
observation.extent = .databaseLifetime
_ = observation.start(in: dbQueue) { newValue in ... }

⚠️ 警告:不要使用 .nextTransaction 生存期,因为这会产生不可靠的结果。GRDB 的未来版本将弃用 ValueObservation.extent,并提供更好的 API。

ValueObservation.scheduling

使用 scheduling 属性可以控制何时通知最新的值。

  • .mainQueue(默认):所有值都在主队列上通知。

    如果观察在主队列上启动,则会在订阅时立即(同步)通知初始值。

    // On main queue
    let observer = ValueObservation
        .trackingAll(Player.all())
        .start(in: dbQueue) { players: [Player] in
            // On main queue
            print("fresh players: \(players)")
        }
    // <- here "fresh players" is already printed.

    如果观察不在主队列上启动,那么初始值也会在主队列上异步通知。

    // Not on the main queue
    let observer = ValueObservation
        .trackingAll(Player.all())
        .start(in: dbQueue) { players: [Player] in
            // On main queue
            print("fresh players: \(players)")
        }

    当数据库发生变化时,新鲜值会异步通知。

    // Eventually prints "fresh players" on the main queue
    try dbQueue.write { db in
        try Player(...).insert(db)
    }
  • .onQueue(_:startImmediately:):所有值都在指定的队列上异步通知。

    如果 startImmediately 为 true,则会获取初始值并通知。

    let customQueue = DispatchQueue(label: "customQueue")
    var observation = ValueObservation.trackingAll(Player.all())
    observation.scheduling = .onQueue(customQueue, startImmediately: true)
    let observer = try observation.start(in: dbQueue) { players: [Player] in
        // On customQueue
        print("fresh players: \(players)")s
    }
  • unsafe(startImmediately:):值不会在相同的调度队列上通知。

    如果 startImmediately 为 true,则初始值会在订阅时立即、同步地在观察启动的调度队列上通知。

    // On any queue
    var observation = ValueObservation.trackingAll(Player.all())
    observation.scheduling = .unsafe(startImmediately: true)
    let observer = try observation.start(in: dbQueue) { players: [Player] in
        print("fresh players: \(players)")
    }
    // <- here "fresh players" is already printed.

    数据库变化时,其他值会在未指定的队列上通知。

    ☝️ 注意:这种不安全模式主要是为那些提供自己的调度引擎的第三方库设计的。

ValueObservation.requiresWriteAccess

默认情况下,requiresWriteAccess 的属性为 false。当为 true 时,ValueObservation 有数据库的写访问权限,并且其获取操作会被自动包装在一个 保存点 中。

var observation = ValueObservation.tracking(..., fetch: { db in
    // write access allowed
})
observation.requiresWriteAccess = true

当您使用 数据库池 时,除非您真的需要,否则不要使用此标志。具有写访问权限的观察效率较低,因为它们在整个获取期间会阻塞所有的写入。

高级:ValueObservation.tracking(_:reducer:)

定义 ValueObservation 的最低级方式是从一个观察的数据库区域(见上面)和一个采用 ValueReducer 协议(🔥 实验)的 reducer 创建它。

protocol ValueReducer {
    associatedtype Fetched
    associatedtype Value
    
    /// Fetches a database value
    func fetch(_ db: Database) throws -> Fetched
    
    /// Returns a notified value
    mutating func value(_ fetched: Fetched) -> Value?
}

当观察到的 数据库区域 发生变化时,会调用 fetch 方法。该方法在受保护的调度队列中运行,并保证获取数据库最后提交状态的不可变视图。

value 方法将获取的值转换成通知的值。如果观察者不应该被通知,则返回 nil。它在一个名为“reducer queue”的调度队列中运行,这不是主队列,也不是数据库队列。

下面的示例代码统计了玩家表被修改的次数:

var count = 0
let reducer = AnyValueReducer(
    fetch: { _ in /* don't fetch anything */ },
    value: { _ -> Int? in
        defer { count += 1 }
        return count })
let observation = ValueObservation.tracking(Player.all(), reducer: { _ in reducer })
let observer = observation.start(in: dbQueue) { count: Int in
    print("Number of transactions that have modified players: \(count)")
}
// Prints "Number of transactions that have modified players: 0"

try dbQueue.write { db in
    try Player(...).insert(db)
}
// Prints "Number of transactions that have modified players: 1"

DatabaseRegionObservation

DatabaseRegionObservation 跟踪数据库 请求 的变化,并通知每个受影响的 事务

不会错过跟踪表中任何插入、更新或删除操作。这包括由外键或 触发器 触发的间接更改。

DatabaseRegionObservation 在数据库变更提交后立即调用您的应用程序,在其他任何线程有机会执行进一步更改之前。这是一个相当强的保证,大多数应用程序实际上并不需要。相反,大多数应用程序更喜欢在接收到新鲜值时被通知:确保在使用 DatabaseRegionObservation 之前检查 ValueObservation

DatabaseRegionObservation 使用方法

通过提供要跟踪的一个或多个请求来定义一个观察器。

// Track all players
let observation = DatabaseRegionObservation(tracking: Player.all())

然后从一个 数据库队列 中启动观察器。

let observer = observation.start(in: dbQueue) { db: Database in
    print("Players were changed")
}

享受更改通知

try dbQueue.write { db in
    try Player(name: "Arthur").insert(db)
}
// Prints "Players were changed"

默认情况下,观察器将持续到由 start 方法返回的观察器被释放为止。有关更多详细信息,请参阅 DatabaseRegionObservation.extent

您也可以向 DatabaseRegionObservation 提供数据库区域 (DatabaseRegion),或任何符合 DatabaseRegionConvertible 协议的类型。例如

// Observe the full database
let observation = DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase)
let observer = observation.start(in: dbQueue) { db: Database in
    print("Database was changed")
}

DatabaseRegionObservation 用例

DatabaseRegionObservation 的用例非常少.

例如

  • 在具有影响的事务之后需要对数据库进行写入。

  • 需要将数据库文件的内容与某些外部资源同步,例如其他文件或系统传感器,如 CLRegion 监视。

  • 在 iOS 上,需要在操作系统有机会将应用程序置于挂起状态之前处理数据库事务。

  • 希望创建一个带有保证快照内容的 数据库快照

在这些用法之外,使用 DatabaseRegionObservation 很可能 是错误的。请检查其他数据库观察选项。

DatabaseRegionObservation.extent

extent 属性用于指定观察的持续时间。有关更多详细信息,请参阅观察范围

// This observation lasts until the database connection is closed
var observation = DatabaseRegionObservation...
observation.extent = .databaseLifetime
_ = observation.start(in: dbQueue) { db in ... }

默认作用域是 .observerLifetime:当通过 start 返回的观察者被释放时,观察停止。

无论观察的范围如何,您都可以始终使用 remove(transactionObserver:) 方法停止观察。

// Start
let observer = observation.start(in: dbQueue) { db in ... }

// Stop
dbQueue.remove(transactionObserver: observer)

FetchedRecordsController

FetchedRecordsController 跟踪请求结果的变化、向表格视图和集合视图提供数据,并在请求结果变化时对单元格进行动画处理。

其外观和行为与Core Data的NSFetchedResultsController非常相似。

给定一个请求和一个接受FetchableRecord协议的类型的请求,例如Record类的子类,FetchedRecordsController可以跟踪请求结果的变化、通知这些变化,并且以适合表格视图或集合视图的格式返回请求结果,每个检索到的记录对应一个单元格。

☝️ 注意:如果您不需要对表格或集合视图进行动画处理,请使用 ValueObservationRxGRDB

💡 提示:请参阅 演示应用程序 中的示例,使用 FetchedRecordsController。

创建 Fetched Records 控制器

初始化 Fetched Records 控制器时,你必须提供以下强制信息:

class Player : Record { ... }
let dbQueue = DatabaseQueue(...)    // or DatabasePool

// Using a Request from the Query Interface:
let controller = FetchedRecordsController(
    dbQueue,
    request: Player.order(Column("name")))

// Using SQL, and eventual arguments:
let controller = FetchedRecordsController<Player>(
    dbQueue,
    sql: "SELECT * FROM player ORDER BY name WHERE countryCode = ?",
    arguments: ["FR"])

获取请求可以涉及多个数据库表。获取记录控制器将仅跟踪获取请求所使用的列和表中的更改。

let controller = FetchedRecordsController<Author>(
    dbQueue,
    sql: """
        SELECT author.name, COUNT(book.id) AS bookCount
        FROM author
        LEFT JOIN book ON book.authorId = author.id
        GROUP BY author.id
        ORDER BY author.name
        """)

创建实例后,您调用performFetch()来执行获取操作。

try controller.performFetch()

响应更改

通常,FetchedRecordsController通过通知来响应数据库层面上的更改,当数据库行位置或值发生变化时会发出通知。

更改只有在通过成功事务将其应用到数据库中后才反映出来。

// One transaction
try dbQueue.write { db in         // or dbPool.write
    try player1.insert(db)
    try player2.insert(db)
}

// One transaction
try dbQueue.inTransaction { db in // or dbPool.writeInTransaction
    try player1.insert(db)
    try player2.insert(db)
    return .commit
}

// Two transactions
try dbQueue.inDatabase { db in    // or dbPool.writeWithoutTransaction
    try player1.insert(db)
    try player2.insert(db)
}

当您将多个更改应用于数据库时,应将它们组合成一个单个显式事务。然后控制器将一次通知所有更改。

更改通知

FetchedRecordsController的实例通过回调的方式通知控制器获取的记录已被更改。

let controller = try FetchedRecordsController(...)

controller.trackChanges(
    // controller's records are about to change:
    willChange: { controller in ... },
    
    // notification of individual record changes:
    onChange: { (controller, record, change) in ... },
    
    // controller's records have changed:
    didChange: { controller in ... })

try controller.performFetch()

有关视图更新更详细的信息,请参阅实现表格视图更新

所有回调都是可选的。 当您只需获取最新结果时,可以省略didChange参数名称。

controller.trackChanges { controller in
    let newPlayers = controller.fetchedRecords // [Player]
}

⚠️ 警告:记录更改的通知(onChange回调)将在FetchedRecordsController中使用一个高复杂度、高内存消耗的差异算法,因此它不适用于大结果集。100行可能是OK的,但1000行可能就不适用了。如果您的应用程序在使用大列表时遇到问题,请参阅问题263以获取更多信息。

回调以获取记录控制器本身作为参数:使用它来避免内存泄漏。

// BAD: memory leak
controller.trackChanges { _ in
    let newPlayers = controller.fetchedRecords
}

// GOOD
controller.trackChanges { controller in
    let newPlayers = controller.fetchedRecords
}

回调是异步调用的。 有关更多信息,请参阅FetchedRecordsController并发

回调内获取的值可能与控制器的记录不一致。 这是因为在数据库更改之后,在控制器有机会在主线程中调用回调之前,可能会发生其他数据库更改。

为避免不一致,在trackChanges方法中提供fetchAlongside参数,如下所示

controller.trackChanges(
    fetchAlongside: { db in
        // Fetch any extra value, for example the number of fetched records:
        return try Player.fetchCount(db)
    },
    didChange: { (controller, count) in
        // The extra value is the second argument.
        let recordsCount = controller.fetchedRecords.count
        assert(count == recordsCount) // guaranteed
    })

每当获取记录控制器在事务可能已修改跟踪请求后无法查找更改时,将调用错误处理器。尽管请求观察并未停止:未来的事务可能可以成功处理,并且通知的更改将基于最后成功的提取。

controller.trackErrors { (controller, error) in
    print("Missed a transaction because \(error)")
}

修改 Fetch 请求

您可以通过更改已获取记录控制器(fetched records controller)的 fetch 请求或 SQL 查询。

controller.setRequest(Player.order(Column("name")))
controller.setRequest(sql: "SELECT ...", arguments: ...)

当新请求获取不同的一组记录时,变更通知回调会通知最终变化。

☝️ 注意:此行为与 Core Data 的 NSFetchedResultsController 不同,它不通知记录变化,因为 fetch 请求已被替换。

变更回调异步调用。这意味着从主线程修改请求并不会立即触发回调。当您需要立即采取行动,请使用控制器的 performFetch 方法强制控制器立即刷新。在这种情况下,变更回调不会调用。

// Change request on the main thread:
controller.setRequest(Player.order(Column("name")))
// Here callbacks have not been called yet.
// You can cancel them, and refresh records immediately:
try controller.performFetch()

表格和集合视图

FetchedRecordsController 允许您将表格和集合视图与数据库内容保持同步,并向它们添加数据。

为了实现良好动画效果的更新,已获取记录控制器需要识别两个不同结果集之间的相同记录。当记录实现了 TableRecord 协议,它们会根据其主键自动进行比较。

class Player : TableRecord { ... }
let controller = FetchedRecordsController(
    dbQueue,
    request: Player.all())

对于其他类型,已获取记录控制器需要您更明确指定。

let controller = FetchedRecordsController(
    dbQueue,
    request: ...,
    isSameRecord: { (player1, player2) in player1.id == player2.id })

实现表格视图数据源方法

表格视图数据源要求已获取记录控制器提供相关信息。

func numberOfSections(in tableView: UITableView) -> Int {
    return fetchedRecordsController.sections.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return fetchedRecordsController.sections[section].numberOfRecords
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = ...
    let record = fetchedRecordsController.record(at: indexPath)
    // Configure the cell
    return cell
}

☝️ 注意:在其当前状态中,FetchedRecordsController 不支持将表格视图行分组到自定义部分:它生成一个唯一的分区。

实现表格视图更新

当获取记录中的变更应重新加载整个表格视图时,您可以简单地这样告知。

controller.trackChanges { [unowned self] _ in
    self.tableView.reloadData()
}

然而,FetchedRecordsController 可以通知由于某些添加、删除、移动或更新操作导致的控制器获取的记录已更改,并帮助将动画更改应用于 UITableView。

典型表格视图更新

对于动画表格视图更新,使用提供的回调函数来包裹由获取记录控制器提供的操作,如下面的示例所示

// Assume self has a tableView property, and a cell configuration
// method named configure(_:at:).

controller.trackChanges(
    // controller's records are about to change:
    willChange: { [unowned self] _ in
        self.tableView.beginUpdates()
    },
    
    // notification of individual record changes:
    onChange: { [unowned self] (controller, record, change) in
        switch change {
        case .insertion(let indexPath):
            self.tableView.insertRows(at: [indexPath], with: .fade)
            
        case .deletion(let indexPath):
            self.tableView.deleteRows(at: [indexPath], with: .fade)
            
        case .update(let indexPath, _):
            if let cell = self.tableView.cellForRow(at: indexPath) {
                self.configure(cell, at: indexPath)
            }
            
        case .move(let indexPath, let newIndexPath, _):
            self.tableView.deleteRows(at: [indexPath], with: .fade)
            self.tableView.insertRows(at: [newIndexPath], with: .fade)

            // // Alternate technique which actually moves cells around:
            // let cell = self.tableView.cellForRow(at: indexPath)
            // self.tableView.moveRow(at: indexPath, to: newIndexPath)
            // if let cell = cell {
            //     self.configure(cell, at: newIndexPath)
            // }
        }
    },
    
    // controller's records have changed:
    didChange: { [unowned self] _ in
        self.tableView.endUpdates()
    })

⚠️ 警告:记录更改的通知(onChange回调)将在FetchedRecordsController中使用一个高复杂度、高内存消耗的差异算法,因此它不适用于大结果集。100行可能是OK的,但1000行可能就不适用了。如果您的应用程序在使用大列表时遇到问题,请参阅问题263以获取更多信息。

☝️ 注意:我们上面的代码示例使用对表格视图控制器的unowned引用。只要表格视图控制器拥有获取记录控制器并且在主线程(通常是这样的)上解除分配,这个模式就是安全的。在其他情况下,请优先使用弱引用。

💡 提示:参见示例应用程序,以获取使用FetchedRecordsController来动画化表格视图的示例应用程序。

FetchedRecordsController 并发

获取记录控制器在任何线程中使用。

当数据库本身可以在任何线程中读写时,获取记录控制器必须从主线程使用。记录变更也将在主线程上通知

变更回调将被异步调用。这意味着在对主线程所做的更改不立即通知。当您需要立即采取行动时,请使用其performFetch方法强制控制器立即刷新。在这种情况下,更改回调不会调用。

// Change database on the main thread:
try dbQueue.write { db in
    try Player(...).insert(db)
}
// Here callbacks have not been called yet.
// You can cancel them, and refresh records immediately:
try controller.performFetch()

☝️ 注意:当主线程不适合您的需求时,向控制器初始化程序提供一个串行分发队列:那么必须从此队列使用控制器,记录变更也在此队列上通知。

let queue = DispatchQueue()
queue.async {
    let controller = try FetchedRecordsController(..., queue: queue)
    controller.trackChanges { /* in queue */ }
    try controller.performFetch()
}

TransactionObserver 协议

TransactionObserver 协议适用于观察单个数据库变更和事务

protocol TransactionObserver : class {
    /// Notifies a database change:
    /// - event.kind (insert, update, or delete)
    /// - event.tableName
    /// - event.rowID
    ///
    /// For performance reasons, the event is only valid for the duration of
    /// this method call. If you need to keep it longer, store a copy:
    /// event.copy().
    func databaseDidChange(with event: DatabaseEvent)
    
    /// Filters the database changes that should be notified to the
    /// `databaseDidChange(with:)` method.
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool
    
    /// An opportunity to rollback pending changes by throwing an error.
    func databaseWillCommit() throws
    
    /// Database changes have been committed.
    func databaseDidCommit(_ db: Database)
    
    /// Database changes have been rollbacked.
    func databaseDidRollback(_ db: Database)
}

激活事务观察者

要激活事务观察者,将其添加到数据库队列或池中

let observer = MyObserver()
dbQueue.add(transactionObserver: observer)

默认情况下,数据库保存对其事务观察者的弱引用:它们不会被保留,并在释放后停止接收通知。请参阅观察范围以获取更多选项。

数据库更改和事务

事务观察者会通知所有数据库更改:插入、更新和删除。这包括由相关的外键ON DELETEON UPDATE行为以及SQL触发器引发的间接更改。

☝️ 注意:未通知的更改包括内部系统表的更改(例如sqlite_master)、WITHOUT ROWID表的更改,以及由ON CONFLICT REPLACE子句触发的重复行的删除(这个例外可能在SQLite的未来版本中改变)。

通知的更改实际上直到事务提交并被调用databaseDidCommit回调才真正写入磁盘。另一方面,databaseDidRollback确认它们的无效性。

try dbQueue.write { db in
    try db.execute("INSERT ...") // 1. didChange
    try db.execute("UPDATE ...") // 2. didChange
}                                // 3. willCommit, 4. didCommit

try dbQueue.inTransaction { db in
    try db.execute("INSERT ...") // 1. didChange
    try db.execute("UPDATE ...") // 2. didChange
    return .rollback             // 3. didRollback
}

try dbQueue.write { db in
    try db.execute("INSERT ...") // 1. didChange
    throw SomeError()
}                                // 2. didRollback

在事务外部执行的数据库语句不会被淘汰出视野。

try dbQueue.inDatabase { db in
    try db.execute("INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit
    try db.execute("UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit
}

由于保存点而挂起的更改只有在释放保存点之后才会被通知,这确保了通知的事件仅是能够提交的事件。

try dbQueue.inTransaction { db in
    try db.execute("INSERT ...")            // 1. didChange
    
    try db.execute("SAVEPOINT foo")
    try db.execute("UPDATE ...")            // delayed
    try db.execute("UPDATE ...")            // delayed
    try db.execute("RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange
    
    try db.execute("SAVEPOINT foo")
    try db.execute("UPDATE ...")            // not notified
    try db.execute("ROLLBACK TO SAVEPOINT foo")
    
    return .commit                          // 4. willCommit, 5. didCommit
}

databaseWillCommit抛出的最终错误将对应用程序代码可见。

do {
    try dbQueue.inTransaction { db in
        ...
        return .commit           // 1. willCommit (throws), 2. didRollback
    }
} catch {
    // 3. The error thrown by the transaction observer.
}

☝️ 注意:所有回调都是在受保护的调度队列上调用,并与所有数据库更新进行了序列化。

☝️ 注意:数据库更改通知回调databaseDidChange(with:)databaseWillCommit不可直接操作SQLite数据库。这个限制不适用于可以操作其数据库参数的databaseDidCommitdatabaseDidRollback

DatabaseRegionObservationValueObservationFetchedRecordsControllerRxGRDB都基于TransactionObserver协议。

参见TableChangeObserver.swift,它展示了通过NSNotificationCenter通知修改数据库表的交易观察者的示例。

过滤数据库事件

事务观察者可以避免被它们不感兴趣的数据库更改通知。

过滤发生在observes(eventsOfKind:)方法中,它告诉观察者是否希望通知特定类型的更改。例如,以下是一个观察者可以专注于“player”数据库表中发生的更改的示例。

class PlayerObserver: TransactionObserver {
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
        // Only observe changes to the "player" table.
        return eventKind.tableName == "player"
    }
    
    func databaseDidChange(with event: DatabaseEvent) {
        // This method is only called for changes that happen to
        // the "player" table.
    }
}

一般来说,observes(eventsOfKind:)方法可以区分插入、删除和更新,还能够检查即将更改的列。

class PlayerScoreObserver: TransactionObserver {
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
        // Only observe changes to the "score" column of the "player" table.
        switch eventKind {
        case .insert(let tableName):
            return tableName == "player"
        case .delete(let tableName):
            return tableName == "player"
        case .update(let tableName, let columnNames):
            return tableName == "player" && columnNames.contains("score")
        }
    }
}

observes(eventsOfKind:) 方法对于所有事件类型都返回false时,观察者仍然会收到提交和回滚的通知。

class PureTransactionObserver: TransactionObserver {
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
        // Ignore all individual changes
        return false
    }
    
    func databaseDidChange(with event: DatabaseEvent) { /* Never called */ }
    func databaseWillCommit() throws { /* Called before commit */ }
    func databaseDidRollback(_ db: Database) { /* Called on rollback */ }
    func databaseDidCommit(_ db: Database) { /* Called on commit */ }
}

有关事件过滤的更多信息,请参阅 DatabaseRegion

观察范围

您可以指定观察者接收数据库更改和事务通知的时间长度。

remove(transactionObserver:) 方法明确停止通知,可以在任何时间进行。

// From a database queue or pool:
dbQueue.remove(transactionObserver: observer)

// From a database connection:
dbQueue.inDatabase { db in
    db.remove(transactionObserver: observer)
}

☝️ 注意:删除事务观察者保证了它不会收到任何未来的事务通知 - 没有竞态条件。然而,这并不停止之前交易的最后处理。例如,remove(transactionObserver:) 不是停止由 ValueObservation 启动的观察者的正确方式。在这种情况下,正确的方式是释放观察者。

或者,使用 add(transactionObserver:extent:) 方法中的 extent 参数。

let observer = MyObserver()

// On a database queue or pool:
dbQueue.add(transactionObserver: observer) // default extent
dbQueue.add(transactionObserver: observer, extent: .observerLifetime)
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)

// On a database connection:
dbQueue.inDatabase { db in
    db.add(transactionObserver: ...)
}
  • 默认范围是 .observerLifetime:数据库持有观察者的弱引用,并在观察者释放时自动结束观察。同时,观察者会通知所有更改和事务。

  • .nextTransaction激活观察者,直到当前或下一个事务完成。数据库在其 databaseDidCommitdatabaseDidRollback 方法最终被调用之前继续保持对观察者的强引用。从那时起,观察者将不再收到任何其他通知。

  • .databaseLifetime 设置数据库在数据库连接关闭之前保持并通知观察者。

最后,观察者可以忽略当前事务结束前的所有数据库更改。

class PlayerObserver: TransactionObserver {
    var playerTableWasModified = false
    
    func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
        return eventKind.tableName == "player"
    }
    
    func databaseDidChange(with event: DatabaseEvent) {
        playerTableWasModified = true
        
        // It is pointless to keep on tracking further changes:
        stopObservingDatabaseChangesUntilNextTransaction()
    }
}

stopObservingDatabaseChangesUntilNextTransaction() 后,databaseDidChange(with:) 方法将通知当前事务剩余时间内的任何更改。这有助于 GRDB 优化数据库观察。

数据库区域

DatabaseRegion 是一种类型,它有助于监听数据库结果的变化。.

一个请求知道哪些数据库修改可能影响其结果。通过 DatabaseRegion 方式,它可以与 事务观察者 通信此信息。

DatabaseRegion 为例如 ValueObservation 和 DatabaseRegionObservation 提供了支持。

一个区域通知潜在的变化,而不是实际的变化。只有当有一个语句实际上通过插入、更新或删除行修改了追踪的表和列时,才会发出更改通知。

例如,如果您观察Player.select(max(Column("score")))的区域,那么您将收到针对player表的score列的任何更改通知(更新、插入和删除),即使它们没有修改最高分数的值。然而,您不会收到对其他数据库表上执行更改或更新玩家表其他列的任何通知。

类似地,观察Country.filter(key: "FR")的区域将通知对该整个country表的任何更改。这是因为SQLite仅通知已更改行的数值行ID,我们无法检查是否更改的是行“FR”,还是其他行。这种限制不适用于主键是行ID的表:Player.filter(key: 42)将只通知对ID为42的行执行的更改。

有关更多详细信息,请参阅参考

DatabaseRegionConvertible 协议

DatabaseRegionConvertible 是一个协议,用于可以将类型转换为 DatabaseRegion

protocol DatabaseRegionConvertible {
    func databaseRegion(_ db: Database) throws -> DatabaseRegion
}

所有 请求 都采用了此协议,并且这允许它们使用 DatabaseRegionObservationValueObservation 进行观察。

当您想在专用类型中封装您的复杂请求并仍然从观察API中受益时,请使用此协议。有关更多信息,请参阅DatabaseRegionConvertible Observation

对 SQLite 预更新钩子的支持

可以通过自定义 SQLite 构建版本激活SQLite "preupdate hooks"。在这种情况下,TransactionObserverType 获得了额外的回调,允许您观察事务中修改的行的单个列值

protocol TransactionObserverType : class {
    #if SQLITE_ENABLE_PREUPDATE_HOOK
    /// Notifies before a database change (insert, update, or delete)
    /// with change information (initial / final values for the row's
    /// columns).
    ///
    /// The event is only valid for the duration of this method call. If you
    /// need to keep it longer, store a copy: event.copy().
    func databaseWillChange(with event: DatabasePreUpdateEvent)
    #endif
}

加密

GRDB 可以使用 SQLCipher v3.4.2 对您的数据库进行加密。

您可以使用 CocoaPods(版本 1.2 或更高),并在您的 Podfile 中指定

use_frameworks!
pod 'GRDBCipher'

或者,手动安装 GRDB 和 SQLCipher

  1. 克隆 GRDB git 仓库,检出最新的标记版本,并下载 SQLCipher 源代码

    cd [GRDB directory]
    git checkout v3.7.0
    git submodule update --init SQLCipher/src
  2. GRDBCipher.xcodeproj 项目嵌入到您自己的项目中。

  3. GRDBCipherOSXGRDBCipheriOS 目标添加到应用程序目标的文本选项卡中“目标依赖关系”部分的“构建阶段”部分。

  4. GRDBCipher.framework 从靶场平台添加到您目标的文本选项卡中“通用”部分的“嵌入二进制文件”部分。

您通过提供密码短语来创建和打开一个加密数据库,具体操作是通过您的数据库连接

import GRDBCipher

var configuration = Configuration()
configuration.passphrase = "secret"
let dbQueue = try DatabaseQueue(path: "...", configuration: configuration)

您还可以更改已加密数据库的密码短语

try dbQueue.change(passphrase: "newSecret")

不过,提供密码短语不会加密一个已经存在的明文数据库。SQLCipher 无法完成此操作,您会收到错误信息:SQLite错误26:文件已加密或不是数据库

要加密现有的明文数据库,创建一个新的空加密数据库,并将明文数据库的内容复制到其中。SQLCipher 已经提供了操作该技术的文档。GRDB会给它

// The clear-text database
let clearDBQueue = try DatabaseQueue(path: "/path/to/clear.db")

// The encrypted database, at some distinct location:
var configuration = Configuration()
configuration.passphrase = "secret"
let encryptedDBQueue = try DatabaseQueue(path: "/path/to/encrypted.db", configuration: config)

try clearDBQueue.inDatabase { db in
    try db.execute("ATTACH DATABASE ? AS encrypted KEY ?", arguments: [encryptedDBQueue.path, "secret"])
    try db.execute("SELECT sqlcipher_export('encrypted')")
    try db.execute("DETACH DATABASE encrypted")
}

// Now the copy is done, and the clear-text database can be deleted.

SQLCipher的高级配置选项

您可以为配置SQLCipher设置两个高级配置选项,以控制加密和密钥生成过程中的各个方面。

var configuration = Configuration()
configuration.passphrase = "secret"
configuration.cipherPageSize = .pageSize4K
configuration.kdfIterations = 128000
let dbQueue = try DatabaseQueue(path: "...", configuration: configuration)

cipherPageSize

cipherPageSize用于调整加密数据库的页面大小(这与SQLCipher的PRAGMA cipher_page_size配置选项相对应)。增加页面大小可以显著提高访问大量页面的一些查询的性能。

当前GRDB.swift版本中使用的SQLCipher默认cipherPageSize.pageSize1K

☝️ 注意:每次打开数据库文件时都必须提供相同的cipherPageSize;如果没有设置正确的cipherPageSize而尝试访问数据库,则会抛出SQLite错误26:文件已加密或不是数据库错误。

kdfIterations

kdfIterations值用于调整PBKDF2密钥派生运行到从提供的passphrase中派生密钥的迭代次数(这与SQLCipher的PRAGMA kdf_iter配置选项相对应)。

当前GRDB.swift版本中使用的SQLCipher默认kdfIterations64000。不建议从默认迭代次数中降低迭代次数。

☝️ 注意:每次打开数据库文件时都必须提供相同的 kdfIterations;如果尝试访问数据库而未设置正确的 kdfIterations,则会导致抛出 SQLite 错误 26:文件已加密或不是数据库 错误。

备份

您可以将数据库备份(复制)到另一个数据库中。

例如,备份可以帮助您在实现 NSDocument 子类时,将内存数据库复制到数据库文件或从中复制。

let source: DatabaseQueue = ...      // or DatabasePool
let destination: DatabaseQueue = ... // or DatabasePool
try source.backup(to: destination)

backup 方法会阻塞当前线程,直到目标数据库包含与源数据库相同的数据库内容。

当源是 数据库池 时,备份期间可以发生并发写入。这些写入可能会也可能不会反映在备份中,但不会引发任何错误。

避免 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("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(
        "UPDATE students SET name = ? WHERE id = ?",
        arguments: [name, id])
    
    // Just as good
    try db.execute(
        "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, key: id) {
        student.name = name
        try student.update(db)
    }
}

错误处理

GRDB 可以抛出 DatabaseErrorPersistenceError,或者通过 致命错误 崩溃您的程序。

考虑到本地数据库不是从远程服务器加载的 JSON,GRDB 侧重于 可信数据库。处理 不受信任的数据 需要额外的注意。

DatabaseError

DatabaseError 在 SQLite 错误时抛出

do {
    try db.execute(
        "INSERT INTO pet (masterId, name) VALUES (?, ?)",
        arguments: [1, "Bobby"])
} 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
    
    // Full error description:
    // "SQLite error 19 with statement `INSERT INTO pet (masterId, name)
    //  VALUES (?, ?)` arguments [1, "Bobby"]: FOREIGN KEY constraint failed""
    error.description
}

SQLite 使用代码来区分各种错误

do {
    try ...
} catch let error as DatabaseError where error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY {
    // foreign key constraint error
} catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT {
    // any other constraint error
} catch let error as DatabaseError {
    // any other database error
}

在上面的示例中,error.extendedResultCode 是一种精确的 扩展结果代码,而 error.resultCode 则是一种不那么精确的 主要结果代码。扩展结果代码是主要结果代码的细化,例如 SQLITE_CONSTRAINT_FOREIGNKEY 相对于 SQLITE_CONSTRAINT。更多关于 SQLite 结果代码的信息,请参阅 SQLite 结果代码

为了方便起见,扩展结果代码与它们的原始结果代码在 switch 语句中相匹配

do {
    try ...
} catch let error as DatabaseError {
    switch error.extendedResultCode {
    case ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY:
        // foreign key constraint error
    case ResultCode.SQLITE_CONSTRAINT:
        // any other constraint error
    default:
        // any other database error
    }
}

⚠️ 警告:SQLite 在其各个版本中逐渐引入了扩展结果代码。例如,在 iOS 8.1 上尚未引入 SQLITE_CONSTRAINT_FOREIGNKEY。不幸的是,SQLite 发布说明 并不是很清楚这一点:请谨慎处理扩展结果代码。

持久性错误

持久性错误持久记录协议 抛出,仅在这种情况下:当 update 方法找不到任何更新行时。

do {
    try player.update(db)
} catch PersistenceError.recordNotFound {
    // There was nothing to update
}

致命错误

致命错误通知程序或数据库需要更改。

它们揭露程序员错误、假设错误,并防止滥用。以下是一些例子:

  • 代码请求一个非可选值,但数据库包含 NULL

    // fatal error: could not convert NULL to String.
    let name: String = row["name"]

    解决方案:修复数据库内容,使用 NOT NULL 约束 或加载一个可选值。

    let name: String? = row["name"]
  • 从数据库值转换为 Swift 类型失败

    // fatal error: could not convert "Mom’s birthday" to Date.
    let date: Date = row["date"]
    
    // fatal error: could not convert "" to URL.
    let url: URL = row["url"]

    解决方案:修复数据库内容,或使用 DatabaseValue 处理所有可能的案例。

    let dbValue: DatabaseValue = row["date"]
    if dbValue.isNull {
        // Handle NULL
    } else if let date = Date.fromDatabaseValue(dbValue) {
        // Handle valid date
    } else {
        // Handle invalid date
    }
  • 数据库无法保证代码按指定执行

    // fatal error: table player has no unique index on column email
    try Player.deleteOne(db, key: ["email": "[email protected]"])

    解决方案:向玩家.email 列添加唯一索引,或使用 deleteAll 方法清楚地指出您可能删除多行。

    try Player.filter(Column("email") == "[email protected]").deleteAll(db)
  • 数据库连接是不可重入的

    // fatal error: Database methods are not reentrant.
    dbQueue.write { db in
        dbQueue.write { db in
            ...
        }
    }

    解决方案:避免重入,而是传递一个数据库连接。

如何处理未经验证的输入

让我们看看下面的代码

let sql = "SELECT ..."

// Some untrusted arguments for the query
let arguments: [String: Any] = ...
let rows = try Row.fetchCursor(db, sql, arguments: StatementArguments(arguments))

while let row = try rows.next() {
    // Some untrusted database value:
    let date: Date? = row[0]
}

它有两个抛出致命错误的机会

  • 未经验证的参数:字典可能包含不符合 DatabaseValueConvertible 协议 的值,或者可能缺少语句所需的键。
  • 未经验证的数据库内容:行的非空值可能无法转换成日期。

在这种情况下,您仍然可以通过在 GRDB API 中向下处理每个失败点来避免致命错误。

// Untrusted arguments
if let arguments = StatementArguments(arguments) {
    let statement = try db.makeSelectStatement(sql)
    try statement.validate(arguments: arguments)
    statement.unsafeSetArguments(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
        }
    }
}

请参阅预定义语句数据库值以获取更多信息。

错误日志

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, "SELECT UPPER('Jérôme')")

GRDB 通过调用 Swift 内置字符串函数 capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercased 扩展 SQLite 的 SQL 函数。

// "JÉRÔME"
let uppercased = DatabaseFunction.uppercase
try String.fetchOne(db, "SELECT \(uppercased.name)('Jérôme')")

这些支持 Unicode 的字符串函数在查询接口中同样可用。

Player.select(nameColumn.uppercased)

字符串比较

SQLite 在许多情况下比较字符串:当你根据字符串列排序行时,或者当你使用比较运算符(例如 =<=)时。

比较结果来自一个 排序函数排序规则。SQLite 随附了三个内置的排序规则,它们不支持 Unicode:二进制、不区分大小写和右截断

GRDB 额外提供五个排序规则,它们基于标准 Swift 字符串比较函数和运算符实现支持 Unicode 的比较。

  • code`unicodeCompare`(使用内置的 <=== Swift 运算符)
  • caseInsensitiveCompare
  • localizedCaseInsensitiveCompare
  • localizedCompare
  • localizedStandardCompare

可以为一个表列应用排序规则。涉及此列的所有比较都将自动触发比较函数

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 需要主机应用程序提供除了二进制、不区分大小写和删除尾部空白之外的所有排序规则的定义。当一个数据文件需要共享或迁移到另一个SQLite库或平台(例如应用程序的Android版本)时,请确保您提供了兼容的排序规则。

如果您无法或不希望定义一个列的比较行为(见上述警告),您仍然可以在SQL请求和查询接口中使用显式的排序规则

let collation = DatabaseCollation.localizedCaseInsensitiveCompare
let players = try Player.fetchAll(db,
    "SELECT * FROM player ORDER BY name COLLATE \(collation.name))")
let players = try Player.order(nameColumn.collating(collation)).fetchAll(db)

您也可以定义自己的排序规则:

let collation = DatabaseCollation("customCollation") { (lhs, rhs) -> NSComparisonResult in
    // return the comparison of lhs and rhs strings.
}
dbQueue.add(collation: collation) // Or dbPool.add(collation: ...)

内存管理

SQLite和GRDB都使用辅助内存来提高它们的性能。

您可以使用releaseMemory方法来回收此内存

// Release as much memory as possible.
dbQueue.releaseMemory()
dbPool.releaseMemory()

此方法会阻塞当前线程,直到所有当前数据库访问都完成,并且已收集内存。

在iOS上的内存管理

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

数据库队列可以在应用程序接收到内存警告或进入后台时调用releaseMemory方法。请在创建队列或池实例后调用setupMemoryManagement方法。

let dbQueue = try DatabaseQueue(...)
dbQueue.setupMemoryManagement(in: UIApplication.shared)

数据保护

数据保护可以使您加密文件,使其在设备解锁前不可用。

您可以为所有由应用程序创建的文件全局启用数据保护(全局开启)

您也可以通过配置其包含的目录来显式保护数据库。这将不仅保护数据库文件,而且还会保护SQLite创建的所有临时文件(包括数据库池创建的持久.shm.wal文件)。

例如,要显式使用完整保护

// Paths
let fileManager = FileManager.default
let directoryURL = try fileManager
    .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    .appendingPathComponent("database", isDirectory: true)
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")

// Create directory if needed
var isDirectory: ObjCBool = false
if !fileManager.fileExists(atPath: directoryURL.path, isDirectory: &isDirectory) {
    try fileManager.createDirectory(atPath: directoryURL.path, withIntermediateDirectories: false)
} else if !isDirectory.boolValue {
    throw NSError(domain: NSCocoaErrorDomain, code: NSFileWriteFileExistsError, userInfo: nil)
}

// Enable data protection
try fileManager.setAttributes([.protectionKey : FileProtectionType.complete], ofItemAtPath: directoryURL.path)

// Open database
let dbQueue = try DatabaseQueue(path: databaseURL.path)

当数据库受到保护时,在锁定设备上运行的背景应用程序无法从其中读取或写入。相反,它将获取数据库错误,错误代码为SQLITE_IOERR(10)“磁盘I/O错误”,或SQLITE_AUTH(23)“未授权”。

您可以捕获这些错误并等待UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:)UIApplicationProtectedDataDidBecomeAvailable通知,以重新尝试失败的数据库操作。

并发

保障和规则

GRDB 提供三种并发模式

  • 数据库队列打开单个数据库连接,并序列化所有数据库访问。
  • 数据库池管理一组数据库连接,并允许并发读取和写入。
  • 数据库快照在不变的数据库内容上打开单个只读数据库连接,并(目前)序列化所有数据库访问

始终保证应用程序安全:无论您选择哪种并发模式,只要您遵循三条规则,GRDB 都会为您提供相同的保障。

  • :bowtie: 保障1:总是序列化写操作。在每一个时刻,只有一个线程在写入数据库。

    数据库写操作总是在名为“写入保护调度队列”的唯一序列调度队列中发生。

  • :bowtie: 保障2:读操作总是隔离的。这意味着它们确保能够看到数据库的最后提交状态的不变视图,并且可以在不担心潜在并发写操作乱序应用程序逻辑的情况下执行后续获取操作。

    try dbPool.read { db in // or dbQueue.read
        // Guaranteed to be equal
        let count1 = try Player.fetchCount(db)
        let count2 = try Player.fetchCount(db)
    }

    数据库队列中,读取和写入发生在相同的保护调度队列中:隔离仅仅是数据库访问序列化的结果

    数据库池快照都使用了SQLite的WAL模式(请参阅SQLite中的隔离)实现的“快照隔离”。

  • :bowtie: 保证3:请求不会失败,除非是违反数据库约束、程序错误,或者非常低级的问题,如磁盘错误或不可读的数据库文件。GRDB 保证了 SQLite 的正确使用,并特别避免了锁定错误和其他 SQLite 的滥用。

只要遵循三条规则,这些保证就成立:

  • ☝️ 规则1:确保每个数据库文件都连接一个唯一的 DatabaseQueue 或 DatabasePool 实例。

    这意味着每次访问数据库时都打开一个新连接是一个坏主意。相反,应该共享一个连接。

    请参考 示例应用程序 中的示例,该示例创建了一个在整个应用程序中可用的单个数据库队列。

    如果在同一数据库中存在多个实例的数据库队列或池进行写入,多线程应用程序最终会遇到“数据库已锁定”错误。请参阅 处理外部连接

    // SAFE CONCURRENCY
    func fetchCurrentUser(_ db: Database) throws -> User? {
        return try User.fetchOne(db)
    }
    // dbQueue is a singleton defined somewhere in your app
    let user = try dbQueue.read { db in // or dbPool.read
        try fetchCurrentUser(db)
    }
    
    // UNSAFE CONCURRENCY
    // This method fails when some other thread is currently writing into
    // the database.
    func currentUser() throws -> User? {
        let dbQueue = try DatabaseQueue(...)
        return try dbQueue.read { db in
            try User.fetchOne(db)
        }
    }
    let user = try currentUser()
  • ☝️ 规则2:将相关的语句组合在一个 DatabaseQueue 或 DatabasePool 数据库访问方法的调用中(或使用 快照)。

    数据库访问方法将隔离您的相关语句组,对其最终的数据库更新与其他线程保持隔离,并保证对数据库的一致性视图。这种隔离仅在方法的自闭型参数的内部保证。连续两次调用 保证隔离。

    // SAFE CONCURRENCY
    try dbPool.read { db in  // or dbQueue.read
        // Guaranteed to be equal:
        let count1 = try Place.fetchCount(db)
        let count2 = try Place.fetchCount(db)
    }
    
    // UNSAFE CONCURRENCY
    // Those two values may be different because some other thread may have
    // modified the database between the two blocks:
    let count1 = try dbPool.read { db in try Place.fetchCount(db) }
    let count2 = try dbPool.read { db in try Place.fetchCount(db) }

    同理,当您获取一些依赖于数据库更新的值时,请将这些值分组:

    // SAFE CONCURRENCY
    try dbPool.write { db in
        // The count is guaranteed to be non-zero
        try Place(...).insert(db)
        let count = try Place.fetchCount(db)
    }
    
    // UNSAFE CONCURRENCY
    // The count may be zero because some other thread may have performed
    // a deletion between the two blocks:
    try dbPool.write { db in try Place(...).insert(db) }
    let count = try dbPool.read { db in try Place.fetchCount(db) }

    关于这个最后的例子,如果您关注额外的性能,请参阅 高级 DatabasePool

  • ☝️ 规则3:当您执行多个暂时使数据库处于不一致状态的修改时,确保这些修改分组在一个 事务 中。

    // SAFE CONCURRENCY
    try dbPool.write { db in               // or dbQueue.write
        try Credit(destinationAccout, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
    }
    
    // SAFE CONCURRENCY
    try dbPool.writeInTransaction { db in  // or dbQueue.inTransaction
        try Credit(destinationAccout, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
        return .commit
    }
    
    // UNSAFE CONCURRENCY
    try dbPool.writeWithoutTransaction { db in
        try Credit(destinationAccout, amount).insert(db)
        // <- Concurrent dbPool.read sees a partial db update here
        try Debit(sourceAccount, amount).insert(db)
    }

    没有事务,DatabasePool.read { ... } 可能会看到第一条语句,但看不到第二条,并访问一个账户余额不为零的数据库。这是一个高度易出bug的情况。

    所以,请使用 事务 来保证应用程序线程之间的数据库一致性:这就是它们被制作出来的原因。

数据库队列和池之间的区别

尽管 保证和规则数据库队列 中是共有的,但这两个数据库访问器的行为并不相同。

数据库队列 序列化所有数据库访问,包括读取和写入。在任何时候都只有一个线程使用数据库。在下图中,我们可以看到三个线程如何随着时间的推移查看数据库:

DatabaseQueueScheduling

数据库池 也序列化所有写入。但它们允许并发读取和写入,并隔离读取,以便它们不会看到其他线程进行的更改。这会呈现出一个非常不同的画面:

DatabasePoolScheduling

看看,在数据库池中,两个读取可以同时看到不同的数据库状态。

有关更多信息关于数据库池,您可以获取有关 SQLite WAL 模式 的信息和 快照隔离 的信息。如果您关注数据库变更的自动通知,请参阅 数据库观察

高级数据库池

数据库池非常并发,因为所有读取都可以并行运行,甚至在写操作期间也可以运行。但写操作仍然是串行的:在任何给定的时间点,只有一个线程正在向数据库写入。

当您的应用程序修改数据库,然后读取依赖于这些修改的一些值时,您可能希望避免在必要时长时间锁定写者队列。

try dbPool.write { db in
    // Increment the number of players
    try Player(...).insert(db)
    
    // Read the number of players. The writer queue is still locked :-(
    let count = try Player.fetchCount(db)
}

一种错误的解决方案是将写操作和读操作链式调用,如下所示。不要这样做,因为另一个线程可能在之间修改数据库,并使读操作不可靠。

// WRONG
try dbPool.write { db in
    // Increment the number of players
    try Player(...).insert(db)
}
// <- other threads can write in the database here
try dbPool.read { db in
    // Read some random value :-(
    let count = try Player.fetchCount(db)
}

正确的解决方案是调用concurrentRead方法,必须在写块中调用,在事务之外。

concurrentRead返回一个未来值,您可以在任何调度队列上使用wait()方法进行消费。

// CORRECT
let futureCount: Future<Int> = try dbPool.writeWithoutTransaction { db in
    // increment the number of players
    try Player(...).insert(db)
    
    // <- not in a transaction here
    let futureCount = dbPool.concurrentRead { db
        return try Player.fetchCount(db)
    }
    return futureCount
}
// <- The writer queue has been unlocked :-)

// Wait for the player count
let count: Int = try futureCount.wait()

concurrentRead会阻塞直到它可以保证其闭包参数对数据库的最后提交状态的隔离访问。然后它将异步执行闭包。

闭包可以与在concurrentRead后执行的最终更新并发运行:这些更新在闭包内部不可见。在下面的示例中,即使它与其他玩家删除并发进行,保证了玩家数量的非零性。

try dbPool.writeWithoutTransaction { db in
    // Increment the number of players
    try Player(...).insert(db)
    
    let futureCount = dbPool.concurrentRead { db
        // Guaranteed to be non-zero
        return try Player.fetchCount(db)
    }
    
    try Player.deleteAll(db)
}

事务观察者在他们的databaseDidCommit方法中也可以使用concurrentRead,以便在不阻塞想要写入数据库的其他线程的情况下处理数据库更改。

数据库快照

数据库快照看到的是数据库在创建时的内容,始终如一。

"不变"意味着在快照的整个生命周期中,从不看到对数据库的任何修改。然而,它并不阻止数据库更新。这种"魔法"是通过SQLite的WAL模式实现的(见SQLite的隔离)。

您可以从一个数据库池中创建快照。

let snapshot = try dbPool.makeSnapshot()

可以创建任意数量的快照,无论池中最大读取者数量是多少。当快照被卸载时,快照数据库连接才会关闭。

快照可以从任何线程使用。read方法是同步的,在数据库语句执行之前会阻塞当前线程。

// Read values:
try snapshot.read { db in
    let players = try Player.fetchAll(db)
    let playerCount = try Player.fetchCount(db)
}

// Extract a value from the database:
let playerCount = try snapshot.read { db in
    try Player.fetchCount(db)
}

当您想要控制快照看到的最新提交更改时,从池的写保护调度队列外部创建它,在选择事务之外。

let snapshot1 = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshot in
    try db.inTransaction {
        // delete all players
        try Player.deleteAll()
        return .commit
    }
    
    // <- not in a transaction here
    return dbPool.makeSnapshot()
}
// <- Other threads may modify the database here
let snapshot2 = try dbPool.makeSnapshot()

try snapshot1.read { db in
    // Guaranteed to be zero
    try Player.fetchCount(db)
}

try snapshot2.read { db in
    // Could be anything
    try Player.fetchCount(db)
}

☝️ 注意:快照当前会序列化所有数据库访问。将来,快照可能会允许并发读取。

数据写入器与数据库读取器协议

数据库队列和数据库池都采用了数据库读取器数据库写入器协议。数据库快照只采用数据库读取器。

这些协议提供了一致的API,使您可以编写针对所有并发模式的泛型代码。例如

只有五种类型采用了这些协议:数据库队列,数据库池,数据库快照,AnyDatabaseReader,和 AnyDatabaseWriter。不支持扩展这个集合:任何未来的GRDB版本都可能破坏您的自定义写入器和读取器,而不会有任何通知。

数据库读取器和数据库写入器提供了最少的共同保证:它们不抹去队列,池和快照之间的差异。例如,参见数据库队列和池之间的差异

然而,您可以通过给应用程序的某些部分一个数据库读取器来防止它们在数据库中写入

// This class can read in the database, but can't write into it.
class MyReadOnlyComponent {
    let reader: DatabaseReader
    
    init(reader: DatabaseReader) {
        self.reader = reader
    }
}

let dbQueue: DatabaseQueue = ...
let component = MyReadOnlyComponent(reader: dbQueue)

☝️ 注意:数据库读取器不是防止应用程序组件在数据库中写入的安全方式,因为写入访问只是转变一下

if let dbQueue = reader as? DatabaseQueue {
    try dbQueue.write { ... }
}

不安全的并发API

数据库队列、池、快照以及它们共同的协议DatabaseReaderDatabaseWriter提供了不安全的API。不安全的API取消了并发保证,并允许先进而不安全的模式。

  • unsafeRead

    unsafeRead方法是同步的,会阻塞当前线程,直到在受保护的调度队列中执行数据库语句。GRDB仅提供足够的数据库连接以进行读取。

    在数据库池上使用时,读取不再隔离

    dbPool.unsafeRead { db in
        // Those two values may be different because some other thread
        // may have inserted or deleted a player between the two requests:
        let count1 = try Player.fetchCount(db)
        let count2 = try Player.fetchCount(db)
    }

    在数据库队列上使用时,闭包参数允许在数据库中写入。

  • unsafeReentrantRead

    unsafeReentrantRead的行为与unsafeRead(见上文)相同,并允许重入调用

    dbPool.read { db1 in
        // No "Database methods are not reentrant" fatal error:
        dbPool.unsafeReentrantRead { db2 in
            dbPool.unsafeReentrantRead { db3 in
                ...
            }
        }
    }

    重入式数据库访问使得很容易打破第二个安全规则,该规则指出:“将相关语句组合在单个DatabaseQueue或DatabasePool数据库访问方法调用中。”使用重入方法几乎肯定是错误的应用程序架构需要重构的迹象。

    重入方法的单个有效用例是当您无法控制数据库访问调度时。

  • unsafeReentrantWrite

    unsafeReentrantWrite方法是同步的,会阻塞当前线程,直到在受保护的调度队列中执行数据库语句。写入是序列化的:最终并发数据库更新将延迟到块执行完成。

    允许重入调用

    dbQueue.write { db1 in
        // No "Database methods are not reentrant" fatal error:
        dbQueue.unsafeReentrantWrite { db2 in
            dbQueue.unsafeReentrantWrite { db3 in
                ...
            }
        }
    }

    重入式数据库访问使得很容易打破第二个安全规则,该规则指出:“将相关语句组合在单个DatabaseQueue或DatabasePool数据库访问方法调用中。”使用重入方法几乎肯定是错误的应用程序架构需要重构的迹象。

    重入方法的单个有效用例是当您无法控制数据库访问调度时。

处理外部连接

GRDB的第一条规则是

  • 规则1:确保每个数据库文件都有DatabaseQueue或DatabasePool的唯一实例。

这意味着GRDB并不专注于处理外部连接。在某些外部连接修改数据库后,GRDB的保证可能成立,也可能不成立。

如果您确实需要多个连接,那么

性能

GRDB是一个相对较快的库,并且可以提供相当有效的SQLite访问。请参阅比较Swift SQLite库性能以获取概述。

以下是在关注性能时会发现的一般建议

  • 着重
  • 了解您的平台
  • 使用事务
  • 避免无谓的工作
  • 了解SQL的优缺点
  • 避免字符串与字典

性能提示:着重

直到您运行基准测试工具,您不知道程序哪个部分需要改进。

不要做任何假设,避免过早优化代码,并使用Instruments

性能提示:了解您的平台

如果您的应用程序处理一个巨大的JSON文件,并从主线程直接在数据库中插入数千行,它很可能变得无响应,并造成次质的用户体验。

如果尚未做,请阅读并发编程指南,了解如何在不停阻应用的情况下进行重量级计算。

大多数GRBD API是同步的。将它们生成到并行队列上就像

DispatchQueue.global().async { 
    dbQueue.write { db in
        // Perform database work
    }
    DispatchQueue.main.async { 
        // update your user interface
    }
}

性能提示:使用事务

在事务中执行对数据库的多项更新要快得多。这是因为事务允许SQLite推迟将更改写入磁盘,直到最终提交。

// Inefficient
try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
    for player in players {
        try player.insert(db)
    }
}

// Efficient
try dbQueue.write { db in      // or dbPool.write
    for player in players {
        try player.insert(db)
    }
}

// Efficient
try dbQueue.inTransaction { db in // or dbPool.writeInTransaction
    for player in players {
        try player.insert(db)
    }
    return .commit
}

性能提示:不要做无用功

显然,没有代码比任何代码更快的。

不要获取你不需要的列

// SELECT * FROM player
try Player.fetchAll(db)

// SELECT id, name FROM player
try Player.select(idColumn, nameColumn).fetchAll(db)

如果你的Player类型不能没有其他列构建(它对其他列有非可选属性),定义并使用不同的类型

有关更多信息,请参阅由请求选择的列

不要获取你不需要的行

当你需要单个值时使用fetchOne,否则在数据库级别限制你的查询

// Wrong way: this code may discard hundreds of useless database rows
let players = try Player.order(scoreColumn.desc).fetchAll(db)
let hallOfFame = players.prefix(5)

// Better way
let hallOfFame = try Player.order(scoreColumn.desc).limit(5).fetchAll(db)

除非必要,不要复制值

特别是:由fetchAll方法返回的数组和由fetchCursor返回的游标不是相同的

fetchAll将所有值从数据库复制到内存中,而当fetchCursor迭代由SQLite生成的数据库结果时,利用SQLite的效率。

如果你需要保留数组以供以后使用(例如,在主线程中迭代其内容),则应仅加载数组,否则请使用fetchCursor

有关fetchAllfetchCursor的更多信息,请参阅获取方法。另请参阅Row.dataNoCopy方法。

除非必要,不要更新行

UPDATE语句代价很大:SQLite必须查找更新的行,更新值,并将更改写入磁盘。

当重写的值与现有的值相同,最好避免执行UPDATE语句:

if player.hasDatabaseChanges {
    try player.update(db)
}

有关更多信息,请参阅记录比较

性能提示:了解SQL的优缺点

考虑一个简单的用例:您的商店应用程序需要显示作者列表以及可用的书籍数量

  • J. M. 科依兹(6本)
  • 赫尔曼·梅尔维尔(1本)
  • 艾丽丝·孟若(3本)
  • 金·斯坦利·罗宾逊(7本)
  • 奥利弗·萨克斯(4本)

以下代码效率低下。它是一个 N+1 问题 的例子,因为它执行了一次查询来加载作者,然后针对每个作者执行了N次查询,查询次数等于作者的数量。当作者数量增加时,这会使效率变得非常低下

// SELECT * FROM author
let authors = try Author.fetchAll(db)
for author in authors {
    // SELECT COUNT(*) FROM book WHERE authorId = ...
    author.bookCount = try Book.filter(authorIdColumn == author.id).fetchCount(db)
}

相反,执行 单个查询

let sql = """
    SELECT author.*, COUNT(book.id) AS bookCount
    FROM author
    LEFT JOIN book ON book.authorId = author.id
    GROUP BY author.id
    """
let authors = try Author.fetchAll(db, sql)

在上面的示例中,考虑扩展您的 Author,添加一个额外的 bookCount 属性,或者定义并使用不同的类型。

一般来说,在数据库表上定义索引,并使用 SQLite 的有效查询计划。

性能提示:避免字符串和字典

在寻找最佳性能时,最好避免使用 Swift 的 String 和 Dictionary 类型。

现在 GRDB 为您的方便起见,也使用字符串和字典。

class Player : Record {
    var id: Int64?
    var name: String
    var email: String
    
    required init(_ row: Row) {
        id = row["id"]       // String
        name = row["name"]   // String
        email = row["email"] // String
        super.init()
    }
    
    override func encode(to container: inout PersistenceContainer) {
        container["id"] = id              // String
        container["name"] = name          // String
        container["email"] = email        // String
    }
}

当便利性损害性能时,您仍然可以使用记录,但最好避免他们的基于字符串和字典的方法。

例如,在获取值时,优先选择按索引加载列。

// Strings & dictionaries
let players = try Player.fetchAll(db)

// Column indexes
// SELECT id, name, email FROM player
let request = Player.select(idColumn, nameColumn, emailColumn)
let rows = try Row.fetchCursor(db, request)
while let row = try rows.next() {
    let id: Int64 = row[0]
    let name: String = row[1]
    let email: String = row[2]
    let player = Player(id: id, name: name, email: email)
    ...
}

在插入值时,使用可重用的 预处理语句,并使用 数组 设置语句值。

// Strings & dictionaries
for player in players {
    try player.insert(db)
}

// Prepared statement
let insertStatement = db.prepareStatement("INSERT INTO player (name, email) VALUES (?, ?)")
for player in players {
    // Only use the unsafe arguments setter if you are sure that you provide
    // all statement arguments. A mistake can store unexpected values in
    // the database.
    insertStatement.unsafeSetArguments([player.name, player.email])
    try insertStatement.execute()
}

常见问题解答(FAQ)

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

该问题假设您的应用程序必须从头开始创建新数据库。如果您的应用程序需要打开嵌入在应用程序资源中的现有数据库,请参阅 我如何打开作为应用程序资源的数据库存储的数据库?

数据库必须存储在一个有效位置,可以创建和修改它。例如,在 应用程序支持目录 中。

let databaseURL = try FileManager.default
    .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    .appendingPathComponent("db.sqlite")
let dbQueue = try DatabaseQueue(path: databaseURL.path)

如何打开我的应用程序资源中的数据库?

如果您的应用程序不需要修改数据库,请打开对资源的只读 连接

var configuration = Configuration()
configuration.readonly = true
let dbPath = Bundle.main.path(forResource: "db", ofType: "sqlite")!
let dbQueue = try DatabaseQueue(path: dbPath, configuration: configuration)

如果应用程序应当修改数据库,您需要将它复制到可以修改的地方。例如,在 应用程序支持目录 中。只有在此之后,才能打开 连接

let fileManager = FileManager.default
let dbPath = try fileManager
    .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    .appendingPathComponent("db.sqlite")
    .path
if !fileManager.fileExists(atPath: dbPath) {
    let dbResourcePath = Bundle.main.path(forResource: "db", ofType: "sqlite")!
    try fileManager.copyItem(atPath: dbResourcePath, toPath: dbPath)
}
let dbQueue = try DatabaseQueue(path: dbPath)

如何关闭数据库连接?

数据库连接由 数据库队列 管理的。当数据库队列或池被释放在并且此连接的所有用法都已经完成时,会关闭连接。

在后台线程中运行的数据库访问将推迟连接的关闭。

如何将请求作为 SQL 打印出来?

当您想调试一个没有达到预期结果的请求时,您可能想打印出实际执行的 SQL。

您可以将您的请求转换为 SQLRequest 实例

try dbQueue.read { db in
    let request = Wine
        .filter(Column("origin") == "Burgundy")
        .order(Column("price")
    
    let sqlRequest = try SQLRequest(db, request: request)
    print(sqlRequest.sql)
    // Prints SELECT * FROM wine WHERE origin = ? ORDER BY price
    print(sqlRequest.arguments)
    // Prints ["Burgundy"]
}

另一种选择是设置一个跟踪函数,该函数将打印出应用程序执行的所有 SQL 请求。您在连接数据库时提供跟踪函数。

var config = Configuration()
config.trace = { print($0) } // Prints all SQL statements
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

try dbQueue.read { db in
    let wines = Wine
        .filter(Column("origin") == "Burgundy")
        .order(Column("price")
        .fetchAll(db)
    // Prints SELECT * FROM wine WHERE origin = 'Burgundy' ORDER BY price
}

泛型参数 'T' 无法推断

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

// Generic parameter 'T' could not be inferred
let x = try dbQueue.read { db in
    let result = try String.fetchOne(db, ...)
    return result
}

这是一个 Swift 编译器问题(请参阅 SR-1570)。

一般解决办法是显式声明闭包结果的类型

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

SQLite错误10 "磁盘I/O错误",SQLite错误23 "未授权"

这些错误可能是SQLite无法访问数据库的标志,原因在于数据保护

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

通过使用更宽松的文件保护,也可以完全防止此错误发生。

实验特性是什么?

自GRDB 1.0以来,所有语义版本的向后兼容保证都适用:库的下一个主要版本之前不会发生破坏性变更。

然而,有一个例外:实验性特性,标记为"🔥 EXPERIMENTAL"徽章。它们是过于年轻或缺乏用户反馈的先进功能。它们尚未稳定。

这些实验性特性不受语义版本保护,可能在库的两次次要发布之间发生破坏。为了帮助它们变得稳定,非常欢迎您的反馈

示例代码


感谢

旧版本

变更跟踪

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

可持久化协议

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

RowConvertible协议

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

TableMapping协议

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

用户自定义数据库行的解码

本章已被重命名为超越FetchableRecord