GRDBPlus 3.6.2

GRDBPlus 3.6.2

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



GRDBPlus 3.6.2

  • Gwendal Roué

GRDB 3 Swift Swift Platforms License Build Status

SQLite数据库工具包,侧重于应用开发

最新发布:2018年1月5日 • 版本 3.6.2 • 变更日志从GRDB 2迁移到GRDB 3

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

Swift版本 GRDB版本
Swift 4.2 v3.6.2
Swift 4.1 v3.6.2
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

记录和查询接口

应用工具

  • 迁移:随着您的应用程序的发展,转换您的数据库。
  • 全文搜索:执行高效和可定制的全文搜索。
  • 联合查询支持:消费复杂的联合查询。
  • 数据库变更观察:观察数据库变更和事务。
  • 加密:使用SQLCipher加密您的数据库。
  • 备份:将数据库内容转储到另一个数据库。

了解一些有用的信息

通用指南与良好实践

常见问题解答

示例代码

安装

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

有关GRDB和SQLCipher的安装程序,请参阅加密

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

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

CocoaPods

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

use_frameworks!
pod 'GRDB.swift'

Swift 包管理器

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

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

注意,目前不支持 Linux。

Carthage

Carthage 可以构建 GRDB 框架,但它也可能无法解释地失败。因此,这种方法不受 支持

如果您决定使用 Carthage 尽管有此警告,并且遇到任何 Carthage 相关错误,请在本 Carthage 仓库 中打开 issues,在 Stack Overflow 上提问,召唤您的本地 Xcode 专家,或者提交一个 pull request,确保 make test_CarthageBuild 命令能够 100% 成功(一次不够)。更多信息请见 #262

手动

  1. 下载 GRDB 的副本,或者克隆其代码库,并确保您使用带有 git checkout v3.6.2 命令的最新标签版本。

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

  3. 在您的应用程序目标(WatchOS 的扩展目标)的 Target Dependencies 部分添加 GRDBOSXGRDBiOSGRDBWatchOS 目标,在 Build Phases 选项卡的 Target Dependencies 部分。

  4. 将来自目标平台的 GRDB.framework 添加到您的应用程序目标的 Embedded Binaries 部分(WatchOS 的扩展目标)。

💡 提示:有关此类集成的示例,请参阅 示例应用程序

示例应用程序

此仓库包含一个演示应用程序,展示了以下内容

数据库连接

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

import GRDB

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

它们的不同之处在于

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

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

数据库队列

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

import GRDB

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

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

数据库队列可以在任何线程中使用。写入和读取方法是同步的,会在受保护的dispatch队列中执行数据库语句,直到当前线程被阻塞

// 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方法将您的数据库语句包装在事务中,只有在没有错误发生时才提交事务。在第一次未处理的错误时,所有更改都会回滚,整个事务都会回滚,并重新抛出错误。

    当需要精确的事务处理时,请参阅事务和保存点

数据库队列需要您的应用程序遵循某些规则,以确保其安全性保证。请参阅并发章节。

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

DatabaseQueue配置

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方法将您的数据库语句包装在事务中,只有在没有错误发生时才提交事务。在第一次未处理的错误时,所有更改都会回滚,整个事务都会回滚,并重新抛出错误。

    当需要精确的事务处理时,请参阅事务和保存点

  • 数据库连接池可以对数据库进行快照

为了提供其安全保证,数据库连接池需要您的应用程序遵循某些规则。 请参见并发 章节,以了解有关数据库连接池、它们与数据库队列的不同之处以及高级用例的更多详情。

💡 提示:请参阅示例应用程序 以获取在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
    }
}

数组和光标都可以迭代数据库的结果。你怎么选择其中一个?看看它们的区别

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

    // 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枚举等)信息,请参见,以及StatementArguments以获取SQLite参数的详细文档。

与包含数据库行副本的行数组不同,行指针接近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 String。
    • Blob SQLite值转换到Foundation Data。

    更多支持类型信息,请参见

  • 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有一个弱类型系统,并提供了方便转换,如将字符串转换为整数,将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类型完全防止这些便利转换。

DatabaseValue

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

你可以像获取其他值类型一样获取 DatabaseValue。

let dbValue: DatabaseValue = row[0]
let dbValue: DatabaseValue? = row["name"] // nil if and only if column does not exist

// Check for NULL:
dbValue.isNull // Bool

// The stored value:
dbValue.storage.value // Int64, Double, String, Data, or nil

// All the five storage classes supported by SQLite:
switch dbValue.storage {
case .null:                 print("NULL")
case .int64(let int64):     print("Int64: \(int64)")
case .double(let double):   print("Double: \(double)")
case .string(let string):   print("String: \(string)")
case .blob(let data):       print("Data: \(data)")
}

您可以使用 DatabaseValueConvertible.fromDatabaseValue() 方法从 DatabaseValue 提取常规的 values(Bool,Int,String,Date,Swift 枚举等)。

let dbValue: DatabaseValue = row["bookCount"]
let bookCount   = Int.fromDatabaseValue(dbValue)   // Int?
let bookCount64 = Int64.fromDatabaseValue(dbValue) // Int64?
let hasBooks    = Bool.fromDatabaseValue(dbValue)  // Bool?, false when 0

let dbValue: DatabaseValue = row["date"]
let string = String.fromDatabaseValue(dbValue)     // "2015-09-11 18:14:15.123"
let date   = Date.fromDatabaseValue(dbValue)       // Date?

fromDatabaseValue 对于无效转换返回 nil。

let row = try Row.fetchOne(db, "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 协议,可以视为一个 DatabaseValue 字典。

// All the (columnName, dbValue) tuples, from left to right:
for (columnName, dbValue) in row {
    ...
}

你可以从字典构建行(标准的 Swift 字典和 NSDictionary)。有关支持类型的更多信息,请参阅

let row: Row = ["name": "foo", "date": nil]
let row = Row(["name": "foo", "date": nil])
let row = Row(/* [AnyHashable: Any] */) // nil if invalid dictionary

但是行不是真正的字典:它们可能包含重复的列。

let row = try Row.fetchOne(db, "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 完全相同。在 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:) 了解更多信息。

值查询

你可以直接获取值,而不是行。像行一样,你可以以 cursorsarrayssingle 值的形式获取它们(参见 获取方法)。值是从 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 值。

有许多支持值类型(Bool,Int,String,Date,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)

数据(以及内存节省)

数据 适合于 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?
}

未被复制的数据不比迭代步骤活得久:请确保您不要在这之后使用它。

日期和日期组件

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

以下是如何支持 SQLite 所支持的多种 日期格式

SQLite 格式 日期 日期组件
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 纪元以来的时间戳 读取 ²
现在

¹ 日期以 UTC 时区存储和读取。缺失的组件默认为零。

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

Date

Date 可以像其他 常用值 一样存储和检索。

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" 格式以 UTC 时区存储。精确到毫秒。

☝️ 注意:此格式被选择,因为它:

  • 可比较(可以使用 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"])

请参阅 Codable Records 获取更多日期自定义选项。

DateComponents

DateComponents 通过 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 可以像其他 一样存储和检索。浮点型 NSNumbers 存储为 Double。整数和布尔值存储为 Int64。不满足 Int64 的整数无法存储:您将收到一个致命错误。当 NSNumber 包含 UInt64 类型时(例如)请小心。

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 将 uuid 存储为 16 字节的数据块,并可以从 16 字节的数据块和类似 "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 和 Data。由于 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 ... }

预编语句

预编语句(Prepared Statements)允许你预先准备一个SQL查询,并可以在需要的时候多次执行,每次使用不同的参数。

有两种类型的预编语句:选择语句(select statements)更新语句(update statements)

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,它恰好实现了ExpressibleByArrayLiteralExpressibleByDictionaryLiteral协议)来设置它们。

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

设置好参数后,你可以执行预编语句。

try updateStatement.execute()

选择语句可以在原始SQL查询字符串可以适应的任何位置使用(参见提取查询)。

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可能会崩溃。

关于行查询(row queries)、值查询(value queries)和记录(Records),请参阅更多信息。

预编语句缓存

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

不要自己缓存语句。

☝️ 注意:这是因为你没有必要的工具。语句与特定的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')")!
}

函数参数接收一个 DatabaseValue 数组,并返回任何有效的 (Bool,Int,String,Date,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 方法返回最终的聚合 (Bool,Int,String,Date,Swift 枚举等)。

当聚合是“纯”的时候,SQLite 有机会执行额外的优化,这意味着它们的输出仅取决于它们的输入。所以,尽可能地将 pure 参数设置为 true。

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

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

数据库模式自省

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 Package Manager。

#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 连接,这(奇怪地)意味着它们不是线程安全的。确保您在专用 dispatch queues 中触摸原始数据库和语句。
  • 自行承担使用原始 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:请参阅Configuration.busyMode
  • sqlite3_changessqlite3_total_changes:请参阅Database.changesCount 和 Database.totalChangesCount
  • sqlite3_closesqlite3_close_v2sqlite3_next_stmtsqlite3_open_v2:请参阅数据库连接
  • sqlite3_commit_hooksqlite3_rollback_hooksqlite3_update_hook:请参阅事务观察者协议数据库区域观测值观察获取记录控制器
  • 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

记录

GRDB在SQLite API之上,提供了一系列协议和一个类,帮助将数据库行作为名为“记录”的普通对象进行操作

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

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

为了定义自己的记录,你可以从现有的Record类进行子类化,或者使用具有聚焦功能集的协议扩展你的结构体和类:获取方法、持久化方法、记录比较等。

使用协议扩展结构体更为“Swift风格”。从记录类进行子类化更为“经典”。你可以任选其一。请参阅一些记录定义示例记录方法列表以获取概述。

☝️ 注意:如果您熟悉Core Data的NSManagedObject或Realm的Object,可能会遇到文化冲击:GRDB的记录不是唯一的,不会自动更新,也不会懒加载。这是面向协议编程的目的和后果。您应该阅读如何用SQLite和GRDB构建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)

👉delete方法在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

可访问记录协议

可访问记录协议为任何可以从数据库行构造的类型提供了获取方法

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

使用可访问记录,可以继承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[]下标的详细信息。

当你的记录类型采纳标准可编码协议时,你不必提供init(row:)的实现。请参阅可编码记录以了解更多信息

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

可访问记录允许采纳类型可以由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以了解更多关于查询参数的详细信息。

☝️ 注意:出于性能考虑,在获取查询迭代的期间,重用相同的行参数到init(row:)。如果你希望保留行供以后使用,请确保存储一个副本:self.row = row.copy()

☝️ 注意FetchableRecord.init(row:)初始化符能满足大多数应用程序的需求。但是,一些应用程序的需求比其他应用程序更严格。当可访问记录未能提供所需的支持时,请参阅超越可访问记录章节。

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和可访问记录协议时,可以使用查询接口进行获取

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

持久化记录协议

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方法允许采用类型在成功插入后存储其rowID,并且仅对具有自动增长主键的表有用。它在受保护的dispatch队列中被调用,并与其他数据库更新进行序列化。

要使用持久化协议,请从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
    }
}

当您的记录类型采用标准的Encodable协议时,您不需要提供encode(to:)的实现。有关更多信息,请参见Codable Records

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

持久化方法

Record子类和采用PersistableRecord的类型提供了插入、更新和删除方法的默认实现

// 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可能会抛出DatabaseError

  • 如果更新失败因为数据库中没有匹配的行,则updateupdateChanges也可能抛出PersistenceError

    当保存一个可能在或可能不在数据库中存在的对象时,偏好使用save方法

  • save确保您values被存储在数据库中。

    如果记录有一个非空的primary key,则执行UPDATE,然后如果没有行被修改,则执行INSERT。如果没有primary key或primary key为null,则直接执行INSERT。

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

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

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

自定义持久化方法

您自定义的类型可能在调用持久化方法时执行额外的操作。

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

当您子类化 Record 时,您只需重写自定义方法并调用 super

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

如果您使用原始的 PersistableRecord 协议,请使用以下特殊方法之一: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)
}

可编码记录根据它们自己对 Encodable 和 Decodable 协议的实施编码和解析其属性。然而,数据库有特定的要求

  • 如果属性具有首选的数据库表示(所有 都采用 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中的标准<ЦИТwireJSONDecoder和<克林JSONEncoder。默认情况下,Data值使用.base64策略处理,Date使用.millisecondsSince1970策略,非符合规范的浮点数使用.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是稳定的。这种稳定性对于记录比较按预期工作以及数据库观察工具,如值观察,以便准确识别已更改的记录是必需的。

日期和UUID编码策略

默认情况下,Codable记录将他们的日期和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字典

您的Codable记录可以存储在数据库中,但它们可能还有其他用途。在这种情况下,您可能需要根据上下文自定义它们的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 枚举 (这是什么意思?),则可以 instruct 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")
    }

记录类

记录 是一个设计用来继承的类。它从 可取用记录、表记录和可持久记录 协议继承其特性。此外,记录实例可以与自己之前的版本进行比较,以 避免无用的更新

记录子类通过重写数据库方法来定义它们自己的数据库关系。例如

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

记录比较

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

这有助于在记录未被编辑时避免昂贵的 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 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)
}

☝️ 注意:比较是在记录的数据库表示形式上进行的。只要您的记录类型采用PersistableRecord协议,您就不必担心Equatable。

databaseChangeshasDatabaseChanges 方法

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

let oldPlayer = Player(id: 1, name: "Arthur", score: 100)
let newPlayer = Player(id: 1, name: "Arthur", score: 1000)
for (column, oldValue) in 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

记录定制选项

GRDB记录附带许多默认行为,这些行为旨在适应大多数情况。其中许多默认设置可以根据您的特定需求进行定制

Codable记录有几个额外的选项

冲突解决

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

这些冲突通常以错误结束。但SQLite允许修改默认行为,并使用特定策略处理冲突。例如,INSERT OR REPLACE语句使用“替换”策略来替换冲突行,而不是抛出错误。

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

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

当您想在查询级别处理冲突时,指定自定义的
persistenceConflictPolicy在符合PersistableRecord协议的类型中。它将修改
insertupdate
save 持久化方法执行的INSERT和UPDATE查询。

protocol MutablePersistableRecord {
    /// The policy that handles SQLite conflicts when records are
    /// inserted or updated.
    ///
    /// This property is optional: its default value uses the ABORT
    /// policy for both insertions and updates, so that GRDB generate
    /// regular INSERT and UPDATE queries.
    static var persistenceConflictPolicy: PersistenceConflictPolicy { get }
}

struct Player : MutablePersistableRecord {
    static let persistenceConflictPolicy = PersistenceConflictPolicy(
        insert: .replace,
        update: .replace)
}

// INSERT OR REPLACE INTO player (...) VALUES (...)
try player.insert(db)

☝️ 注意ignore策略与
didInsert方法不兼容,后者通知插入的行ID。选择你想要的恶习

  • 如果您在数据库表定义中指定了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列。

没有主键,记录没有任何标识, persistence 方法可能会表现得不受欢迎: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:)(来自《a href="#fetchablerecord-protocol">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:) 方法(来自《a href="#persistablerecord-protocol">PersistableRecord 和 《a href="#mutablerecord-protocol">MutablePersistableRecord 协议)中保持它

    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,不要尝试在协议之上实现它们:您只会与框架作斗争。

相反,请查看《a href="Playgrounds/CustomizedDecodingOfDatabaseRows.playground/Contents.swift">自定义数据库行解码游乐场。您将运行一些示例代码,并学习如何在需要时脱离 FetchableRecord。记住,离开 FetchableRecord 不会使您失去《a href="#requests">查询接口请求以及一般而言,《a href="#tablerecord-protocol">TableRecord 和 《a href="#persistablerecord-protocol">PersistableRecord 协议的所有 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()
    }
}

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

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

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

有关更多信息,请参阅《a href="#record-protocols-overview">记录协议概述和《a href="#codable-records">可编码记录。

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

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

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

记录方法列表

这是记录方法及其所需协议的列表。记录类采用了所有这些协议,并添加了一些额外的方法。

方法 协议 注意
核心方法
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 ²
获取记录游标Cursors
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 CONFLICTWITHOUT 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'。

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

添加具有其名称和最终类型(文本、整数、双精度、数值、布尔型、BLOB、日期和日期时间)的常规列 - 请参阅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 子类(见 Records

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' 为 true,但 'æ' LIKE 'Æ' 为 false。

  • MATCH

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

    FTS3 和 FTS4

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

    FTS5

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

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 通过调用 Swift 内置字符串函数 capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercased 扩展 SQLite 的 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?

当你还希望使用数据库观察工具(如 ValueObservation)时,你必须走得更远一步,并更改请求的类型

  • 当您更改选择时,首选 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?

这些请求可以为 ValueObservation 提供数据

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)

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

根据主键删除记录 也是一种非常常见的操作。它有一个接受任何单列主键的快捷方式

// 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 关联的类型是 Row 或一个 或一个符合 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 构建的一个-fetch 请求。例如

    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 关联类型可以解析数据库行(Row 本身、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选项触发数据库的重创建。模式更改是任何与包含用于创建数据库表的SQL的sqlite_master表不同的差异。

高级数据库模式更改

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
    前缀搜索(包含以“数据”开头的单词的文档) X X X
    短语搜索(包含短语“SQLite 数据库”的文档) X X X
    布尔搜索(包含“SQLite”或“数据库”的文档) X X X
    邻近搜索(包含“SQLite”接近“数据库”的文档) 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 ²
    停用词(不索引,不匹配“和”、“the”等单词) ¹ ¹ X ²
    拼写检查(使“alamaba”与“alabama”匹配) ¹ ¹ ¹
    :bowtie: 其他功能
    排名(按相关性排序结果) ¹ ¹ X
    片段(显示匹配周围的几个字) X X X

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

    ² 需要一个 自定义分词器

    欲查询完整特性列表,请阅读SQLite文档。一些缺失的特性可以通过额外的应用程序代码实现。

  • 速度与磁盘空间限制。 大约来说,FTS4和FTS5比FTS3快,但占用更多空间。FTS4仅支持内容压缩。

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

  • 应用中集成的SQLite库。 iOS、macOS和watchOS捆绑的SQLite版本默认支持FTS3和FTS4,但不是FTS5。要使用FTS5,请参阅启用FTS5支持

  • 有关更多信息,请参阅FST3与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分词器:`simple`、`porter`和`unicode61`,它们使用不同的算法匹配查询与索引内容

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 X ¹ X ¹ X ¹
Jérôme JÉRÔME X ¹
Jérôme Jerome X ¹
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"分词器对Unicode字符不区分大小写。它将“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)
}

第一个初始化器会将您原始的模式与查询语法进行验证,可能会抛出数据库错误

// 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. 使用GRDBPlus CocoaPod。它使用系统SQLite,并需要iOS 11.4+ / macOS 10.13+ / watchOS 4.3+

    pod 'GRDBPlus'
  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 在 ascii 上 porter 在 unicode61 上
Foo Foo X X X X
Foo FOO X X X X
Jérôme Jérôme X ¹ X ¹ X ¹ X ¹
Jérôme JÉRÔME X ¹ X ¹
Jérôme Jerome X ¹ X ¹
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])

全文记录

您可以在全文虚拟表的周围定义 记录 类型。

但是,这些表没有明确的首要键。相反,它们使用 隐式行ID首要键:一个名为 rowid 的特殊隐藏列。

您必须 公开这个隐藏列 才能通过首要键获取、删除和更新全文记录。

Unicode全文注意事项

SQLite内置的针对 FTS3、FTS4FTS5 的分词器通常具有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. 这些扭曲的名称对于期望特定列名的 FetchableRecord 类型来说并不适合。毕竟,如果 Team 记录类型可以读取 SELECT * FROM team ...,则它应该能够读取 SELECT ..., team.*, ...

因此我们需要另一种技术。 以下我们将看到如何将行拆分为片段,并保留列名称。

SELECT player.*, team.*, MAX(round.score) AS maxScore FROM ... 将被拆分为三个片段:一个包含球员列的片段,一个包含团队列的片段,以及一个剩余片段,包含剩余列。Player 记录类型将能够读取第一个片段,其中包含 Player.init(row:) 初始化器所需的列。同样,Team 记录类型可以读取第二个片段。

与名称混淆技术不同,拆分行保持了 SQL 的易读性,接受您亲手编写的 SQL 查询,并且尽可能地与您现有的 record 类型 合作。

分割行,简介

让我们首先编写一些介绍性代码,希望本章能让你理解各个部分是如何组合在一起的。我们将看到稍后如何使用记录精简初始方法,如何跟踪连接请求的变化,以及如何使用标准的可解码协议。

为了分割行,我们将使用行适配器。行适配器将行适配,使得行消费者能够看到他们想要的确切列。行适配器可以定义几个行作用域,提供许多行段的访问。听起来就像是一个完美的匹配。

一开始,有一个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 ...
        """

我们需要一个适配器,用于提取运动员列,在一个切片中,其列数与运动员表中的列数相同。这是范围行适配器

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

我们在一个单一的作用域适配器中将这两个适配器合并,这样我们就可以访问两个切片行:

    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通过命名作用域提供对多个行切片的访问。
  • 如何使用行索引提取记录,或使用可选记录以处理LEFT JOIN。

分割行,记录方式

我们上面的介绍介绍了重要的技术。它使用行适配器来分割行。它使用行索引从行切片中提取记录。

但我们可能想要使其更易于使用和更健壮。

  1. 消费记录通常比原始行要容易。
  2. 连接的记录不一定需要表中的所有列(见“请求选择的列”中的TableRecord.databaseSelection)。
  3. 构建行适配器花费时间长且容易出错。

为了解决第一个问题,让我们定义一个记录,它包含我们的运动员、可选团队和最高得分。因为它可以解码数据库行,所以它采用可获取记录协议

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] {

为了承认玩家和队伍记录可以自定义选择“玩家”和“队伍”列,我们将以稍微不同的方式编写我们的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.*,除非玩家定义了自定义的选择

☝️ 注意:您也可以使用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")
    })

💡在本章中,我们学习了如何定义一个自定义请求,它可以既从联合查询中抓取记录,又可以提供数据库观察工具。

按可编码方式拆分行

可编码记录在标准的Decodable协议之上构建,以便解码数据库行。

您也可以使用可编码记录来消费复杂的联合查询。作为演示,我们将重写上面的示例代码

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"

该闭包在受保护的分发队列中运行,与所有数据库更新序列化。

此"提交后挂钩"有助于同步数据库与其他资源,例如文件或系统传感器。

下面示例中,如果位置管理器成功存储在数据库中,它则开始监控位置管理器的区域。

/// 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("Player 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的示例。

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 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.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 request。

☝️ 注意:熟悉反应式编程的读者将认识到在 ValueObservation.combine 方法中的反应式操作符 CombineLatest。尽管反应式操作符不关心数据一致性,但如果你 使用如 RxGRDB 的反应式层,请用 ValueObservation.combine 组合观测,而不是用 CombineLatest 操作符。

值观测错误处理

当您开始进行观测时,可以提供一个 onError 回调。当在数据库更改后获取新值发生错误时,将调用此回调。它就像值一样进行调度(参见 值观测调度

let observer = try observation.start(
    in: dbQueue,
    onError: { error in
        print("fresh value could not be fetched")
    },
    onChange: { value in
        print("fresh value: \(value)")
    })

值观测选项

可以配置值观测的一些行为

值观测范围

使用 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。

值观测调度

使用 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。它在名为“reduce 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 跟踪数据库 请求 的更改,并通知每个影响性的 事务

在跟踪的表中不会错过任何插入、更新或删除操作。这包括由 外键SQL 触发器 触发的不直接更改。

当数据库中的更改被提交后,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

您还可以用 数据库区域 或符合 DatabaseRegionConvertible 协议的任何类型来填充 DatabaseRegionObservation。例如

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

获取记录控制器

获取记录控制器跟踪请求结果的更改,向表格视图和集合视图提供数据,并在请求结果更改时对单元格进行动画。

其外观和行为与Core Data的NSFetchedResultsController非常相似。

给定一个获取请求和一个实现了FetchableRecord协议的类型,例如Record类的子类,获取记录控制器能够跟踪获取请求结果的更改,通知这些更改,并以表格视图或集合视图适合的形式返回请求结果,每个获取的记录一个单元格。

☝️ 注意:当您不需要对表格或集合视图进行动画处理时,请使用值观察RxGRDB

💡 提示:请参阅示例应用程序,了解使用获取记录控制器的示例。

创建获取记录控制器

初始化获取记录控制器时,您需要提供以下必填信息

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

响应用户更改

通常,获取记录控制器通过通知,在数据库层设计来响应用户更改,当数据库行位置或值发生变化时进行通知。

更改不会反映,直到它们在成功的事务事务中应用到数据库。

// 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的具有高复杂度、高内存消耗的diffing算法,因此不适用于大型结果集。一百行可能还好,但一千行可能就不太好了。如果您的应用程序在大列表上遇到问题,请参阅问题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)")
}

修改获取请求

您可以更改获取记录控制器的获取请求或SQL查询。

controller.setRequest(Player.order(Column("name")))
controller.setRequest(sql: "SELECT ...", arguments: ...)

如果新的请求检索不同的记录集,则通知回调会通知最终更改。

☝️ 注意:此行为与Core Data的NSFetchedResultsController不同,后者在替换获取请求时不会通知记录更改。

更改回调是异步调用的。 这意味着在主线程中修改请求不会立即触发回调。当您需要立即采取行动时,使用控制器的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 允许您填充表和集合视图,并通过对数据库内容的更新确保它们保持最新状态。

为了实现好的动画更新效果,FetchedRecordsController 需要识别两个不同结果集之间的相同记录。当记录使用了TableRecord 协议时,它们会根据主键自动进行比较。

class Player : TableRecord { ... }
let controller = FetchedRecordsController(
    dbQueue,
    request: Player.all())

对于其他类型,FetchedRecordsController 需要您更加明确。

let controller = FetchedRecordsController(
    dbQueue,
    request: ...,
    isSameRecord: { (player1, player2) in player1.id == player2.id })

实现表格视图数据源方法

表格视图数据源会请求 FetchedRecordsController 提供相关信息。

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 可以通知由于某些添加、删除、移动或更新操作而导致控制器已获取的记录已更改,并帮助将动画更改应用于 UITableViewDataSource。

典型的表格视图更新

对于动画表格视图更新,使用 willChangedidChange 回调来包围 FetchedRecordsController 提供的事件,如下例所示

// 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的具有高复杂度、高内存消耗的diffing算法,因此不适用于大型结果集。一百行可能还好,但一千行可能就不太好了。如果您的应用程序在大列表上遇到问题,请参阅问题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协议让您能够观察单个数据库更改和事务。

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 DELETE和ON UPDATE操作以及SQL触发器(外键)(SQL触发器)触发的间接更改。

☝️ 注意:未通知的更改包括更改内部系统表(例如sqlite_master),更改WITHOUT ROWID表,以及由ON CONFLICT REPLACE子句触发的重复行的删除(这个例外可能在SQLite的下一个版本中发生变化)。

在事务提交并将databaseDidCommit回调调用之前,通知的更改实际上并未写入磁盘。

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
}

由于 setData?(with:) 而暂停的更改仅在该保存点释放后通知。这确保了通知的事件仅为有机会被提交的事件。

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数据库。此限制不适用于databaseDidCommit和databaseDidRollback,它们可以使用其数据库参数。

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():)并不是停止由值观察启动的观察者的正确方法。在这种情况下,正确的方法是取消分配观察者。

或者,可以使用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的方式将这些信息传达给事务观察者

DatabaseRegion为例如值观察和数据库区域观察提供动力。

一个区域通知的是请求结果中的潜在变化,而不是实际变化。只有当语句实际上通过插入、更新或删除行修改了所追踪的表和列时,才会通知更改。

例如,如果您观察了Player.select(max(Column("score")))的区域,就会接收到对player表的score列(更新、插入和删除)执行的所有更改的通知,即使这些更改没有修改最高分值。但是,您不会收到对其他数据库表执行更改或对玩家表其他列进行更新的任何通知。

类似地,观察Country.filter(key: "FR")的区域将通知对整个country表的更改。这是因为在SQLite中,它只通知改变化的行的rowid,我们无法检查是否是更改行“FR”,或者可能是其他行。这种限制不适用于rowid为主键的表:Player.filter(key: 42)只会在id为42的行上执行更改时通知。

有关更多详情,请参阅参考

数据库区域可转换协议

DatabaseRegionConvertible是一个用于所有可以转换成数据库区域的类型协议

protocol DatabaseRegionConvertible {
    func databaseRegion(_ db: Database) throws -> DatabaseRegion
}

所有请求都采用此协议,这使用户可以使用数据库区域观察值观察对其进行观察。

当您想要封装复杂的请求为一个专门的类型,同时仍能从观察API中受益时,请使用此协议。有关更多信息,请参阅数据库区域可转换观察

对SQLite预更新挂钩的支持

一个自定义SQLite构建可以激活SQLite“预更新挂钩”。在这种情况下,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.6.2
    git submodule update --init SQLCipher/src
  2. GRDBCipher.xcodeproj项目嵌入您的自己的项目中。

  3. GRDBCipherOSXGRDBCipheriOS目标添加到您的应用程序目标的Target Dependencies部分,在Build Phases标签页的

  4. GRDBCipher.framework从目标平台添加到您的目标的Embedded Binaries部分,在General标签页中。

您可以通过提供口令来创建和打开一个加密数据库

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.

备份

可以将数据库(复制)备份到另一个位置。

示例来说,备份可以帮助你在实现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结果码

为了方便,扩展结果码在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发行说明对此并不十分清楚:请谨慎编写处理扩展结果码的代码。

持久性错误

持久性错误可持久化记录协议抛出,在单一情况下:当更新方法找不到任何可更新的行时

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

    解决方案:修复数据库的内容,使用非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]"])

    解决方案:向玩家电子邮件列添加唯一索引,或使用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
        }
    }
}

有关更多信息,请参阅预编译语句DatabaseValue

错误日志

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

这个全局错误回调必须在您的应用程序生命周期早期进行配置。

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

⚠️ 警告:Database.logError必须在打开任何数据库连接之前设置。这包括GRDB打开的连接,以及其他工具(如第三方库)打开的连接。在连接打开后设置它是不正确的SQLite使用,并且没有任何效果。

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

Unicode

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

然而,SQLite并没有提供任何对Unicode敏感的字符串转换或比较功能。

Unicode函数

UPPERLOWER内置的SQLite函数不是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的内置校对:二进制、不区分大小写和rtrim

GRDB 搭载了五个扩展校对功能,它利用基于标准 Swift 字符串比较函数和运算符的 Unicode 可知比较

  • unicodeCompare(使用内置的 <=== Swift 运算符)
  • 不区分大小写的比较
  • 局部化不区分大小写的比较
  • 局部化比较
  • 局部化标准比较

可以将校对应用于表列。 thereafter,涉及此列的所有比较将自动触发比较函数

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返回一个future值,您需要在任何派发队列中使用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)
}

☝️ 注意:目前,快照序列化了所有数据库访问。在将来,快照可能允许并发读取。

DatabaseWriter 和 DatabaseReader 协议

DatabaseQueue 和 DatabasePool 都采用了 DatabaseReaderDatabaseWriter 协议。DatabaseSnapshot 仅采用 DatabaseReader。

这些协议提供了一个统一的 API,让你可以编写针对所有并发模式的通用代码。例如,它们可以支持

只有五种类型采用了这些协议:DatabaseQueue、DatabasePool、DatabaseSnapshot、AnyDatabaseReader 和 AnyDatabaseWriter。不支持扩展这个集合:任何未来的 GRDB 发布版本都可能打破你的自定义写入器和读取器,而无需通知。

DatabaseReader 和 DatabaseWriter 提供了 小的通用保证:它们不消除队列、池和快照之间的差异。例如,请参阅数据库队列和池的差异

然而,你可以通过给它们一个 DatabaseReader 来防止应用程序的一部分在数据库中写入

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

☝️ 注意:DatabaseReader 并非一种防止应用程序组件在数据库中写入的 安全 方法,因为写入访问只是形式上的转换

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方法返回的Array和fetchCursor返回的光标是不一样的

fetchAll会将所有值从数据库复制到内存中,而fetchCursor会像SQLite生成结果那样迭代数据库结果,从而利用SQLite的高效性。

你应该只在需要将数组保存供以后使用时(例如在主线程中迭代它们的内容)才加载数组。否则,使用fetchCursor

有关有关fetchAllfetchCursor的更多信息,请参阅获取方法。另外,请参阅Row.dataNoCopy方法。

除非必要,不要更新行

更新语句代价高昂:SQLite必须查找更新的行,更新值,并将更改写入磁盘。

因此,当覆盖的值与现有值相同,最好避免执行UPDATE语句:

if player.hasDatabaseChanges {
    try player.update(db)
}

有关更多信息,请参阅记录比较

性能提示:了解SQL的优点和缺点

考虑一个简单的情况:您的商店应用程序需要显示作者及其可用的书籍数量列表

  • J. M. Coetzee (6)
  • Herman Melville (1)
  • Alice Munro (3)
  • Kim Stanley Robinson (7)
  • Oliver Sacks (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的高效查询计划。

性能提示:避免使用字符串和字典

在寻找最佳性能时,最好避免使用String和Dictionary Swift类型。

现在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()
}

常见问题解答

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

此问题假定您的应用程序需要从头创建一个新的数据库。如果您的应用程序需要打开嵌入在应用程序中的作为资源存储的现有数据库,请参阅如何打开应用程序作为一个资源存储的数据库?

数据库必须存储在有效位置,以便可以创建和修改。例如,在应用程序支持目录中。

let databaseURL = try FileManager.default
    .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    .appendingPathComponent("db.sqlite")
let dbQueue = try DatabaseQueue(path: databaseURL.path)

如何打开存储为应用程序资源的数据库?

如果您的应用程序不需要修改数据库,可以打开读取-only连接到您的资源

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开始,所有向后兼容的语义版本保证都适用:除非库的下一个主要版本,否则不会有破坏性变更。

然而,有一个例外:实验性功能,标记有 "🔥 实验" 徽章。这些是太年轻或缺乏用户反馈的先进功能。它们尚未稳定。

这些实验性功能不受语义版本控制保护,在库的两个次要版本之间可能会破坏。为了帮助它们变得稳定,非常重视您的反馈

示例代码

  • 文档中充满了GRDB代码片段。
  • 示例应用:一个iOS应用样本。
  • WWDC伴侣:一个iOS应用样本。
  • 检查GRDB.xcworkspace:它包含可用于玩耍的GRDB启用游乐场。
  • 如何使用JSON有效负载同步数据库表:JSONSynchronization.playground

感谢

旧版本

变更跟踪

本章已更名为 记录比较

可持久化协议

此协议在 GRDB 3.0 中已更名为 PersistableRecord

行可转换协议

此协议在 GRDB 3.0 中已更名为 FetchableRecord

表映射协议

此协议在 GRDB 3.0 中已更名为 TableRecord

用户自定义数据库行解码

本章已重命名为超越可检索记录