GRDB.swift 6.24.1

GRDB.swift 6.24.1

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布时间最后发布时间2024年1月
SPM支持 SPM

由Gwendal Roué,Gwendal Roué,Gwendal Roué维护。



GRDB.swift 6.24.1

  • Gwendal Roué
GRDB: A toolkit for SQLite databases, with a focus on application development.

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

Swift 5.7 License CI Status

最新版本:2023年7月9日 • 版本 6.16.0 • 版本 6.16.0变更日志从GRDB 5迁移到GRDB 6

要求:iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 5.7+ / Xcode 14+

联系方式:

什么是GRDB?

使用此库将应用程序的永久数据保存到SQLite数据库中。它内置了满足常见需求的工具

  • SQL生成

    通过持久化和查询方法增强应用程序模型,这样您就可以在没有必要处理SQL和原始数据库行的情况下操作。

  • 数据库观察

    当数据库值被修改时,获取通知。

  • 鲁棒的并发

    多线程应用程序可以高效地使用它们的数据库,包括支持并发读取和写入的WAL数据库。

  • 迁移

    随着您发布应用程序的新版本而发展数据库的架构。

  • 利用SQLite技能

    并非所有开发人员都需要高级SQLite功能。但是,当您需要时,GRDB可以像您希望的那样锋利。带上您的SQL和SQLite技能,或在继续的过程中学习新的技能!


使用方法文档安装常见问题


使用方法

在四个步骤中开始使用数据库
import GRDB

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

// 2. Define the database schema
try dbQueue.write { db in
    try db.create(table: "player") { t in
        t.primaryKey("id", .text)
        t.column("name", .text).notNull()
        t.column("score", .integer).notNull()
    }
}

// 3. Define a record type
struct Player: Codable, FetchableRecord, PersistableRecord {
    var id: String
    var name: String
    var score: Int
}

// 4. Write and read in the database
try dbQueue.write { db in
    try Player(id: "1", name: "Arthur", score: 100).insert(db)
    try Player(id: "2", name: "Barbara", score: 1000).insert(db)
}

let players: [Player] = try dbQueue.read { db in
    try Player.fetchAll(db)
}
访问原始 SQL
try dbQueue.write { db in
    try db.execute(sql: """
        CREATE TABLE place (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          favorite BOOLEAN NOT NULL DEFAULT 0,
          latitude DOUBLE NOT NULL,
          longitude DOUBLE NOT NULL)
        """)
    
    try db.execute(sql: """
        INSERT INTO place (title, favorite, latitude, longitude)
        VALUES (?, ?, ?, ?)
        """, arguments: ["Paris", true, 48.85341, 2.3488])
    
    let parisId = db.lastInsertedRowID
    
    // Avoid SQL injection with SQL interpolation
    try db.execute(literal: """
        INSERT INTO place (title, favorite, latitude, longitude)
        VALUES (\("King's Cross"), \(true), \(51.52151), \(-0.12763))
        """)
}

查看 执行更新

访问原始数据库行和值
try dbQueue.read { db in
    // Fetch database rows
    let rows = try Row.fetchCursor(db, sql: "SELECT * FROM place")
    while let row = try rows.next() {
        let title: String = row["title"]
        let isFavorite: Bool = row["favorite"]
        let coordinate = CLLocationCoordinate2D(
            latitude: row["latitude"],
            longitude: row["longitude"])
    }
    
    // Fetch values
    let placeCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")! // Int
    let placeTitles = try String.fetchAll(db, sql: "SELECT title FROM place") // [String]
}

let placeCount = try dbQueue.read { db in
    try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")!
}

查看 查询检索

数据库模型类型即“记录”
struct Place {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
}

// snip: turn Place into a "record" by adopting the protocols that
// provide fetching and persistence methods.

try dbQueue.write { db in
    // Create database table
    try db.create(table: "place") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("title", .text).notNull()
        t.column("favorite", .boolean).notNull().defaults(to: false)
        t.column("longitude", .double).notNull()
        t.column("latitude", .double).notNull()
    }
    
    var berlin = Place(
        id: nil,
        title: "Berlin",
        isFavorite: false,
        coordinate: CLLocationCoordinate2D(latitude: 52.52437, longitude: 13.41053))
    
    try berlin.insert(db)
    berlin.id // some value
    
    berlin.isFavorite = true
    try berlin.update(db)
}

查看 记录

使用 Swift 查询接口查询数据库
try dbQueue.read { db in
    // Place
    let paris = try Place.find(db, id: 1)
    
    // Place?
    let berlin = try Place.filter(Column("title") == "Berlin").fetchOne(db)
    
    // [Place]
    let favoritePlaces = try Place
        .filter(Column("favorite") == true)
        .order(Column("title"))
        .fetchAll(db)
    
    // Int
    let favoriteCount = try Place.filter(Column("favorite")).fetchCount(db)
    
    // SQL is always welcome
    let places = try Place.fetchAll(db, sql: "SELECT * FROM place")
}

查看 查询接口

数据库更改通知
// Define the observed value
let observation = ValueObservation.tracking { db in
    try Place.fetchAll(db)
}

// Start observation
let cancellable = observation.start(
    in: dbQueue,
    onError: { error in ... },
    onChange: { (places: [Place]) in print("Fresh places: \(places)") })

原生支持 Combine 和 RxSwift

// Combine
let cancellable = observation.publisher(in: dbQueue).sink(
    receiveCompletion: { completion in ... },
    receiveValue: { (places: [Place]) in print("Fresh places: \(places)") })

// RxSwift
let disposable = observation.rx.observe(in: dbQueue).subscribe(
    onNext: { (places: [Place]) in print("Fresh places: \(places)") },
    onError: { error in ... })

查看 数据库观察Combine 支持RxGRDB

文档

GRDB 在 SQLite 之上运行:你应该熟悉 SQLite 常见问题。对于一般和详细信息,请转到 SQLite 文档

演示应用程序和常见问题

参考

开始使用

SQLite和SQL

记录和查询接口

应用工具

  • 迁移:随着应用程序的进化转换数据库。
  • 全文搜索:执行高效和可定制的全文搜索。
  • 数据库观察:观察数据库更改和事务。
  • 加密:使用SQLCipher加密您的数据库。
  • 备份:将数据库内容导出至其他地方。
  • 中断数据库:取消任何挂起的数据库操作。
  • 共享数据库:如何在多个进程间共享SQLite数据库——针对App Group容器、App扩展、App沙盒和文件协调的建议。

重要信息

常见问题

示例代码

安装

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

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

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

CocoaPods

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

pod 'GRDB.swift'

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

Swift 包管理器

Swift 包管理器(Swift Package Manager)自动化 Swift 代码的发行。要使用 GRDB 与 SPM,请添加对 https://github.com/groue/GRDB.swift.git 的依赖。

GRDB 提供了两个库,GRDBGRDB-dynamic。只选择其中一个。如有疑问,请首选 GRDB。如果您的应用程序需要将库链接到多个目标,并且只想一次链接到一个共享的动态框架,那么 GRDB-dynamic 库可能会很有用。有关更多信息,请参阅 如何将 Swift 包作为动态链接

注意:当前不支持 Linux。

警告:由于 Xcode 的一个 bug,当您想将 GRDB 包嵌入主应用程序以外其他的目标(如手表扩展)时,您将收到 "No such module 'CSQLite'" 的错误。UI 和单元测试目标是 OK 的。关于更多信息,请参阅 #642

Carthage

Carthage 不再被支持。有关此决定的背景信息,请参阅 #433

手动方式

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

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

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

  4. GRDB.framework 添加到应用目标(WatchOS 的扩展目标)的 通用 选项卡的 嵌入的二进制文件 部分中。

💡 提示:浏览 演示应用 以获取此类集成的示例。

数据库连接

GRDB 通过两个类提供访问 SQLite 数据库的功能:DatabaseQueueDatabasePool

import GRDB

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

它们之间的区别如下

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

如果您不确定,请选择 DatabaseQueue之后您可以随时切换到 DatabasePool

有关打开连接的更多信息以及提示,请参阅 数据库连接

SQLite API

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

高级主题

执行更新

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

例如

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

SQL查询中的 ? 和以冒号开头的关键字,如 :score,是 语句参数。您可以使用数组或字典传递参数,如示例所示。有关支持的参数类型(Bool、Int、String、Date、Swift枚举等)的更多信息,请参阅,有关SQLite参数的详细文档,请参阅StatementArguments

您还可以使用 execute(literal:) 直接在SQL查询中嵌入查询参数,如下例所示。有关更多信息,请参阅SQL内插

try dbQueue.write { db in
    let name = "O'Brien"
    let score = 550
    try db.execute(literal: """
        INSERT INTO player (name, score) VALUES (\(name), \(score))
        """)
}

永远不要直接在原始SQL字符串中嵌入值。有关更多信息,请参阅避免SQL注入

// WRONG: don't embed values in raw SQL strings
let id = 123
let name = textField.text
try db.execute(
    sql: "UPDATE player SET name = '\(name)' WHERE id = \(id)")

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

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

// CORRECT: use SQL Interpolation
try db.execute(
    literal: "UPDATE player SET name = \(name) WHERE id = \(id)")

用分号连接多个语句:

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

try db.execute(literal: """
    INSERT INTO player (name, score) VALUES (\("Arthur"), \(750));
    INSERT INTO player (name, score) VALUES (\("Barbara"), \(1000));
    """)

如果您想确保单个语句被执行,请使用预编译的Statement

在插入语句之后,您可以使用 lastInsertedRowID 获取插入行的行ID

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

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

var player = Player(name: "Arthur", score: 1000)
try player.insert(db)
let playerId = player.id

获取查询

数据库连接 允许您获取数据库行、普通值和自定义模型,也称为“记录”。

是SQL查询的原始结果

try dbQueue.read { db in
    if let row = try Row.fetchOne(db, sql: "SELECT * FROM wine WHERE id = ?", arguments: [1]) {
        let name: String = row["name"]
        let color: Color = row["color"]
        print(name, color)
    }
}

是存储在行列中的_bool、Int、String、Date、Swift枚举等。

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

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

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

获取方法

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

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

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

    let players = try Player.fetchAll(db, sql: "SELECT ...") // [Player]
  • fetchSet返回一个集合

    let names = try String.fetchSet(db, sql: "SELECT ...") // Set<String>
  • fetchOne返回一个单个可选值,并消耗一个数据库行(如果有的话)

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

所有这些获取方法都需要一个包含单个SQL语句的SQL字符串。当您想从多个语句获取数据(用分号连接)时,请遍历SQL字符串中找到的多个准备好的语句

游标

📖 游标

您从数据库中获取多行数据时,可以获取数组、集合或游标.

fetchAll()fetchSet()方法返回普通的Swift数组和集合,您可以像迭代其他数组和集合一样迭代它们

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

与数组和集合不同,fetchCursor()返回的游标是逐步加载其结果

try dbQueue.read { db in
    // Cursor of Player
    let players = try Player.fetchCursor(db, sql: "SELECT ...")
    while let player = try players.next() {
        // use player
    }
}
  • 游标不能在任何线程上使用:您必须在使用它的派遣队列上消耗游标。特别是,不要从数据库访问方法中提取游标

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

    相反,数组和集合可以在任何线程上消耗

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

  • 游标以懒加载的方式迭代数据库结果,且消耗的内存很少。数组和集合包含数据库值的副本,并且当有大量的获取结果时,可能需要大量的内存。

  • 游标可以直接访问SQLite,与必须复制数据库值的数组集合不同。如果您在意额外的性能,您可能更喜欢使用游标。

  • 游标可以为Swift集合提供数据。

    当您想获取数组或集合时,通常使用fetchAllfetchSet。对于更具体的需要,您可能更喜欢以下初始化器之一。所有这些初始化器都接受额外的可选参数minimumCapacity,这在您对游标中元素的数量有一定的了解时可以帮助优化您的应用程序(内置的fetchAllfetchSet不执行此类优化)。

    数组和所有符合RangeReplaceableCollection类型的类型

    // [String]
    let cursor = try String.fetchCursor(db, ...)
    let array = try Array(cursor)

    集合:

    // Set<Int>
    let cursor = try Int.fetchCursor(db, ...)
    let set = try Set(cursor)

    字典:

    // [Int64: [Player]]
    let cursor = try Player.fetchCursor(db)
    let dictionary = try Dictionary(grouping: cursor, by: { $0.teamID })
    
    // [Int64: Player]
    let cursor = try Player.fetchCursor(db).map { ($0.id, $0) }
    let dictionary = try Dictionary(uniqueKeysWithValues: cursor)
  • 游标采用 Cursor 协议,这与 Swift 的标准 懒序列 非常相似。 因此,游标提供了许多便捷的方法:compactMapcontainsdropFirstdropLastdrop(while:)enumeratedfilterfirstflatMapforEachjoinedjoined(separator:)maxmax(by:)minmin(by:)mapprefixprefix(while:)reducereduce(into:)suffix

    // Prints all Github links
    try URL
        .fetchCursor(db, sql: "SELECT url FROM link")
        .filter { url in url.host == "github.com" }
        .forEach { url in print(url) }
    
    // An efficient cursor of coordinates:
    let locations = try Row.
        .fetchCursor(db, sql: "SELECT latitude, longitude FROM place")
        .map { row in
            CLLocationCoordinate2D(latitude: row[0], longitude: row[1])
        }
  • 游标不是 Swift 集合。 这是因为 Swift 集合不能处理迭代错误,读 SQLite 结果可能会随时失败。

  • 游标需要一些注意:

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

      // Undefined behavior
      while let player = try players.next() {
          try db.execute(sql: "DELETE ...")
      }
    • 不要将 Row 游标转换为数组或集合。您可能不会得到预期的唯一行。要获取行数组,请使用 Row.fetchAll(...)。要获取行集合,请使用 Row.fetchSet(...)。一般来说,每当您从游标中提取行以供后续使用时,请确保复制该行:row.copy()

如果您看不到或不在乎这些差异,请使用数组。如果在意内存和性能,则在适当的情况下使用游标。

行查询

查询行

检索行 游标数组集合 或单个 (参看 检索方法

try dbQueue.read { db in
    try Row.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Row
    try Row.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Row]
    try Row.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Row>
    try Row.fetchOne(db, sql: "SELECT ...", arguments: ...)    // Row?
    
    let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
    while let row = try rows.next() {
        let name: String = row["name"]
        let color: Color = row["color"]
        print(name, color)
    }
}

let rows = try dbQueue.read { db in
    try Row.fetchAll(db, sql: "SELECT * FROM player")
}

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

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

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

有关支持的参数类型(Bool、Int、String、Date、Swift 枚举等)的更多信息,请参阅 ,有关 SQLite 参数的详细文档,请参阅 StatementArguments

与包含数据库行副本的行数组不同,行游标接近 SQLite 的底层结构,需要一些注意

注意: 不要将Row光标转换为数组或集合。您不会得到预期的唯一行。要获取行的数组,使用Row.fetchAll(...)。要获取行的集合,使用Row.fetchSet(...)。一般来说,每次您从光标中提取行用于后续操作时,确保复制行:row.copy()

列值

通过索引或列名读取列值

let name: String = row[0]      // 0 is the leftmost column
let name: String = row["name"] // Leftmost matching column - lookup is case-insensitive
let name: String = row[Column("name")] // Using query interface's Column

确保当值可能为NULL时请求可选值

let name: String? = row["name"]

row[]下标返回您要求的类型。有关受支持的值类型更多信息,请参阅

let bookCount: Int     = row["bookCount"]
let bookCount64: Int64 = row["bookCount"]
let hasBooks: Bool     = row["bookCount"] // false when 0

let string: String     = row["date"]      // "2015-09-11 18:14:15.123"
let date: Date         = row["date"]      // Date
self.date = row["date"] // Depends on the type of the property.

您也可以使用as类型转换运算符

row[...] as Int
row[...] as Int?

警告:避免使用as!as?运算符

if let int = row[...] as? Int { ... } // BAD - doesn't work
if let int = row[...] as Int? { ... } // GOOD

一般来说,只要可以将其从底层SQLite值转换,您就可以提取所需的类型

  • 成功的转换包括

    • 所有SQLite数值到所有数值Swift类型,以及Bool(零是唯一的布尔值)。
    • 文本SQLite值到Swift String。
    • Blob SQLite值到Foundation Data。

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

  • NULL返回nil。

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

    有一个例外,即DatabaseValue类型

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

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

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

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

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

    可以使用DatabaseValue类型避免这些致命错误

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

    这种额外的冗余是处理不受信任数据库的结果:您可能需要考虑修复数据库的内容。有关更多信息,请参阅致命错误

  • SQLite有一个弱类型系统,并提供便捷转换,例如将String转换为Int,将Double转换为Blob等。

    GRDB有时会允许这些转换进行

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

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

DatabaseValue

📖 数据库值

DatabaseValue 是 SQLite 和您的值之间的中间类型,提供有关数据库中存储的原始值的信息。

您可以通过其他值类型一样的方式来获得 DatabaseValue

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

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

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

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

您可以使用 fromDatabaseValue() 方法从 DatabaseValue 中提取常规 (Bool、Int、String、Date、Swift 枚举等)。

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

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

fromDatabaseValue 在无效转换时返回 nil。

let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
let dbValue: DatabaseValue = row[0]
let string = String.fromDatabaseValue(dbValue) // "Mom’s birthday"
let int    = Int.fromDatabaseValue(dbValue)    // nil
let date   = Date.fromDatabaseValue(dbValue)   // nil

作为字典的行

行实现了标准 RandomAccessCollection 协议,可以看作是一个 DatabaseValue 的字典。

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

您可以从字典构建行(标准 Swift 字典和 NSDictionary)。关于支持的数据类型,请参阅

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

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

let row = try Row.fetchOne(db, sql: "SELECT 1 AS foo, 2 AS foo")!
row.columnNames    // ["foo", "foo"]
row.databaseValues // [1, 2]
row["foo"]         // 1 (leftmost matching column)
for (columnName, dbValue) in row { ... } // ("foo", 1), ("foo", 2)

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

  • 一个 [String: DatabaseValue] 字典,在列名重复时保持最左边的值

    let dict = Dictionary(row, uniquingKeysWith: { (left, _) in left })
  • 一个 [String: AnyObject] 字典,在列名重复时保持最右边的值。该字典与 FMDB 的 FMResultSet 的结果字典相同。它包含 null 列的 NSNull 值,可以与 Objective-C 一起使用

    let dict = Dictionary(
        row.map { (column, dbValue) in
            (column, dbValue.storage.value as AnyObject)
        },
        uniquingKeysWith: { (_, right) in right })
  • 一个 [String: Any] 字典,可以输入到,例如,JSONSerialization

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

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

值查询

📖 DatabaseValueConvertible

您可以直接获取值,而不是行。 支持许多 值类型(Bool、Int、String、Date、Swift 枚举等)。

与行类似,以 游标数组集合单个值(参阅 获取方法)的形式获取值。值从 SQL 查询的左侧列中提取

try dbQueue.read { db in
    try Int.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int
    try Int.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Int]
    try Int.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Int>
    try Int.fetchOne(db, sql: "SELECT ...", arguments: ...)    // Int?
    
    let maxScore = try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") // Int?
    let names = try String.fetchAll(db, sql: "SELECT name FROM player")       // [String]
}

Int.fetchOne 返回 nil 的情况有两种:要么是因为 SELECT 语句没有返回任何行,要么是返回了一行包含 NULL 值的数据。

// No row:
try Int.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // nil

// One row with a NULL value:
try Int.fetchOne(db, sql: "SELECT NULL")           // nil

// One row with a non-NULL value:
try Int.fetchOne(db, sql: "SELECT 42")             // 42

对于可能包含 NULL 的请求,请获取可选值。

try dbQueue.read { db in
    try Optional<Int>.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int?
    try Optional<Int>.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Int?]
    try Optional<Int>.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Int?>
}

💡 提示:在获取一个值时,有一个高级用例,可以区分没有返回任何行或者是返回了一行包含 NULL 值的情况。为此,请使用 Optional.fetchOne,它返回一个双重可选的 Int??

// No row:
try Optional<Int>.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // .none
// One row with a NULL value:
try Optional<Int>.fetchOne(db, sql: "SELECT NULL")           // .some(.none)
// One row with a non-NULL value:
try Optional<Int>.fetchOne(db, sql: "SELECT 42")             // .some(.some(42))

支持许多值类型(Bool、Int、String、Date、Swift 枚举等)。有关更多信息,请参阅

GRDB 预先支持以下值类型

值可以用作 语句参数

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

可以从行中提取值

let rows = try Row.fetchCursor(db, sql: "SELECT * FROM link")
while let row = try rows.next() {
    let url: URL = row["url"]
    let verified: Bool = row["verified"]
}

值可以直接获取。

let urls = try URL.fetchAll(db, sql: "SELECT url FROM link")  // [URL]

记录中使用值。

struct Link: FetchableRecord {
    var url: URL
    var isVerified: Bool
    
    init(row: Row) {
        url = row["url"]
        isVerified = row["verified"]
    }
}

查询界面中使用值。

let url: URL = ...
let link = try Link.filter(Column("url") == url).fetchOne(db)

数据(及内存节省)

Data 适用于 BLOB SQLite 列。它可以像其他值一样存储和从数据库中检索。

let rows = try Row.fetchCursor(db, sql: "SELECT data, ...")
while let row = try rows.next() {
    let data: Data = row["data"]
}

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

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

while let row = try rows.next() {
    try row.withUnsafeData(name: "data") { (data: Data?) in
        ...
    }
}

未复制的数据生命周期不超过迭代步骤:请确保您不会在此之后使用它。

日期与DateComponents

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

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

SQLite格式 日期 DateComponents
YYYY-MM-DD 读 / 写
YYYY-MM-DD HH:MM 1 2 2 / 写
YYYY-MM-DD HH:MM:SS 1 2 2 / 写
YYYY-MM-DD HH:MM:SS.SSS 1 2 / 写 1 2 / 写
YYYY-MM-DD T HH:MM 1 2 2
YYYY-MM-DD T HH:MM:SS 1 2 2
YYYY-MM-DD T HH:MM:SS.SSS 1 2 2
HH:MM 2 / 写
HH:MM:SS 2 / 写
HH:MM:SS.SSS 2 / 写
自Unix纪元以来的时间戳 3
now

¹ 缺失的组件默认为0。日期以UTC时区存储和读取,除非格式后跟时间区域指示符 2

² 此格式可以可选地后跟形式为 [+-]HH:MM 或仅为 Z 的时间区域指示符。

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

警告:SQLite日期格式中有效年份的范围为0000-9999。当您的应用程序需要处理此范围之外年代的年份时,您需要选择另一种日期格式。请参阅以下章节。

日期

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

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

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

日期以“YYYY-MM-DD HH:MM:SS.SSS”格式以UTC时区存储。精度达到毫秒。

注意:选择这种格式是因为它是在

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

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

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

  • 有些可能更喜欢带有 T 分隔符的 ISO-8601
  • 有些可能更喜欢带有时区的 ISO-8601
  • 有些可能需要存储 0000-9999 年份范围之外的年份
  • 有些可能需要存储亚毫秒精度
  • 有些可能需要精确的 Date 返回操作
  • 等等。

在选择不同的日期格式之前请三思

  • ISO-8601 主要是关于 交换和通信 的,而 SQLite 主要是关于 存储和数据操作 的。在数据库和 JSON 文件中使用相同的表示形式只提供一种表面的便利,不应是您的首选优先事项。不要在不了解会损失什么的情况下将日期存储为 ISO-8601。例如,ISO-8601 时区禁止数据库层面的日期比较。
  • 亚毫秒精度和精确的 Date 返回操作看起来并不像初次看到的那样明显。实际上,一旦日期离开您的应用程序,日期通常就不精确地返回,因为与您的应用程序通讯的其他系统使用它们自己的日期表示(您的应用程序的安卓版本、您的应用程序正在与之对话的服务器等)。更重要的是,Date 比较至少和 浮点比较 一样困难和复杂。

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

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

if let row = try Row.fetchOne(db, ...) {
    let timeInterval: TimeInterval = row["creationDate"]
    let creationDate = Date(timeIntervalSinceReferenceDate: timeInterval)
}

有关更多日期自定义选项,请参阅可编码记录,如果您想定义一个具有自定义数据库表示形式的包装日期的类型,请参阅 DatabaseValueConvertible

日期组件

通过DatabaseDateComponents辅助类型间接支持DateComponents。

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

:有效年份范围为0000-9999。您将在这个范围之外遇到问题,例如解码错误,或者使用SQLite日期和时间函数进行无效的日期计算。有关更多信息,请参阅日期

DatabaseDateComponents可以像其他一样存入和从数据库检索。

let components = DateComponents()
components.year = 1973
components.month = 9
components.day = 18

// Store "1973-09-18"
let dbComponents = DatabaseDateComponents(components, format: .YMD)
try db.execute(
    sql: "INSERT INTO player (birthDate, ...) VALUES (?, ...)",
    arguments: [dbComponents, ...])

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

NSNumber、NSDecimalNumber和Decimal

NSNumberDecimal可以像其他一样存入和从数据库检索。

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

整数 双精度浮点数 字符串
NSNumber 读 / 写 读 / 写 读取
NSDecimalNumber 读 / 写 读 / 写 读取
Decimal 读取 读取 读 / 写
  • 所有三种类型都可以解码数据库整数和双精度浮点数

    let number = try NSNumber.fetchOne(db, sql: "SELECT 10")            // NSNumber
    let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT 1.23")   // NSDecimalNumber
    let number = try Decimal.fetchOne(db, sql: "SELECT -100")           // Decimal
  • 所有三种类型将数据库字符串解码为十进制数字

    let number = try NSNumber.fetchOne(db, sql: "SELECT '10'")          // NSDecimalNumber (sic)
    let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT '1.23'") // NSDecimalNumber
    let number = try Decimal.fetchOne(db, sql: "SELECT '-100'")         // Decimal
  • NSNumberNSDecimalNumber将64位有符号整数和双精度浮点数发送到数据库

    // INSERT INTO transfer VALUES (10)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10)])
    
    // INSERT INTO transfer VALUES (10.0)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10.0)])
    
    // INSERT INTO transfer VALUES (10)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.0")])
    
    // INSERT INTO transfer VALUES (10.5)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.5")])

    警告:由于SQLite不支持十进制数字,发送非整数的NSDecimalNumber在转换为双精度浮点数过程中可能会丢失精度。

    与数据库发送非整数的NSDecimalNumber相比,您可能更喜欢

    • 发送Decimal而不是(那些在数据库中以十进制字符串形式存储)。
    • 发送整数而不是(例如,存储欧元的金额而不是分)。
  • Decimal在数据库中以十进制字符串形式发送

    // INSERT INTO transfer VALUES ('10')
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(10)])
    
    // INSERT INTO transfer VALUES ('10.5')
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(string: "10.5")!])

UUID

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(
    sql: "INSERT INTO wine (grape, color) VALUES (?, ?)",
    arguments: [Grape.merlot, Color.red])

// Read
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
while let row = try rows.next() {
    let grape: Grape = row["grape"]
    let color: Color = row["color"]
}

当数据库值与任何枚举案例不匹配时,你会得到一个致命错误。可以使用 DatabaseValue 类型来避免这个致命错误。

let row = try Row.fetchOne(db, sql: "SELECT 'syrah'")!

row[0] as String  // "syrah"
row[0] as Grape?  // fatal error: could not convert "syrah" to Grape.
row[0] as Grape   // fatal error: could not convert "syrah" to Grape.

let dbValue: DatabaseValue = row[0]
if dbValue.isNull {
    // Handle NULL
} else if let grape = Grape.fromDatabaseValue(dbValue) {
    // Handle valid grape
} else {
    // Handle unknown grape
}

自定义SQL函数和聚合函数

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

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

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

自定义SQL函数

📖 DatabaseFunction

一个 函数 参数接受一个 DatabaseValue 数组,并返回任何有效的 (Bool,Int,String,Date,Swift枚举等)。数据库值的数量保证为 argumentCount

当函数是 "纯" 时,SQLite 有机会在函数上执行额外的优化,这意味着结果仅取决于其参数。所以在可能的情况下,请将 pure 参数设置为 true。

let reverse = DatabaseFunction("reverse", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
    // Extract string value, if any...
    guard let string = String.fromDatabaseValue(values[0]) else {
        return nil
    }
    // ... and return reversed string:
    return String(string.reversed())
}

你通过其配置将函数设置可用给数据库连接

var config = Configuration()
config.prepareDatabase { db in
    db.add(function: reverse)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

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

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

当你不提供任何明确的 argumentCount 时,函数可以接受任何数量的参数

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

// 2.0
try Double.fetchOne(db, sql: "SELECT averageOf(1, 2, 3)")!

函数可以抛出异常

let sqrt = DatabaseFunction("sqrt", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
    guard let double = Double.fromDatabaseValue(values[0]) else {
        return nil
    }
    guard double >= 0 else {
        throw DatabaseError(message: "invalid negative number")
    }
    return sqrt(double)
}
db.add(function: sqrt)

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

在使用 查询接口 时使用自定义函数

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

GRDB 随带内置SQL函数,执行Unicode感知的字符串转换。 请参阅 Unicode

自定义聚合函数

📖 DatabaseFunction, DatabaseAggregate

在注册自定义聚合函数之前,需要定义一个遵循 DatabaseAggregate 协议的类型

protocol DatabaseAggregate {
    // Initializes an aggregate
    init()
    
    // Called at each step of the aggregation
    mutating func step(_ dbValues: [DatabaseValue]) throws
    
    // Returns the final result
    func finalize() throws -> DatabaseValueConvertible?
}

例如

struct MaxLength : DatabaseAggregate {
    var maxLength: Int = 0
    
    mutating func step(_ dbValues: [DatabaseValue]) {
        // At each step, extract string value, if any...
        guard let string = String.fromDatabaseValue(dbValues[0]) else {
            return
        }
        // ... and update the result
        let length = string.count
        if length > maxLength {
            maxLength = length
        }
    }
    
    func finalize() -> DatabaseValueConvertible? {
        maxLength
    }
}

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

类似于自定义 SQL 函数,您可以通过配置使得聚合函数对数据库连接可用

var config = Configuration()
config.prepareDatabase { db in
    db.add(function: maxLength)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

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

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

聚合函数的 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(...)

有关更多信息,请参阅 tableExists(_:) 和相关方法。

原始 SQLite 指针

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

这些函数直接嵌入到 GRDB 模块中,无论底层 SQLite 实现是什么(系统 SQLite,SQLCipher自定义 SQLite 构建

import GRDB

let sqliteVersion = String(cString: sqlite3_libversion())

数据库连接和语句的原始指针通过 Database.sqliteConnectionStatement.sqliteStatement 属性可用

try dbQueue.read { db in
    // The raw pointer to a database connection:
    let sqliteConnection = db.sqliteConnection

    // The raw pointer to a statement:
    let statement = try db.makeStatement(sql: "SELECT ...")
    let sqliteStatement = statement.sqliteStatement
}

注意

  • 这些指针归 GRDB 所有:不要关闭 GRDB 创建的连接或最终化语句。
  • GRDB在"多线程模式"下打开SQLite连接,这(有些奇怪)意味着它们**不是线程安全的**。确保您在它们专属的派发队列中触摸原始数据库和语句。
  • 请自行承担使用原始SQLite C接口的风险。GRDB不会阻止您自找麻烦。

记录

GRDB在SQLite API的基础上,提供了一些协议和一个类,这些可以帮助以名为“记录”的常规对象的形式操作数据库行。

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

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

要定义您的自定义记录,您可以创建现成的Record类的子类,或者通过包含一系列定向功能的协议扩展您的结构体和类,这些功能包括:获取方法、持久化方法、记录比较等。

使用记录协议扩展结构体更加“Swift式”。子类化Record类更加“传统”。您可以任选其一。查看一些记录定义的示例以及记录方法列表以了解概览。

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

💡 技巧:阅读本章后,请查看设计记录类型的推荐做法指南。

💡 技巧:参阅演示应用程序,了解使用记录的示例应用程序。

概述

协议和记录类

快速了解记录

插入记录

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

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

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

检索记录

要从一个数据库中检索记录,调用一个 检索方法

let arthur = try Player.fetchOne(db,            // Player?
    sql: "SELECT * FROM players WHERE name = ?",
    arguments: ["Arthur"])

let bestPlayers = try Player                    // [Player]
    .order(Column("score").desc)
    .limit(10)
    .fetchAll(db)
    
let spain = try Country.fetchOne(db, id: "ES")  // Country?
let italy = try Country.find(db, id: "IT")      // Country

👉对于使用原始 SQL 进行检索的 Record 类的子类和采用 FetchableRecord 协议的类型,可以使用此方法。

👉对于不使用 SQL,而是使用 查询接口 进行检索,适用于 Record 类的子类以及同时采用 FetchableRecordTableRecord 协议的类型。

更新记录

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

var player: Player = ...
player.score = 1000
try player.update(db)

可以避免无效的更新操作

// does not hit the database if score has not changed
try player.updateChanges(db) {
    $0.score = 1000
}

查看 查询接口 以进行批量更新

try Player
    .filter(Column("team") == "red")
    .updateAll(db, Column("score") += 1)

👉更新方法适用于 Record 类的子类以及采用 PersistableRecord 协议的类型。批量更新在 TableRecord 协议中可用。

删除记录

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

let player: Player = ...
try player.delete(db)

您还可以通过主键、唯一键进行删除,或者执行批量删除(见删除请求

try Player.deleteOne(db, id: 1)
try Player.deleteOne(db, key: ["email": "[email protected]"])
try Country.deleteAll(db, ids: ["FR", "US"])
try Player
    .filter(Column("email") == nil)
    .deleteAll(db)

👉删除方法适用于Record类和遵循 PersistableRecord 协议的类型。批量删除适用于 TableRecord 协议。

记录计数

要计数记录,请调用 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: FetchableRecord { ... }
    let places = try dbQueue.read { db in
        try Place.fetchAll(db, sql: "SELECT * FROM place")
    }

    💡 提示FetchableRecord 可以从标准的 Decodable 协议继承其实现。有关更多信息,请参阅可编码记录

    FetchableRecord 可以解码数据库行,但不能为您构建 SQL 请求。为此,您还需要 TableRecord

  • TableRecord 能够 生成 SQL 查询

    struct Place: TableRecord { ... }
    let placeCount = try dbQueue.read { db in
        // Generates and runs `SELECT COUNT(*) FROM place`
        try Place.fetchCount(db)
    }

    当一个类型采用 TableRecordFetchableRecord 时,它可以加载这些请求。

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

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

    持久化记录还可以与其他记录进行比较,以避免无效的数据库更新。

    💡 提示PersistableRecord 可以从标准的 Encodable 协议继承其实现。有关更多信息,请参阅可编码记录

FetchableRecord 协议

📖 FetchableRecord

FetchableRecord 协议为任何可以从数据库行创建的类型提供了获取方法

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

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

struct Place {
    var id: Int64?
    var title: String
    var coordinate: CLLocationCoordinate2D
}

extension Place : FetchableRecord {
    init(row: Row) {
        id = row["id"]
        title = row["title"]
        coordinate = CLLocationCoordinate2D(
            latitude: row["latitude"],
            longitude: row["longitude"])
    }
}

行也接受列枚举

extension Place : FetchableRecord {
    enum Columns: String, ColumnExpression {
        case id, title, latitude, longitude
    }
    
    init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
    }
}

查看 列值 了解关于 row[] 索引的更多信息。

当你的记录类型采用标准的 Decodable 协议时,你不需要提供 init(row:) 的实现。有关更多信息,请参阅 Codable Records

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

FetchableRecord 允许采用的类型可以从 SQL 查询中提取。

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

关于 fetchCursorfetchAllfetchSetfetchOne 方法的更多信息,请参阅 fetching methods。有关查询参数的更多信息,请参阅 StatementArguments

注意:出于性能考虑,在检索查询迭代期间重复使用相同的行参数给 init(row:)。如果你想稍后使用行,请确保存储一个副本:self.row = row.copy()

注意FetchableRecord.init(row:) 构造器适用于大多数应用需求。但是,某些应用的需求比其他应用更高。当 FetchableRecord 不能提供你所需的确切支持时,请参阅 超越 FetchableRecord 章节。

TableRecord 协议

📖 TableRecord

TableRecord 协议 为你生成 SQL。要使用 TableRecord,可以子类化 Record 类,或者显式采用它。

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

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

databaseTableName 类型属性是数据库表的名字。默认情况下,它是从类型名派生的。

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

例如

  • 地点: place
  • 国家: country
  • 邮寄地址: postalAddress
  • 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 { "place" }
}
print(Place.databaseTableName) // prints "place"

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

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

TableRecord 还可以处理主键和唯一键的检索:请参阅 按键检索检查记录是否存在

持久化记录协议

📖 EncodableRecordMutablePersistableRecordPersistableRecord

GRDB的记录类型可以在数据库中创建、更新和删除行。

这些能力由三个协议赋予

// Defines how a record encodes itself into the database
protocol EncodableRecord {
    /// Defines the values persisted in the database
    func encode(to container: inout PersistenceContainer) throws
}

// Adds persistence methods
protocol MutablePersistableRecord: TableRecord, EncodableRecord {
    /// Optional method that lets your adopting type store its rowID upon
    /// successful insertion. Don't call it directly: it is called for you.
    mutating func didInsert(_ inserted: InsertionSuccess)
}

// Adds immutability
protocol PersistableRecord: MutablePersistableRecord {
    /// Non-mutating version of the optional didInsert(_:)
    func didInsert(_ inserted: InsertionSuccess)
}

是的,三个协议而不是一个。以下是选择其中一个的方法

  • 如果你的类型是类,选择 PersistableRecord。在此基础上,如果数据库表有自增主键,则实现 didInsert(_:)

  • 如果你类型是结构体,并且数据库表有自增主键,选择 MutablePersistableRecord 并实现 didInsert(_:)

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

encode(to:) 方法定义了哪些 (布尔值、整数、字符串、日期、Swift枚举等)将被分配给数据库列。

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

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

extension Place : MutablePersistableRecord {
    /// The values persisted in the database
    func encode(to container: inout PersistenceContainer) {
        container["id"] = id
        container["title"] = title
        container["latitude"] = coordinate.latitude
        container["longitude"] = coordinate.longitude
    }
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var paris = Place(
    id: nil,
    title: "Paris",
    coordinate: CLLocationCoordinate2D(latitude: 48.8534100, longitude: 2.3488000))

try paris.insert(db)
paris.id   // some value

持久化容器也接受列枚举

extension Place : MutablePersistableRecord {
    enum Columns: String, ColumnExpression {
        case id, title, latitude, longitude
    }
    
    func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
}

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

// That's all
struct Player: Encodable, MutablePersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

持久化方法

Record 子类以及采用 PersistableRecord 的类型具有插入、更新和删除的方法

// INSERT
try place.insert(db)
let insertedPlace = try place.inserted(db) // non-mutating

// UPDATE
try place.update(db)
try place.update(db, columns: ["title"])

// Maybe UPDATE
try place.updateChanges(db, from: otherPlace)
try place.updateChanges(db) { $0.isFavorite = true }
try place.updateChanges(db) // Record class only

// INSERT or UPDATE
try place.save(db)
let savedPlace = place.saved(db) // non-mutating

// UPSERT
try place.upsert(db)
let insertedPlace = place.upsertAndFetch(db)

// DELETE
try place.delete(db)

// EXISTENCE CHECK
let exists = try place.exists(db)

更多关于 upsert 的信息,请参阅以下内容:Upsert

TableRecord 协议自带批量操作:

// UPDATE
try Place.updateAll(db, ...)

// DELETE
try Place.deleteAll(db)
try Place.deleteAll(db, ids:...)
try Place.deleteAll(db, keys:...)
try Place.deleteOne(db, id:...)
try Place.deleteOne(db, key:...)

有关批量更新的更多信息,请参阅以下内容:Update Requests

  • 所有持久化方法都可以抛出 DatabaseError

  • 如果数据库不包含记录的主键对应的行,则 updateupdateChanges 会抛出 RecordError

  • save 确保您的值被存储在数据库中。如果记录有一个非空的主键,则执行 UPDATE;如果没有修改的行,则执行 INSERT。如果记录没有主键或主键为空,则直接执行 INSERT。

  • deletedeleteOne 返回是否删除了数据库行。 deleteAll 返回已删除的行数。updateAll 返回已更新的行数。updateChanges 返回是否更新了数据库行。

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

要自定义持久化方法,您需要提供持久化回调,如下文所述。请不要尝试覆盖现成的持久化方法。

Upsert操作

UPSERT 是 SQLite 的一项功能,当插入操作违反唯一性约束(主键或唯一索引)时,该操作将表现为更新操作或无操作。

注意:Upsert API 从 SQLite 3.35.0+ 可用,iOS 15.0+、macOS 12.0+、tvOS 15.0+、watchOS 8.0+ 或通过 自定义 SQLite 构建SQLCipher 提供的支持。

注意:关于 持久化回调,Upsert 操作的行为与插入操作完全一致。特别是:执行 aroundInsert(_:)didInsert(_:) 回调时会报告插入或更新的行ID; willUpdatearoundUdatedidUdate 不会调用。

PersistableRecord 提供了三种 Upsert 方法

  • upsert(_:)

    插入或更新记录。

    当违反表上的任何唯一性约束(主键或唯一索引)时触发行插入行为。在冲突情况下,除了主键外的所有列都将被插入值覆盖

    struct Player: Encodable, PersistableRecord {
        var id: Int64
        var name: String
        var score: Int
    }
    
    // INSERT INTO player (id, name, score)
    // VALUES (1, 'Arthur', 1000)
    // ON CONFLICT DO UPDATE SET
    //   name = excluded.name,
    //   score = excluded.score
    let player = Player(id: 1, name: "Arthur", score: 1000)
    try player.upsert(db)
  • upsertAndFetch(_:onConflict:doUpdate:) (需要 FetchableRecord 协议)

    插入或更新记录,并返回 Upserted 记录。

    onConflictdoUpdate 参数允许您进一步控制 Upsert 行为。请确保您查阅了SQLite UPSERT 文档,以了解详细信息。

    • onConflict:冲突“目标”是触发 Upsert 的唯一性约束(主键或唯一索引)中的列数组。

      如果为空(默认值),则考虑所有唯一性约束。

    • doUpdate:在发生冲突时执行列分配的闭包。其他列使用插入值覆盖。

      默认情况下,所有插入列(除主键和冲突目标外)都被覆盖。

    以下示例中,我们将新的词汇“jovial”更新。如果该词汇尚不在字典中,则将其插入。否则,将递增countisTainted不会被覆盖,而kind会被覆盖。

    // CREATE TABLE vocabulary(
    //   word TEXT NOT NULL PRIMARY KEY,
    //   kind TEXT NOT NULL,
    //   isTainted BOOLEAN DEFAULT 0,
    //   count INT DEFAULT 1))
    struct Vocabulary: Encodable, PersistableRecord {
        var word: String
        var kind: String
        var isTainted: Bool
    }
    
    // INSERT INTO vocabulary(word, kind, isTainted)
    // VALUES('jovial', 'adjective', 0)
    // ON CONFLICT(word) DO UPDATE SET \
    //   count = count + 1,   -- on conflict, count is incremented
    //   kind = excluded.kind -- on conflict, kind is overwritten
    // RETURNING *
    let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
    let upserted = try vocabulary.upsertAndFetch(
        db, onConflict: ["word"],
        doUpdate: { _ in
            [Column("count") += 1,            // on conflict, count is incremented
             Column("isTainted").noOverwrite] // on conflict, isTainted is NOT overwritten
        })

    doUpdate闭包接受一个指向引发冲突的插入值的excluded TableAlias参数。您可以使用它来指定显式的覆盖,或执行计算。以下示例中,upsert在发生冲突时保留最大日期。

    // INSERT INTO message(id, text, date)
    // VALUES(...)
    // ON CONFLICT DO UPDATE SET \
    //   text = excluded.text,
    //   date = MAX(date, excluded.date)
    // RETURNING *
    let upserted = try message.upsertAndFetch(doUpdate: { excluded in
        // keep the maximum date in case of conflict
        [Column("date").set(to: max(Column("date"), excluded["date"]))]
    })
  • upsertAndFetch(_:as:onConflict:doUpdate:)(不需要实现FetchableRecord协议)

    此方法与上面描述的upsertAndFetch(_:onConflict:doUpdate:)相同,但您可以提供一种独特的FetchableRecord记录类型作为结果,从而指定返回的列。

持久化方法和《returning》子句

SQLite可以返回插入、更新或删除行的值,使用returning子句

注意:从SQLite 3.35.0+开始支持returning子句,iOS 15.0+、macOS 12.0+、tvOS 15.0+、watchOS 8.0+,或通过自定义SQLite构建SQLCipher

returning子句有助于处理数据库特征,如自动增长的id、默认值和生成列。例如,您可以在单个步骤中插入几个列并检索默认或生成的列。

GRDB在所有包含AndFetch的名称持久化方法中使用returning子句。

例如,给定一个具有自增主键和默认分数的数据库表

try dbQueue.write { db in
    try db.execute(sql: """
        CREATE TABLE player(
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          score INTEGER NOT NULL DEFAULT 1000)
        """)
}

您可以定义包含完整数据库信息的记录类型和另一个部分记录类型,该类型处理列的子集

// A player with full database information
struct Player: Codable, PersistableRecord, FetchableRecord {
    var id: Int64
    var name: String
    var score: Int
}

// A partial player
struct PartialPlayer: Encodable, PersistableRecord {
    static let databaseTableName = "player"
    var name: String
}

现在您可以插入一个部分记录以获得完整的玩家信息

try dbQueue.write { db in
    let partialPlayer = PartialPlayer(name: "Alice")
    
    // INSERT INTO player (name) VALUES ('Alice') RETURNING *
    if let player = try partialPlayer.insertAndFetch(db, as: Player.self) {
        print(player.id)    // The inserted id
        print(player.name)  // The inserted name
        print(player.score) // The default score
    }
}

为了更高的精确度,您可以选择所需的所有列,并从提供的已准备的环境中获取所需的值Statement

try dbQueue.write { db in
    let partialPlayer = PartialPlayer(name: "Alice")
    
    // INSERT INTO player (name) VALUES ('Alice') RETURNING score
    let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
        try Int.fetchOne(statement)
    }
    print(score) // Prints 1000, the default score
}

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

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

请参阅持久化方法upsertupdateChanges方法获取更多信息。

批处理操作可以返回更新或删除的值。

警告:请确保检查RETURNING子句的文档(链接:https://www.sqlite.org/lang_returning.html),它描述了批量操作的重要限制和注意事项。

let request = Player.filter(...)...

// Fetch all deleted players
// DELETE FROM player RETURNING *
let deletedPlayers = try request.deleteAndFetchAll(db) // [Player]

// Fetch a selection of columns from the deleted rows
// DELETE FROM player RETURNING name
let statement = try request.deleteAndFetchStatement(db, selection: [Column("name")])
let deletedNames = try String.fetchSet(statement)

// Fetch all updated players
// UPDATE player SET score = score + 10 RETURNING *
let updatedPlayers = try request.updateAndFetchAll(db, [Column("score") += 10]) // [Player]

// Fetch a selection of columns from the updated rows
// UPDATE player SET score = score + 10 RETURNING score
let statement = try request.updateAndFetchStatement(
    db, [Column("score") += 10],
    select: [Column("score")])
let updatedScores = try Int.fetchAll(statement)

持久化回调

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

为此,您的记录类型可以实现持久化回调。回调是在记录生命周期中的某些时刻被调用的方法。使用回调可以在记录插入、更新或删除时运行代码。

为了使用回调方法,您需要提供其实现。例如,常用的回调是didInsert,用于自动增量数据库ID的情况。

struct Player: MutablePersistableRecord {
    var id: Int64?
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

try dbQueue.write { db in
    var player = Player(id: nil, ...)
    try player.insert(db)
    print(player.id) // didInsert was called: prints some non-nil id
}

当您继承Record类时,覆盖回调并在实现中某些点调用super方法。

class Player: Record {
    var id: Int64?
    
    // Update auto-incremented id upon successful insertion
    func didInsert(_ inserted: InsertionSuccess) {
        super.didInsert(inserted)
        id = inserted.rowID
    }
}

回调还可以帮助实现记录验证。

struct Link: PersistableRecord {
    var url: URL
    
    func willSave(_ db: Database) throws {
        if url.host == nil {
            throw ValidationError("url must be absolute.")
        }
    }
}

try link.insert(db) // Calls the willSave callback
try link.update(db) // Calls the willSave callback
try link.save(db)   // Calls the willSave callback
try link.upsert(db) // Calls the willSave callback

可用的回调

以下是所有可用持久化回调的列表,以相应操作中它们将被调用的顺序列出:

  • 插入记录(所有record.insertrecord.upsert方法)

    • willSave
    • aroundSave
    • willInsert
    • aroundInsert
    • didInsert
    • didSave
  • 更新记录(所有record.update方法)

    • willSave
    • aroundSave
    • willUpdate
    • aroundUpdate
    • didUpdate
    • didSave
  • 删除记录(仅record.delete(_:)方法)

    • willDelete
    • aroundDelete
    • didDelete

有关每个回调的详细信息,请参阅参考

MutablePersistableRecord协议中,willInsertdidInsert是会变更的方法。在PersistableRecord中,它们不是会变更的。

注意事项: 当 record.save(_:) 方法执行时,如果记录具有非空的主键,则会执行 UPDATE,如果没有任何行被修改,则再进行 INSERT。如果记录没有主键或者主键为空,则直接执行 INSERT。根据需要触发更新和/或插入回调。

警告: 只有在调用记录实例上的持久化方法时才会调用回调。当你调用类型方法、执行批量操作或执行原始 SQL 时,回调不会被调用。

警告: 当调用 did*** 回调时,不要假设更改实际上已持久保存在磁盘上,因为数据库可能仍然处于未提交的事务中。当需要处理事务完成时,请使用 afterNextTransaction(onCommit:onRollback:)。例如

struct PictureFile: PersistableRecord {
    var path: String
    
    func willDelete(_ db: Database) {
        db.afterNextTransaction { _ in
            try? deleteFileOnDisk()
        }
    }
}

可识别的记录

当记录类型将具有单个列主键的表进行映射时,建议使其采用标准的 Identifiable 协议。

struct Player: Identifiable, FetchableRecord, PersistableRecord {
    var id: Int64 // fulfills the Identifiable requirement
    var name: String
    var score: Int
}

id 具有与数据库兼容的类型(Int64,Int,String,UUID,...)时,Identifiable 兼容性可以解锁类型安全的记录和请求方法

let player = try Player.find(db, id: 1)               // Player
let player = try Player.fetchOne(db, id: 1)           // Player?
let players = try Player.fetchAll(db, ids: [1, 2, 3]) // [Player]
let players = try Player.fetchSet(db, ids: [1, 2, 3]) // Set<Player>

let request = Player.filter(id: 1)
let request = Player.filter(ids: [1, 2, 3])

try Player.deleteOne(db, id: 1)
try Player.deleteAll(db, ids: [1, 2, 3])

注意事项: Identifiable 并不在所有应用程序目标上可用,也并非所有表都有单列主键。GRDB 提供其他处理主键和唯一键的方法,但它们不会检查它们参数的类型

// Those methods are not type-checked
try Player.fetchOne(db, key: 1)
try Player.fetchOne(db, key: ["email": "[email protected]"])
try Country.fetchAll(db, keys: ["FR", "US"])
try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"])

let request = Player.filter(key: 1)
let request = Player.filter(keys: [1, 2, 3])

try Player.deleteOne(db, key: 1)
try Player.deleteAll(db, keys: [1, 2, 3])

一些数据库表有一个单列主键,但它不被称作 "id"。

try db.create(table: "country") { t in
    t.primaryKey("isoCode", .text)
    t.column("name", .text).notNull()
    t.column("population", .integer).notNull()
}

在这种情况下,可以通过例如从 id 属性返回主键列来实现 Identifiable 兼容性。

struct Country: Identifiable, FetchableRecord, PersistableRecord {
    var isoCode: String
    var name: String
    var population: Int
    
    // Fulfill the Identifiable requirement
    var id: String { isoCode }
}

let france = try dbQueue.read { db in
    try Country.fetchOne(db, id: "FR")
}

可编码记录

采用档案协议(Codable, Encodable 或 Decodable)的记录类型,通过声明符合所需的记录协议即可获得免费的数据库支持

// Declare a record...
struct Player: Codable, FetchableRecord, PersistableRecord {
    var name: String
    var score: Int
}

// ...and there you go:
try dbQueue.write { db in
    try Player(name: "Arthur", score: 100).insert(db)
    let players = try Player.fetchAll(db)
}

可编码记录根据自己实现的 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 的标准 JSONDecoderJSONEncoder。默认情况下,Data 值使用 .base64 策略处理,Date 使用 .millisecondsSince1970 策略,非符合标准的浮点数使用 .throw 策略。

您可以通过实现这些方法自定义 JSON 格式

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

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

💡 提示:请确保设置 JSONEncoder 的 sortedKeys 选项。此选项确保 JSON 输出是稳定的。这种稳定性对于 记录比较 正常工作以及数据库观察工具(如 ValueObservation)准确识别更改的记录是必需的。

列名编码策略

默认情况下,可编码记录将它们的值存储到与编码键匹配的数据库列中:将teamID属性存储到teamID列中。

此行为可以被覆盖,例如,可以将teamID属性存储到team_id列中。

protocol FetchableRecord {
    static var databaseColumnDecodingStrategy: DatabaseColumnDecodingStrategy { get }
}

protocol EncodableRecord {
    static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
}

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

日期和UUID编码策略

默认情况下,可编码记录按照通用的日期和日期组件UUID章节中所述,对其日期和UUID属性进行编码和解析。

总结一下:日期以“YYYY-MM-DD HH:MM:SS.SSS”的格式进行编码,在UTC时区,并进行多种日期格式和时间的解析。UUID以16字节数据块的形式进行编码,解码包括16字节数据块和如“E621E1F8-C36C-495A-93FC-0C247A3E6E5F”的字符串。

这些行为可以被覆盖

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

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

要了解所有可用策略,请参阅DatabaseDateDecodingStrategyDatabaseDateEncodingStrategyDatabaseUUIDEncodingStrategy

无法自定义UUID解码,因为UUID可以解码其所有已编码变体(16字节数据块和uuid字符串,大小写均可)。

自定义日期和UUID处理适用于

  • 在将数据库行编码和解码为记录(获取和持久性方法)时。
  • 在基于单个列主键的请求中:例如fetchOne(_:id:)filter(id:)deleteAll(_:keys:)等。

它们不适用于基于日期或UUID值的其他请求。

因此,请确保在请求中正确地对日期和UUID进行编码。例如

struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable {
    // UUIDs are stored as strings
    static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
    var id: UUID
    ...
}

try dbQueue.write { db in
    let uuid = UUID()
    let player = Player(id: uuid, ...)
    
    // OK: inserts a player in the database, with a string uuid
    try player.insert(db)
    
    // OK: performs a string-based query, finds the inserted player
    _ = try Player.filter(id: uuid).fetchOne(db)

    // NOT OK: performs a blob-based query, fails to find the inserted player
    _ = try Player.filter(Column("id") == uuid).fetchOne(db)
    
    // OK: performs a string-based query, finds the inserted player
    _ = try Player.filter(Column("id") == uuid.uuidString).fetchOne(db)
}

userInfo 字典

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

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

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

protocol EncodableRecord {
    static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
}

例如,这里是一个自定义解码的 Player 类型:

// A key that holds a decoder's name
let decoderName = CodingUserInfoKey(rawValue: "decoderName")!

struct Player: FetchableRecord, Decodable {
    init(from decoder: Decoder) throws {
        // Print the decoder name
        let decoderName = decoder.userInfo[decoderName] as? String
        print("Decoded from \(decoderName ?? "unknown decoder")")
        ...
    }
}

您可以指定从 JSON...

// prints "Decoded from JSON"
let decoder = JSONDecoder()
decoder.userInfo = [decoderName: "JSON"]
let player = try decoder.decode(Player.self, from: jsonData)

...Lastly, from database rows

extension Player: FetchableRecord {
    static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "database row"]
}

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

Note: 确保 databaseDecodingUserInfodatabaseEncodingUserInfo 属性显式地声明为 [CodingUserInfoKey: Any]。如果没有声明,Swift 编译器可能静默地遗漏协议要求,导致 userInfo 为空。

提示:从编码键派生列

可编码类型授予了一个 CodingKeys 枚举。您可以使用它们安全地定义数据库列

struct Player: Codable {
    var id: Int64
    var name: String
    var score: Int
}

extension Player: FetchableRecord, PersistableRecord {
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let name = Column(CodingKeys.name)
        static let score = Column(CodingKeys.score)
    }
}

有关更多信息,请参阅 查询接口设计记录类型建议的最佳实践

记录类

Record 是一个设计中用来继承的类。它通过 FetchableRecord,TableRecord 和 PersistableRecord 协议继承其功能。在此基础上,Record 实例可以通过 避免不必要的更新 来比较它们与自己以前的版本。

Record 子类通过重写数据库方法定义自己的数据库关系。例如

class Place: Record {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
    
    init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) {
        self.id = id
        self.title = title
        self.isFavorite = isFavorite
        self.coordinate = coordinate
        super.init()
    }
    
    /// The table name
    override class var databaseTableName: String { "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) throws {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.favorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
        try super.init(row: row)
    }
    
    /// The values persisted in the database
    override func encode(to container: inout PersistenceContainer) throws {
        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(_ inserted: InsertionSuccess) {
        super.didInsert(inserted)
        id = inserted.rowID
    }
}

记录比较

采用可编码记录协议的记录可以与其他记录或与其之前版本进行比较。

这有助于避免记录未编辑时成本高昂的UPDATE语句。

updateChanges方法

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

  • updateChanges(_:from:)

    此方法允许您比较两个记录

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

    此方法允许您原地更新记录

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

    Record类的实例能够与自身进行比较,并知道自上次获取或保存以来是否有未保存的更改

    // Record class only
    if let player = try Player.fetchOne(db, id: 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)
}

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

databaseChangeshasDatabaseChanges 方法

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

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

Record 类能够与自身进行比较

// Record class only
let player = Player(id: 1, name: "Arthur", score: 100)
try player.insert(db)
player.score = 1000
for (column, oldValue) in try 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
try player.databaseChanges // ["score": 750]

有关将数据库表的合约内容与 JSON 有效负载同步的高效算法,请参阅 groue/SortedDifference

Record 自定义选项

GRDB 的 Record 包含许多默认行为,旨在适应大多数情况。其中许多默认行为可以根据您特定的需求进行自定义

Codable Record 一些额外的选项

冲突解决

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

这些冲突通常以错误结束。然而,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(sql: "INSERT INTO player (email) VALUES (?)", arguments: ["[email protected]"])
    try db.execute(sql: "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(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["[email protected]"])
    try db.execute(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["[email protected]"])

当您想在查询级别处理冲突时,指定采用 PersistableRecord 协议的类型中的自定义 persistenceConflictPolicy。这将改变 insertupdatesave 持久化方法 执行的 INSERT 和 UPDATE 查询。

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

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

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

注意:如果您为插入指定了“忽略”策略,则在失败插入的情况下,didInsert 回调将以一些随机 ID 被调用。您可以使用 insertAndFetch 检测失败的插入。

// How to detect failed `INSERT OR IGNORE`:
// INSERT OR IGNORE INTO player ... RETURNING *
if let insertedPlayer = try player.insertAndFetch(db) {
    // Succesful insertion
} else {
    // Ignored failure
}

注意:“替换”策略可能需要删除行以使插入和更新成功。这些删除不会报告给 事务观察器(这可能在 SQLite 的未来版本中有所改变)。

FetchableRecord 之外

一些 GRDB 用户最终发现 FetchableRecord 协议 并不适合所有场景。 没有很好地由 FetchableRecord 处理的用例包括

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

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

由于这些用例没有很好地通过 FetchableRecord 处理,因此请勿尝试在此协议之上实现它们:这将只是与框架作斗争。

记录定义示例

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

try dbQueue.write { db in
    try db.create(table: "place") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("title", .text).notNull()
        t.column("isFavorite", .boolean).notNull().defaults(to: false)
        t.column("longitude", .double).notNull()
        t.column("latitude", .double).notNull()
    }
}

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

定义一个Codable结构体,并实现所需的记录协议

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

请参阅记录协议概述Codable记录获取更多信息。

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

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let title = Column(CodingKeys.title)
        static let isFavorite = Column(CodingKeys.isFavorite)
        static let latitude = Column(CodingKeys.latitude)
        static let longitude = Column(CodingKeys.longitude)
    }
}

// Fetching methods
extension Place: FetchableRecord { }

// Persistence methods
extension Place: MutablePersistableRecord {
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
定义一个普通的结构体,并实现所需的记录协议

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

struct Place {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
}

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, isFavorite, latitude, longitude
    }
}

// Fetching methods
extension Place: FetchableRecord {
    /// Creates a record from a database row
    init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.isFavorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
    }
}

// Persistence methods
extension Place: MutablePersistableRecord {
    /// The values persisted in the database
    func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.isFavorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
定义一个针对检索性能优化的普通结构体

此结构体从标准Encodable协议继承其持久化方法(请参阅Codable记录),但通过使用数字索引访问数据库列来执行优化的行解码。

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

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

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let title = Column(CodingKeys.title)
        static let isFavorite = Column(CodingKeys.isFavorite)
        static let latitude = Column(CodingKeys.latitude)
        static let longitude = Column(CodingKeys.longitude)
    }
    
    /// Arrange the selected columns and lock their order
    static let databaseSelection: [any SQLSelectable] = [
        Columns.id,
        Columns.title,
        Columns.favorite,
        Columns.latitude,
        Columns.longitude]
}

// Fetching methods
extension Place: FetchableRecord {
    /// Creates a record from a database row
    init(row: Row) {
        // For high performance, use numeric indexes that match the
        // order of Place.databaseSelection
        id = row[0]
        title = row[1]
        isFavorite = row[2]
        coordinate = CLLocationCoordinate2D(
            latitude: row[3],
            longitude: row[4])
    }
}

// Persistence methods
extension Place: MutablePersistableRecord {
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
Record类派生

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

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

记录方法列表

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

方法 协议 注释
核心方法
init(row:) FetchableRecord
Type.databaseTableName TableRecord
Type.databaseSelection TableRecord *
Type.persistenceConflictPolicy PersistableRecord *
record.encode(to:) EncodableRecord
插入和更新记录
record.insert(db) PersistableRecord
record.insertAndFetch(db) PersistableRecord & FetchableRecord
record.insertAndFetch(_:as:) PersistableRecord
record.insertAndFetch(_:selection:fetch:) PersistableRecord
record.inserted(db) PersistableRecord
record.save(db) PersistableRecord
record.saveAndFetch(db) PersistableRecord & FetchableRecord
record.saveAndFetch(_:as:) PersistableRecord
record.saveAndFetch(_:selection:fetch:) PersistableRecord
record.saved(db) PersistableRecord
record.update(db) PersistableRecord
record.updateAndFetch(db) PersistableRecord & FetchableRecord
record.updateAndFetch(_:as:) PersistableRecord
record.updateAndFetch(_:selection:fetch:) PersistableRecord
record.update(db, columns:...) PersistableRecord
record.updateAndFetch(_:columns:selection:fetch:) PersistableRecord
record.updateChanges(db, from:...) PersistableRecord *
record.updateChanges(db) { ... } PersistableRecord *
record.updateChangesAndFetch(_:columns:as:modify:) PersistableRecord
record.updateChangesAndFetch(_:columns:selection:fetch:modify:) PersistableRecord
record.updateChanges(db) Record *
record.upsert(db) PersistableRecord
record.upsertAndFetch(db) PersistableRecord & FetchableRecord
record.upsertAndFetch(_:as:) PersistableRecord
Type.updateAll(db, ...) TableRecord
Type.filter(...).updateAll(db, ...) TableRecord ²
删除记录
record.delete(db) PersistableRecord
Type.deleteOne(db, key:...) TableRecord ¹
Type.deleteOne(db, id:...) TableRecord & Identifiable ¹
Type.deleteAll(db) TableRecord
Type.deleteAll(db, keys:...) TableRecord ¹
Type.deleteAll(db, ids:...) TableRecord & Identifiable ¹
Type.filter(...).deleteAll(db) TableRecord ²
持久化回调
record.willInsert(_:) PersistableRecord
record.aroundInsert(_:insert:) PersistableRecord
record.didInsert(_:) PersistableRecord
record.willUpdate(_:columns:) PersistableRecord
record.aroundUpdate(_:columns:update:) PersistableRecord
record.didUpdate(_:) PersistableRecord
record.willSave(_:) PersistableRecord
record.aroundSave(_:save:) PersistableRecord
record.didSave(_:) PersistableRecord
record.willDelete(_:) PersistableRecord
record.aroundDelete(_:delete:) PersistableRecord
record.didDelete(deleted:) PersistableRecord
检查记录存在
record.exists(db) PersistableRecord
Type.exists(db, key: ...) TableRecord ¹
Type.exists(db, id: ...) TableRecord & Identifiable ¹
Type.filter(...).isEmpty(db) TableRecord ²
将记录转换为字典
record.databaseDictionary EncodableRecord
计算记录数量
Type.fetchCount(db) TableRecord
Type.filter(...).fetchCount(db) TableRecord ²
检索记录游标
Type.fetchCursor(db) 可检索记录 & 表记录
Type.fetchCursor(db, keys:...) 可检索记录 & 表记录 ¹
Type.fetchCursor(db, ids:...) 可检索记录 & 表记录 & 可识别的 ¹
Type.fetchCursor(db, sql: sql) FetchableRecord ³
Type.fetchCursor(statement) FetchableRecord
Type.filter(...).fetchCursor(db) 可检索记录 & 表记录 ²
检索记录数组
Type.fetchAll(db) 可检索记录 & 表记录
Type.fetchAll(db, keys:...) 可检索记录 & 表记录 ¹
Type.fetchAll(db, ids:...) 可检索记录 & 表记录 & 可识别的 ¹
Type.fetchAll(db, sql: sql) FetchableRecord ³
Type.fetchAll(statement) FetchableRecord
Type.filter(...).fetchAll(db) 可检索记录 & 表记录 ²
检索记录集合
Type.fetchSet(db) 可检索记录 & 表记录
Type.fetchSet(db, keys:...) 可检索记录 & 表记录 ¹
Type.fetchSet(db, ids:...) 可检索记录 & 表记录 & 可识别的 ¹
Type.fetchSet(db, sql: sql) FetchableRecord ³
Type.fetchSet(statement) FetchableRecord
Type.filter(...).fetchSet(db) 可检索记录 & 表记录 ²
检索单个记录
Type.fetchOne(db) 可检索记录 & 表记录
Type.fetchOne(db, key:...) 可检索记录 & 表记录 ¹
Type.fetchOne(db, id:...) 可检索记录 & 表记录 & 可识别的 ¹
Type.fetchOne(db, sql: sql) FetchableRecord ³
Type.fetchOne(statement) FetchableRecord
Type.filter(...).fetchOne(db) 可检索记录 & 表记录 ²
Type.find(db, key:...) 可检索记录 & 表记录 ¹
Type.find(db, id:...) 可检索记录 & 表记录 & 可识别的 ¹
可编码记录
Type.databaseDecodingUserInfo FetchableRecord *
Type.databaseJSONDecoder(for:) FetchableRecord *
Type.databaseDateDecodingStrategy FetchableRecord *
Type.databaseEncodingUserInfo EncodableRecord *
Type.databaseJSONEncoder(for:) EncodableRecord *
Type.databaseDateEncodingStrategy EncodableRecord *
Type.databaseUUIDEncodingStrategy EncodableRecord *
定义关联关系
Type.belongsTo(...) TableRecord *
Type.hasMany(...) TableRecord *
Type.hasOne(...) TableRecord *
Type.hasManyThrough(...) TableRecord *
Type.hasOneThrough(...) TableRecord *
构建查询界面请求
record.request(for:...) 表记录 & 可编码记录 *
Type.all() TableRecord ²
Type.none() TableRecord ²
Type.select(...) TableRecord ²
Type.select(..., as:...) TableRecord ²
Type.selectPrimaryKey(as:...) TableRecord ²
Type.annotated(with:...) TableRecord ²
Type.filter(...) TableRecord ²
Type.filter(id:) 表记录 & Identifiable *
Type.filter(ids:) 表记录 & Identifiable *
Type.matching(...) TableRecord ²
Type.including(all:) TableRecord ²
Type.including(optional:) TableRecord ²
Type.including(required:) TableRecord ²
Type.joining(optional:) TableRecord ²
Type.joining(required:) TableRecord ²
Type.group(...) TableRecord ²
Type.groupByPrimaryKey() TableRecord ²
Type.having(...) TableRecord ²
Type.order(...) TableRecord ²
Type.orderByPrimaryKey() TableRecord ²
Type.limit(...) TableRecord ²
Type.with(...) TableRecord ²
记录比较
record.databaseEquals(...) EncodableRecord
record.databaseChanges(from:...) EncodableRecord
record.updateChanges(db, from:...) PersistableRecord
record.updateChanges(db) { ... } PersistableRecord
record.hasDatabaseChanges Record
record.databaseChanges Record
record.updateChanges(db) Record

¹ 所有唯一键均受支持:主键(单列、组合、rowid)和唯一索引

try Player.fetchOne(db, id: 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(db, sql: "SELECT * FROM player WHERE id = ?", arguments: [1]) // Player?

Statement

let statement = try db.makeStatement(sql: "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(originColumn == "Burgundy")
        .order(priceColumn)
        .fetchAll(db)
    
    // Count
    let count = try Wine
        .filter(colorColumn == Color.red)
        .fetchCount(db)
    
    // Update
    try Wine
        .filter(originColumn == "Burgundy")
        .updateAll(db, priceColumn *= 0.75)
    
    // Delete
    try Wine
        .filter(corkedColumn == true)
        .deleteAll(db)
}

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

请注意,查询接口无法生成所有可能的 SQL 查询。您可能也更愿意编写 SQL,这完全没问题。从小的代码段到完整的查询,您的 SQL 技能都是非常受欢迎的。

try dbQueue.write { db in
    // Update database schema (with SQL)
    try db.execute(sql: "CREATE TABLE wine (...)")
    
    // Fetch records (with SQL)
    let wines = try Wine.fetchAll(db,
        sql: "SELECT * FROM wine WHERE origin = ? ORDER BY price",
        arguments: ["Burgundy"])
    
    // Count (with an SQL snippet)
    let count = try Wine
        .filter(sql: "color = ?", arguments: [Color.red])
        .fetchCount(db)
    
    // Update (with SQL)
    try db.execute(sql: "UPDATE wine SET price = price * 0.75 WHERE origin = 'Burgundy'")
    
    // Delete (with SQL)
    try db.execute(sql: "DELETE FROM wine WHERE corked")
}

因此,不要错过SQL API

注意:生成的 SQL 代码可能在 GRDB 版本之间发生变化,恕不另行通知:请勿让您的应用程序依赖任何特定的 SQL 输出。

请求

📖 QueryInterfaceRequestTable

查询接口请求让您可以从数据库中获取值。

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

// The request for all players:
let request = Player.all()
let players = try request.fetchAll(db) // [Player]

当您不能使用记录类型时,请使用 Table

// The request for all rows from the player table:
let table = Table("player")
let request = table.all()
let rows = try request.fetchAll(db)    // [Row]

// The request for all players from the player table:
let table = Table<Player>("player")
let request = table.all()
let players = try request.fetchAll(db) // [Player]

注意:以下文档中的所有示例都使用记录类型,但您始终可以用 Table 代替。

接下来,声明你想要用于过滤或排序的表

let idColumn = Column("id")
let nameColumn = Column("name")

你也可以声明列枚举,如果更喜欢这种方式的话。

// Columns.id and Columns.name can be used just as
// idColumn and nameColumn declared above.
enum Columns: String, ColumnExpression {
    case id
    case name
}

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

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

    // SELECT * FROM player
    Player.all()

    默认情况下,所有列都被选中。参见请求选中的列

  • select(...)select(..., as:)定义了所选列。参见请求选中的列

    // SELECT name FROM player
    Player.select(nameColumn, as: String.self)
  • annotated(with: expression...)扩展了选择。

    // SELECT *, (score + bonus) AS total FROM player
    Player.annotated(with: (scoreColumn + bonusColumn).forKey("total"))
  • annotated(with: aggregate)通过关联聚合扩展了选择。

    // SELECT team.*, COUNT(DISTINCT player.id) AS playerCount
    // FROM team
    // LEFT JOIN player ON player.teamId = team.id
    // GROUP BY team.id
    Team.annotated(with: Team.players.count)
  • annotated(withRequired: association)annotated(withOptional: association)通过关联扩展了选择。

    // SELECT player.*, team.color
    // FROM player
    // JOIN team ON team.id = player.teamId
    Player.annotated(withRequired: Player.team.select(colorColumn))
  • distinct()执行去重操作。

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

    // SELECT * FROM player WHERE id IN (1, 2, 3)
    Player.filter([1,2,3].contains(idColumn))
    
    // SELECT * FROM player WHERE (name IS NOT NULL) AND (height > 1.75)
    Player.filter(nameColumn != nil && heightColumn > 1.75)
  • filter(id:)filter(ids:)是类型安全的方法,在可识别记录上可用。

    // SELECT * FROM player WHERE id = 1
    Player.filter(id: 1)
    
    // SELECT * FROM country WHERE isoCode IN ('FR', 'US')
    Country.filter(ids: ["FR", "US"])
  • filter(key:)filter(keys:)对主键和唯一键应用条件。

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

    // 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.id) >= 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)

    SQLite在排序目的上将NULL值视为小于任何其他值。因此,NULL值自然出现在升序排序的开始和降序排序的末尾。使用自定义SQLite构建,这可以通过.ascNullsLast.descNullsFirst来改变。

    // SELECT * FROM player ORDER BY score ASC NULLS LAST
    Player.order(nameColumn.ascNullsLast)

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

    // SELECT * FROM player ORDER BY name
    Player.order(scoreColumn).order(nameColumn)
  • reversed()反转最终排序。

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

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

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

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

    // SELECT player.*, team.*
    // FROM player
    // JOIN team ON team.id = player.teamId
    Player.including(required: Player.team)
  • with(cte)嵌入一个公用表表达式

    // WITH ... SELECT * FROM player
    let cte = CommonTableExpression(...)
    Player.with(cte)
  • 涉及主键的其他请求

    • selectPrimaryKey(as:)选择主键。

      // SELECT id FROM player
      Player.selectPrimaryKey(as: Int64.self)    // QueryInterfaceRequest<Int64>
      
      // SELECT code FROM country
      Country.selectPrimaryKey(as: String.self)  // QueryInterfaceRequest<String>
      
      // SELECT citizenId, countryCode FROM citizenship
      Citizenship.selectPrimaryKey(as: Row.self) // QueryInterfaceRequest<Row>
    • orderByPrimaryKey()按主键排序。

      // SELECT * FROM player ORDER BY id
      Player.orderByPrimaryKey()
      
      // SELECT * FROM country ORDER BY code
      Country.orderByPrimaryKey()
      
      // SELECT * FROM citizenship ORDER BY citizenId, countryCode
      Citizenship.orderByPrimaryKey()
    • groupByPrimaryKey()按主键分组行。

您可以通过方法链进一步优化请求

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

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
struct Player: TableRecord { ... }
let request = Player.all()

// SELECT * FROM player
let table = Table("player")
let request = table.all()

可以更改每个单个请求的选择,或者在基于记录的请求的情况下,对所有从这个记录类型构建的请求进行选择。

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

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

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

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

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

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

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

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

注意:请确保将databaseSelection属性显式声明为[any SQLSelectable]。如果不这样做,Swift编译器可能会悄然忽略协议要求,从而导致粘性的SELECT *请求。要验证您的设置,请参阅如何将请求打印为SQL?常见问题解答。

表达式

将您的Swift代码构建的SQL表达式提供给请求

SQL 运算符

📖 SQLSpecificExpressible

GRDB提供了许多SQLite 内置运算符的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)

    支持子查询

    // SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
    let maximumScore = Player.select(max(scoreColumn))
    Player.filter(scoreColumn == maximumScore)
    
    // SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
    let maximumScore = SQLRequest("SELECT max(score) FROM player")
    Player.filter(scoreColumn == maximumScore)

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

  • *, /, +, -

    SQLite算术运算符与它们的Swift等效项类似

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

    注意:的表达式如nameColumn + "rrr"将被SQLite解释为数值相加(结果可能有趣),而不是字符串连接。请参阅下文中的`concat`运算符。

    当您想用`+`或`*`运算符连接一系列表达式时,请使用`joined(operator:)

    // SELECT score + bonus + 1000 FROM player
    let values = [
        scoreColumn,
        bonusColumn,
        1000.databaseValue]
    Player.select(values.joined(operator: .add))

    注意上述示例中如何连接原始值:1000.databaseValue。一个简单的1000将无法编译。

    当序列为空时,joined(operator: .add)返回0,而joined(operator: .multiply)返回1。

  • &, |, ~, <<, >>

    位运算(位与、或、非、左移、右移)与其Swift等效项类似

    // SELECT mask & 2 AS isRocky FROM planet
    Planet.select((Column("mask") & 2).forKey("isRocky"))
  • ||

    连接几个字符串

    // SELECT firstName || ' ' || lastName FROM player
    Player.select([firstNameColumn, " ".databaseValue, lastNameColumn].joined(operator: .concat))

    在上面的示例中,注意您是如何连接原始字符串的:" ".databaseValue。一个普通的" "是无法编译的。

    当序列为空时,joined(operator: .concat)返回空字符串。

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

    要检查在子查询中的包含,请同样调用contains方法

    // SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
    let selectedPlayerIds = PlayerSelection.select(playerIdColumn)
    Player.filter(selectedPlayerIds.contains(idColumn))
    
    // SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
    let selectedPlayerIds = SQLRequest("SELECT playerId FROM playerSelection")
    Player.filter(selectedPlayerIds.contains(idColumn))

    要检查在公共表表达式(CTE)中的包含,请同样调用contains方法

    // WITH selectedName AS (...)
    // SELECT * FROM player WHERE name IN selectedName
    let cte = CommonTableExpression(named: "selectedName", ...)
    Player
        .with(cte)
        .filter(cte.contains(nameColumn))

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

  • EXISTSNOT EXISTS

    要检查子查询是否会返回行,请调用exists方法

    // Teams that have at least one other player
    //
    //  SELECT * FROM team
    //  WHERE EXISTS (SELECT * FROM player WHERE teamID = team.id)
    let teamAlias = TableAlias()
    let player = Player.filter(Column("teamID") == teamAlias[Column("id")])
    let teams = Team.aliased(teamAlias).filter(player.exists())
    
    // Teams that have no player
    //
    //  SELECT * FROM team
    //  WHERE NOT EXISTS (SELECT * FROM player WHERE teamID = team.id)
    let teams = Team.aliased(teamAlias).filter(!player.exists())

    在上面的示例中,您使用一个TableAlias来让子查询引用另一个表中的列。

    在下一个示例中,其中涉及相同的表两次,表别名需要通过TableAlias(name:)进行显式区分

    // Players who coach at least one other player
    //
    //  SELECT coach.* FROM player coach
    //  WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
    let coachAlias = TableAlias(name: "coach")
    let coachedPlayer = Player.filter(Column("coachId") == coachAlias[Column("id")])
    let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())

    最后,子查询也可以用SQL表达式表示,并使用SQL插值

    // SELECT coach.* FROM player coach
    // WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
    let coachedPlayer = SQLRequest("SELECT * FROM player WHERE coachId = \(coachAlias[Column("id")])")
    let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())
  • LIKE

    SQLite LIKE运算符作为like方法可用

    // SELECT * FROM player WHERE (email LIKE '%@example.com')
    Player.filter(emailColumn.like("%@example.com"))
    
    // SELECT * FROM book WHERE (title LIKE '%10\%%' ESCAPE '\')
    Player.filter(emailColumn.like("%10\\%%", escape: "\\"))

    Note: SQLite LIKE运算符是不区分大小写的,但不支持UTF。例如,表达式'a' LIKE 'A'是true,但'æ' LIKE 'Æ'是false。

  • MATCH

    全文匹配运算符通过FTS3Pattern(适用于FTS3和FTS4表)以及FTS5Pattern(适用于FTS5)提供

    FTS3和FTS4

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

    FTS5

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

    要为一个表达式提供一个别名,使用forKey方法

    // SELECT (score + bonus) AS total
    // FROM player
    Player.select((Column("score") + Column("bonus")).forKey("total"))

    如果您需要在这个请求的其他地方引用此别名列,请使用分离的列

    // SELECT (score + bonus) AS total
    // FROM player 
    // ORDER BY total
    Player
        .select((Column("score") + Column("bonus")).forKey("total"))
        .order(Column("total").detached)

    Column("total")不同,分离列Column("total").detached从未与"player"表关联,所以在生成SQL时总是以total呈现,即使在请求涉及其他表通过关联公共表表达式时也一样。

SQL 函数

📖 SQLSpecificExpressible

GRDB自带了许多SQLite< href="https://sqlite.ac.cn/lang_corefunc.html" rel="nofollow">内置函数的Swift版本,如下所示。但不是全部:有关添加对缺少的SQL函数支持的方式,请参阅在查询接口请求中嵌入SQL.

  • ABSAVGCOUNTDATETIMEJULIANDAYLENGTHMAXMINSUMTOTAL

    这些基于 absaveragecountdateTimejulianDaylengthmaxminsumtotal 等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))
    
    // SELECT JULIANDAY(date, 'start of year') FROM game
    Game.select(julianDay(dateColumn, .startOfYear))

    有关关于 dateTimejulianDay 函数的更多信息,请参阅 日期和时间函数

  • IFNULL

    使用 Swift 的 ?? 操作符

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

    查询接口不提供对这些 SQLite 函数的访问。并非反对它们,但它们不支持 Unicode。

    相反,GRDB 扩展 SQLite,带有调用 Swift 内置字符串函数 capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercased 的 SQL 函数

    Player.select(nameColumn.uppercased())

    注意:当 比较 字符串时,您最好使用 校对

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

    您可以使用自己的 自定义 SQL 函数和聚合

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

在查询接口请求中嵌入 SQL

有时您想扩展您的查询接口请求以包含 SQL 片断。这可能是由于 GRDB 不提供某些 SQL 函数或操作符的 Swift 接口,或者因为您想使用 GRDB 不支持的 SQLite 构造。

对可扩展性的支持很广,但并非无限。所有通过查询接口请求构建的 SQL 查询都具有以下形状。 如果您需要其他内容,您必须使用 原始 SQL 请求

WITH ...     -- 1
SELECT ...   -- 2
FROM ...     -- 3
JOIN ...     -- 4
WHERE ...    -- 5
GROUP BY ... -- 6
HAVING ...   -- 7
ORDER BY ... -- 8
LIMIT ...    -- 9
  1. WITH ...:请参阅 公用表表达式

  2. SELECT ...

    选择可以作为原始 SQL 提供

    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let request = Player.select(sql: "IFNULL(name, 'O''Brien'), score")
    
    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let defaultName = "O'Brien"
    let request = Player.select(sql: "IFNULL(name, ?), score", arguments: [suffix])

    选择可以提供 SQL 插值

    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let defaultName = "O'Brien"
    let request = Player.select(literal: "IFNULL(name, \(defaultName)), score")

    选择可以提供 Swift 和 SQL 插值 的组合

    // SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
    let defaultName = "O'Brien"
    let displayName: SQL = "IFNULL(\(Column("name")), \(defaultName)) AS displayName"
    let request = Player.select(displayName, Column("score"))

    当自定义 SQL 片断应以支持 + Swift 操作符的完整表达式运行为目标,并支持 forKey 别名方法以及所有其他 SQL 操作符 时,使用 SQL.sqlExpression 方法构建一个 表达式文字

    // SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
    let defaultName = "O'Brien"
    let displayName = SQL("IFNULL(\(Column("name")), \(defaultName))").sqlExpression
    let request = Player.select(displayName.forKey("displayName"), Column("score"))

    此类表达式文字允许您构建一个可重用的 SQL 函数或操作符的辅助库,这些函数或操作符在查询接口中不存在。例如,您可以定义一个 Swift date 函数

    func date(_ value: some SQLSpecificExpressible) -> SQLExpression {
        SQL("DATE(\(value))").sqlExpression
    }
    
    // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
    let request = Player.filter(date(Column("createdAt")) == "2020-01-23")

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

  3. FROM ...:此处仅支持一个表。您不能自定义此 SQL 部分。

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

  5. WHERE ...

    WHERE 子句可以以原生 SQL 的形式提供

    // SELECT * FROM player WHERE score >= 1000
    let request = Player.filter(sql: "score >= 1000")
    
    // SELECT * FROM player WHERE score >= 1000
    let minScore = 1000
    let request = Player.filter(sql: "score >= ?", arguments: [minScore])

    WHERE 子句可以与 SQL Interpolation 一起提供

    // SELECT * FROM player WHERE score >= 1000
    let minScore = 1000
    let request = Player.filter(literal: "score >= \(minScore)")

    WHERE 子句可以与 Swift 和 SQL Interpolation 的混合形式一起提供

    // SELECT * FROM player WHERE (score >= 1000) AND (team = 'red')
    let minScore = 1000
    let scoreCondition: SQL = "\(Column("score")) >= \(minScore)"
    let request = Player.filter(scoreCondition && Column("team") == "red")

    有关更多 SQL Interpolation 示例,请参阅上面的 SELECT ...

  6. GROUP BY ...

    GROUP BY 子句可以以原生 SQL、SQL Interpolation 或 Swift 和 SQL Interpolation 的混合形式提供,就像选择和 WHERE 子句(见上文)一样。

  7. HAVING ...

    HAVING 子句可以以原生 SQL、SQL Interpolation 或 Swift 和 SQL Interpolation 的混合形式提供,就像选择和 WHERE 子句(见上文)一样。

  8. ORDER BY ...

    ORDER BY 子句可以以原生 SQL、SQL Interpolation 或 Swift 和 SQL Interpolation 的混合形式提供,就像选择和 WHERE 子句(见上文)一样。

    为了支持 descasc 查询接口操作符和 reversed() 查询接口方法,您必须以 表达式字面量 的形式提供您的排序方式,使用 SQL.sqlExpression 方法

    // SELECT * FROM "player" 
    // ORDER BY (score + bonus) ASC, name DESC
    let total = SQL("(score + bonus)").sqlExpression
    let request = Player
        .order(total.desc, Column("name"))
        .reversed()
  9. LIMIT ...: 使用 limit(_:offset:) 方法。您无法自定义此 SQL 部分。

从请求中获取数据

一旦您有一个请求,您就可以获取请求源处的记录

// Some request based on `Player`
let request = Player.filter(...)... // QueryInterfaceRequest<Player>

// Fetch players:
try request.fetchCursor(db) // A Cursor of Player
try request.fetchAll(db)    // [Player]
try request.fetchSet(db)    // Set<Player>
try request.fetchOne(db)    // Player?

例如

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

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

您有时想要获取其他值.

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

// Fetch an Int
let request = Player.select(max(scoreColumn))
let maxScore = try Int.fetchOne(db, request) // Int?

// Fetch a Row
let request = Player.select(min(scoreColumn), max(scoreColumn))
let row = try Row.fetchOne(db, request)!     // Row
let minScore = row[0] as Int?
let maxScore = row[1] as Int?

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

  • 使用 asRequest(of:),当您使用 关联 时很有用

    struct BookInfo: FetchableRecord, Decodable {
        var book: Book
        var author: Author
    }
    
    // A request of BookInfo
    let request = Book
        .including(required: Book.author)
        .asRequest(of: BookInfo.self)
    
    let bookInfos = try dbQueue.read { db in
        try request.fetchAll(db) // [BookInfo]
    }
  • 使用 select(..., as:),当您更改选择时很有用

    // A request of Int
    let request = Player.select(max(scoreColumn), as: Int.self)
    
    let maxScore = try dbQueue.read { db in
        try request.fetchOne(db) // Int?
    }

通过键获取数据

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

可识别记录 可以使用类型安全的 find(_:id:)fetchOne(_:id:)fetchAll(_:ids:)fetchSet(_:ids:) 方法

try Player.find(db, id: 1)                   // Player
try Player.fetchOne(db, id: 1)               // Player?
try Country.fetchAll(db, ids: ["FR", "US"])  // [Countries]

所有记录类型都可以使用 find(_:key:)fetchOne(_:key:)fetchAll(_:keys:)fetchSet(_:keys:) 来应用主键和唯一键的条件

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

当表中没有显式的主键时,GRDB 使用 隐藏的 rowid

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

当您想要构建一个请求以后从中提取数据时,可以使用过滤器方法。

let request = Player.filter(id: 1)
let request = Country.filter(ids: ["FR", "US"])
let request = Player.filter(key: ["email": "[email protected]"])
let request = Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])

记录存在性测试

您可以根据数据库中是否存在匹配的行来检查请求。

// Some request based on `Player`
let request = Player.filter(...)...

// Check for player existence:
let noSuchPlayer = try request.isEmpty(db) // Bool

应该检查是否为空,而不是计数。

// Correct
let noSuchPlayer = try request.fetchCount(db) == 0
// Even better
let noSuchPlayer = try request.isEmpty(db)

您还可以检查给定的主键或唯一键是否存在于数据库中。

可识别记录可以使用类型安全的exists(_:id:)方法。

try Player.exists(db, id: 1)
try Country.exists(db, id: "FR")

所有记录类型都可以使用exists(_:key:)来检查主键和唯一键。

try Player.exists(db, key: 1)
try Country.exists(db, key: "FR")
try Player.exists(db, key: ["email": "[email protected]"])
try Citizenship.exists(db, key: ["citizenId": 1, "countryCode": "FR"])

应该检查键的存在性,而不是检索记录并检查是否为nil。

// Correct
let playerExists = try Player.fetchOne(db, id: 1) != nil
// Even better
let playerExists = try Player.exists(db, id: 1)

检索聚合值

请求可以计数。 使用fetchCount()方法可以返回符合查询条件的行数。

// SELECT COUNT(*) FROM player
let count = try Player.fetchCount(db) // Int

// SELECT COUNT(*) FROM player WHERE email IS NOT NULL
let count = try Player.filter(emailColumn != nil).fetchCount(db)

// SELECT COUNT(DISTINCT name) FROM player
let count = try Player.select(nameColumn).distinct().fetchCount(db)

// SELECT COUNT(*) FROM (SELECT DISTINCT name, score FROM player)
let count = try Player.select(nameColumn, scoreColumn).distinct().fetchCount(db)

其他聚合值 可以被选择和检索(见 SQL 函数)。

let request = Player.select(max(scoreColumn))
let maxScore = try Int.fetchOne(db, request) // Int?

let request = Player.select(min(scoreColumn), max(scoreColumn))
let row = try Row.fetchOne(db, request)!     // Row
let minScore = row[0] as Int?
let maxScore = row[1] as Int?

删除请求

请求可以删除记录,使用deleteAll()方法。

// DELETE FROM player
try Player.deleteAll(db)

// DELETE FROM player WHERE team = 'red'
try Player
    .filter(teamColumn == "red")
    .deleteAll(db)

// DELETE FROM player ORDER BY score LIMIT 10
try Player
    .order(scoreColumn)
    .limit(10)
    .deleteAll(db)

注意 删除方法在遵循TableRecord 协议的类型以及Table类型中可用。

struct Player: TableRecord { ... }
try Player.deleteAll(db)          // Fine
try Table("player").deleteAll(db) // Just as fine

根据主键删除记录 是一个常见任务。

可识别记录可以使用类型安全的deleteOne(_:id:)deleteAll(_:ids:)方法。

try Player.deleteOne(db, id: 1)
try Country.deleteAll(db, ids: ["FR", "US"])

所有记录类型都可以使用deleteOne(_:key:)deleteAll(_:keys:),这些方法在对主键和唯一键上应用条件。

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

当表中没有显式的主键时,GRDB 使用 隐藏的 rowid

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

更新请求

请求可以批量更新记录。 updateAll()方法接受使用set(to:)方法定义的列赋值

// UPDATE player SET score = 0, isHealthy = 1, bonus = NULL
try Player.updateAll(db, 
    Column("score").set(to: 0), 
    Column("isHealthy").set(to: true), 
    Column("bonus").set(to: nil))

// UPDATE player SET score = 0 WHERE team = 'red'
try Player
    .filter(Column("team") == "red")
    .updateAll(db, Column("score").set(to: 0))

// UPDATE player SET top = 1 ORDER BY score DESC LIMIT 10
try Player
    .order(Column("score").desc)
    .limit(10)
    .updateAll(db, Column("top").set(to: true))

// UPDATE country SET population = 67848156 WHERE id = 'FR'
try Country
    .filter(id: "FR")
    .updateAll(db, Column("population").set(to: 67_848_156))

列赋值接受任何表达式。

// UPDATE player SET score = score + (bonus * 2)
try Player.updateAll(db, Column("score").set(to: Column("score") + Column("bonus") * 2))

为了方便,您还可以使用+=-=*=/=运算符。

// UPDATE player SET score = score + (bonus * 2)
try Player.updateAll(db, Column("score") += Column("bonus") * 2)

默认冲突解决规则适用,您也可以提供特定的一个。

// UPDATE OR IGNORE player SET ...
try Player.updateAll(db, onConflict: .ignore, /* assignments... */)

注意 updateAll方法在采用TableRecord协议和Table的类型上是可用的。

struct Player: TableRecord { ... }
try Player.updateAll(db, ...)          // Fine
try Table("player").updateAll(db, ...) // Just as fine

自定义请求

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

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

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

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

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

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

但您可能希望带来一些优雅性,并构建自定义请求。

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

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

  • SQLRequest是使用原始SQL构建的获取请求。例如

    extension Player {
        static func filter(color: Color) -> SQLRequest<Player> {
            SQLRequest<Player>(
                sql: "SELECT * FROM player WHERE color = ?"
                arguments: [color])
        }
    }
    
    // [Player]
    try Player.filter(color: .red).fetchAll(db)

    SQLRequest支持SQL插值

    extension Player {
        static func filter(color: Color) -> SQLRequest<Player> {
            "SELECT * FROM player WHERE color = \(color)"
        }
    }
  • asRequest(of:)方法改变请求获取的类型。这在使用关联时非常有用。

    struct BookInfo: FetchableRecord, Decodable {
        var book: Book
        var author: Author
    }
    
    let request = Book
        .including(required: Book.author)
        .asRequest(of: BookInfo.self)
    
    // [BookInfo]
    try request.fetchAll(db)
  • adapted(_:)方法简化了使用行适配器的复杂行的消费。请参见RowAdaptersplittingRowAdapters(columnCounts:)中关于使用adapted(_:)的示例代码。

加密

GRDB可以使用SQLCipher v3.4+对数据库进行加密。

使用CocoaPods,并在你的Podfile中指定

# GRDB with SQLCipher 4
pod 'GRDB.swift/SQLCipher'
pod 'SQLCipher', '~> 4.0'

# GRDB with SQLCipher 3
pod 'GRDB.swift/SQLCipher'
pod 'SQLCipher', '~> 3.4'

请确保从你的Podfile中移除任何现有的pod 'GRDB.swift'。`GRDB.swift/SQLCipher`必须是项目中唯一的GRDB pod,否则因为SQLCipher和系统SQLite之间的冲突,你将面临链接器或运行时错误。

创建或打开加密数据库

您可以通过向数据库连接提供密码来创建和打开加密数据库

var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase("secret")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

prepareDatabase中,您还将执行其他必须在SQLCipher连接的生命周期早期发生的SQLCipher配置步骤。例如

var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase("secret")
    try db.execute(sql: "PRAGMA cipher_page_size = ...")
    try db.execute(sql: "PRAGMA kdf_iter = ...")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

当您想以SQLCipher 4打开现有的SQLCipher 3数据库时,您可能想运行cipher_compatibility子句

// Open an SQLCipher 3 database with SQLCipher 4
var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase("secret")
    try db.execute(sql: "PRAGMA cipher_compatibility = 3")
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

有关更多信息,请参阅SQLCipher 4.0.0版本发布升级到SQLCipher 4

更改加密数据库的密码

您可以更改已加密数据库的密码

当您使用数据库队列时,用旧密码打开数据库,然后应用新密码

try dbQueue.write { db in
    try db.changePassphrase("newSecret")
}

当您使用数据库连接池时,确保在 barrierWriteWithoutTransaction 块内更改密码,以防止并发读取。您还必须通过调用 invalidateReadOnlyConnections 方法确保所有未来的读取都打开一个新的数据库连接。

try dbPool.barrierWriteWithoutTransaction { db in
    try db.changePassphrase("newSecret")
    dbPool.invalidateReadOnlyConnections()
}

注意:当应用程序希望更改密码后继续使用数据库队列或池时,它负责在数据库准备函数中调用 usePassphrase 方法时提供正确的密码。考虑

// WRONG: this won't work across a passphrase change
let passphrase = try getPassphrase()
var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase(passphrase)
}

// CORRECT: get the latest passphrase when it is needed
var config = Configuration()
config.prepareDatabase { db in
    let passphrase = try getPassphrase()
    try db.usePassphrase(passphrase)
}

注意:在更改密码期间,或者将新密码应用于数据库之后,DatabasePool.barrierWriteWithoutTransaction 方法不能阻止对数据库的访问,或保护数据库快照。那些数据库访问可能会抛出错误。应用程序应在其密码更改之前提供自己的机制来无效化打开的快照。

注意:除了在此处描述的“就地”更改密码外,您还可以导出使用新密码的新加密数据库。请参见 将数据库导出到加密数据库

将数据库导出到加密数据库

提供密码不会加密已经存在的纯文本数据库。SQLCipher无法执行此操作,您将收到错误:SQLite错误26:文件已加密或不是数据库

相反,在不同的位置创建新的加密数据库,然后导出现有数据库的内容。这既可以将纯文本数据库加密,也可以更改加密数据库的密码。

完成这一技术的具体方法已经在 SQLCipher 的文档中有记录,详情请查阅:技术文档

使用 GRDB,您将得到这样的结果

// The existing database
let existingDBQueue = try DatabaseQueue(path: "/path/to/existing.db")

// The new encrypted database, at some distinct location:
var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase("secret")
}
let newDBQueue = try DatabaseQueue(path: "/path/to/new.db", configuration: config)

try existingDBQueue.inDatabase { db in
    try db.execute(
        sql: """
            ATTACH DATABASE ? AS encrypted KEY ?;
            SELECT sqlcipher_export('encrypted');
            DETACH DATABASE encrypted;
            """,
        arguments: [newDBQueue.path, "secret"])
}

// Now the export is completed, and the existing database can be deleted.

安全考量

管理密码字符串的生命周期

建议尽可能避免在内存中保留密码过长的时间。为此,请确保从 prepareDatabase 方法中加载密码。

// NOT RECOMMENDED: this keeps the passphrase in memory longer than necessary
let passphrase = try getPassphrase()
var config = Configuration()
config.prepareDatabase { db in
    try db.usePassphrase(passphrase)
}

// RECOMMENDED: only load the passphrase when it is needed
var config = Configuration()
config.prepareDatabase { db in
    let passphrase = try getPassphrase()
    try db.usePassphrase(passphrase)
}

这种技术有助于管理密码的生命周期,但请注意,字符串的实际上可能存储在内存中,即使对象已释放很久。

为了更好地控制内存中密码的生命周期,请使用提供原生活性 resetBytes 函数的 Data 对象。

// RECOMMENDED: only load the passphrase when it is needed and reset its content immediately after use
var config = Configuration()
config.prepareDatabase { db in
    var passphraseData = try getPassphraseData() // Data
    defer {
        passphraseData.resetBytes(in: 0..<passphraseData.count)
    }
    try db.usePassphrase(passphraseData)
}

一些有要求的使用者可能希望走得更远,并管理原始密码字节的生命周期。详见下文。

管理密码字节的生命周期

GRDB 提供了便捷的方法来提供数据库密码作为 Swift 字符串:usePassphrase(_:)changePassphrase(_:)。这些方法不会在内存中保留密码字符串超过必要的时间。但它们与标准字符串类型一样安全:内存中实际密码字节的生命周期无法得到控制。

当您想精确地管理密码字节时,请直接使用 SQLCipher 的原始 C 函数与之交互。

例如

var config = Configuration()
config.prepareDatabase { db in
    ... // Carefully load passphrase bytes
    let code = sqlite3_key(db.sqliteConnection, /* passphrase bytes */)
    ... // Carefully dispose passphrase bytes
    guard code == SQLITE_OK else {
        throw DatabaseError(
            resultCode: ResultCode(rawValue: code), 
            message: db.lastErrorMessage)
    }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

用户密码可用性与数据库可用性对比

当密码安全存储在系统钥匙链中时,您的应用程序可以使用 kSecAttrAccessible 属性来保护它。

此类保护可以防止当密码不可用时GRDB创建SQLite连接。

var config = Configuration()
config.prepareDatabase { db in
    let passphrase = try loadPassphraseFromSystemKeychain()
    try db.usePassphrase(passphrase)
}

// Success if and only if the passphrase is available
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

由于同样的原因,数据库池(按需打开SQLite连接),可能随时会因为密码不可用而失败。

// Success if and only if the passphrase is available
let dbPool = try DatabasePool(path: dbPath, configuration: config)

// May fail if passphrase has turned unavailable
try dbPool.read { ... }

// May trigger value observation failure if passphrase has turned unavailable
try dbPool.write { ... }

因为数据库池维护了一组持久SQLite连接,某些数据库访问将使用现有连接并成功。其他一些数据库访问,只要池想打开一个新的连接就会失败。无法预测哪些访问会成功或失败。

由于同样的原因,即使密码已不可用,数据库队列(也维护持久SQLite连接)仍然可用。

因此,当密码不可用时,应用程序负责保护数据库访问。为此,它们可以使用 数据保护。它们还可以在密码不可用时销毁它们的数据库队列或池实例。

备份

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

例如,备份可以帮助您在实现NSDocument子类时,将内存数据库复制到数据库文件中,并从数据库文件中复制到内存数据库中。

let source: DatabaseQueue = ...      // or DatabasePool
let destination: DatabaseQueue = ... // or DatabasePool
try source.backup(to: destination)

backup 方法将阻塞当前线程,直到目标数据库包含与源数据库相同的内容。

当源是一个 数据库池 时,备份期间可以发生并发写入。这些写入可能或可能不会反映在备份中,但不会引发任何错误。

Database 有类似的 backup 方法。

let source: DatabaseQueue = ...      // or DatabasePool
let destination: DatabaseQueue = ... // or DatabasePool
try source.write { sourceDb in
    try destination.barrierWriteWithoutTransaction { destDb in
        try sourceDb.backup(to: destDb)
    }
}

此方法允许选择用于备份数据库的源和目标 Database 处理句柄。

备份进度报告

备份方法接受可选的 pagesPerStepprogress 参数。这两个参数可以一起用来追踪正在进行的数据库备份并终止不完整的备份。

当提供了 pagesPerStep 时,数据库备份在 步骤 中进行。在每一步中,从源到目的地最多复制 pagesPerStep 张数据库页。备份按顺序进行,直到所有页面都复制完毕。

当提供了 progress 回调函数时,在包括最后一步在内的每个备份步骤后调用 progress。即使指定了非默认的 pagesPerStep 或备份在单步中完成,progress 回调函数也将被调用。

try source.backup(
    to: destination,
    pagesPerStep: ...)
    { backupProgress in
       print("Database backup progress:", backupProgress)
    }

终止不完整的备份

如果当 backupProgress.isComplete == false 时调用 progress 抛出异常,备份将被终止,错误将重新抛出。然而,如果当 backupProgress.isComplete == true 时调用 progress 抛出异常,备份不受影响,错误将被静默忽略。

警告:将非默认的 pagesPerStepprogress 值传递给备份方法是针对高级用户设计的高级 API,旨在为专家用户提供额外的功能。GRDB 的备份 API 为 SQLite 的在线备份 API 提供了忠实的低级封装。GRDB 的文档不是官方 SQLite 备份 API 文档的完整替代品。

中断数据库

《interrupt()》方法会导致任何挂起的数据库操作终止,并在最早的机会返回。

它可以从任何线程中调用。

dbQueue.interrupt()
dbPool.interrupt()

当没有正在运行的SQL语句时,对interrupt()的调用是一个无操作,对interrupt()返回后启动的SQL语句没有影响。

被中断的数据库操作将抛出一个带有代码SQLITE_INTERRUPT的DatabaseError。如果中断的SQL操作是INSERT、UPDATE或DELETE操作,且该操作在一个显式的事务内,则整个事务将自动回滚。如果回滚的事务是由DatabaseWriter.writeDatabase.inTransaction等封装事务的方法启动的,那么直到封装方法返回之前,所有数据库访问都将抛出带有代码SQLITE_ABORT的DatabaseError。

例如

try dbQueue.write { db in
    try Player(...).insert(db)     // throws SQLITE_INTERRUPT
    try Player(...).insert(db)     // not executed
}                                  // throws SQLITE_INTERRUPT

try dbQueue.write { db in
    do {
        try Player(...).insert(db) // throws SQLITE_INTERRUPT
    } catch { }
}                                  // throws SQLITE_ABORT

try dbQueue.write { db in
    do {
        try Player(...).insert(db) // throws SQLITE_INTERRUPT
    } catch { }
    try Player(...).insert(db)     // throws SQLITE_ABORT
}                                  // throws SQLITE_ABORT

您可以捕获SQLITE_INTERRUPTSQLITE_ABORT错误

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

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

避免SQL注入

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

XKCD: Exploits of a Mom

https://xkcd.com/327/

以下是一个容易受到SQL注入的代码示例

// BAD BAD BAD
let id = 1
let name = textField.text
try dbQueue.write { db in
    try db.execute(sql: "UPDATE students SET name = '\(name)' WHERE id = \(id)")
}

如果用户输入一个有趣的字符串,如Robert'; DROP TABLE students; --,SQLite将看到以下SQL并删除您的数据库表,而不是按预期更新一个名称

UPDATE students SET name = 'Robert';
DROP TABLE students;
--' WHERE id = 1

为了避免这些问题,永远不要在SQL查询中嵌入原始值。唯一正确的技术是为您的原始SQL查询提供参数

let name = textField.text
try dbQueue.write { db in
    // Good
    try db.execute(
        sql: "UPDATE students SET name = ? WHERE id = ?",
        arguments: [name, id])
    
    // Just as good
    try db.execute(
        sql: "UPDATE students SET name = :name WHERE id = :id",
        arguments: ["name": name, "id": id])
}

当您使用记录查询接口时,GRDB总是阻止SQL注入为您

let id = 1
let name = textField.text
try dbQueue.write { db in
    if var student = try Student.fetchOne(db, id: id) {
        student.name = name
        try student.update(db)
    }
}

错误处理

GRDB可以抛出DatabaseErrorRecordError或导致您程序崩溃的致命错误

考虑到本地数据库并非从远程服务器加载的JSON,GRDB focuses on trusted databases。处理不受信任的数据库需要特别注意。

数据库错误

📖 DatabaseError

数据库错误会在SQLite错误发生时抛出

do {
    try Pet(masterId: 1, name: "Bobby").insert(db)
} catch let error as DatabaseError {
    // The SQLite error code: 19 (SQLITE_CONSTRAINT)
    error.resultCode
    
    // The extended error code: 787 (SQLITE_CONSTRAINT_FOREIGNKEY)
    error.extendedResultCode
    
    // The eventual SQLite message: FOREIGN KEY constraint failed
    error.message
    
    // The eventual erroneous SQL query
    // "INSERT INTO pet (masterId, name) VALUES (?, ?)"
    error.sql
    
    // The eventual SQL arguments
    // [1, "Bobby"]
    error.arguments
    
    // Full error description
    // > SQLite error 19: FOREIGN KEY constraint failed -
    // > while executing `INSERT INTO pet (masterId, name) VALUES (?, ?)`
    error.description
}

如果您想在错误描述中看到语句参数,请公开语句参数

SQLite使用结果代码来区分各种错误.

您可以捕获数据库错误并匹配结果代码

do {
    try ...
} catch let error as DatabaseError {
    switch error {
    case DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY:
        // foreign key constraint error
    case DatabaseError.SQLITE_CONSTRAINT:
        // any other constraint error
    default:
        // any other database error
    }
}

您也可以直接通过结果代码匹配错误

do {
    try ...
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
    // foreign key constraint error
} catch DatabaseError.SQLITE_CONSTRAINT {
    // any other constraint error
} catch {
    // any other database error
}

每个数据库错误有两个代码:一个extendedResultCode(详见扩展结果代码),和一个较不精确的resultCode(详见基本结果代码)。扩展结果代码是基本结果代码的细化,例如,SQLITE_CONSTRAINT_FOREIGNKEY是相对于SQLITE_CONSTRAINT而言的。

警告:SQLite在不同版本中逐步引入了扩展结果代码。遗憾的是,SQLite发布说明并没有对此做出明确的说明:请谨慎处理扩展结果代码。

记录错误

📖 RecordError

记录错误可持久化记录协议在update方法找不到任何行时抛出

do {
    try player.update(db)
} catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
    print("Key \(key) was not found in table \(table).")
}

记录错误也会在可获取记录协议中,当find方法找不到任何记录时抛出

do {
    let player = try Player.find(db, id: 42)
} catch let RecordError.recordNotFound(databaseTableName: table, key: key) {
    print("Key \(key) was not found in table \(table).")
}

致命错误

致命错误通知程序或数据库需要更改。

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

  • 代码请求数据库中包含NULL的非可选值

    // fatal error: could not convert NULL to String.
    let name: String = row["name"]

    解决方案:修复数据库内容,使用 NOT NULL约束,或加载一个可选值

    let name: String? = row["name"]
  • 从数据库值到Swift类型的转换失败

    // fatal error: could not convert "Mom’s birthday" to Date.
    let date: Date = row["date"]
    
    // fatal error: could not convert "" to URL.
    let url: URL = row["url"]

    解决方案:修复数据库内容,或使用 DatabaseValue 来处理所有可能的案例

    let dbValue: DatabaseValue = row["date"]
    if dbValue.isNull {
        // Handle NULL
    } else if let date = Date.fromDatabaseValue(dbValue) {
        // Handle valid date
    } else {
        // Handle invalid date
    }
  • 数据库不能保证代码做它所说的

    // fatal error: table player has no unique index on column email
    try Player.deleteOne(db, key: ["email": "[email protected]"])

    解决方案:向玩家.email列添加唯一索引,或使用 deleteAll 方法明确您可能会删除多行

    try Player.filter(Column("email") == "[email protected]").deleteAll(db)
  • 数据库连接不是可重入的

    // fatal error: Database methods are not reentrant.
    dbQueue.write { db in
        dbQueue.write { db in
            ...
        }
    }

    解决方案:避免重入,而是传递数据库连接。

如何处理不可信输入

让我们看看下面的代码

let sql = "SELECT ..."

// Some untrusted arguments for the query
let arguments: [String: Any] = ...
let rows = try Row.fetchCursor(db, sql: sql, arguments: StatementArguments(arguments))

while let row = try rows.next() {
    // Some untrusted database value:
    let date: Date? = row[0]
}

它有两个机会抛出致命错误

  • 不可信参数:字典可能包含不符合DatabaseValueConvertible协议的值,或者缺少语句所需的关键字。
  • 不可信的数据库内容:行可能包含一个无法转换成日期的非NULL值。

在这种情况下,您仍然可以通过在GRDB API中的一个级别以下暴露和处理每个失败点来避免致命错误

// Untrusted arguments
if let arguments = StatementArguments(arguments) {
    let statement = try db.makeStatement(sql: sql)
    try statement.setArguments(arguments)
    
    var cursor = try Row.fetchCursor(statement)
    while let row = try iterator.next() {
        // Untrusted database content
        let dbValue: DatabaseValue = row[0]
        if dbValue.isNull {
            // Handle NULL
        if let date = Date.fromDatabaseValue(dbValue) {
            // Handle valid date
        } else {
            // Handle invalid date
        }
    }
}

有关更多信息,请参阅 StatementDatabaseValue

错误日志

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

此全局错误回调必须在应用的生存期内尽早配置

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

警告:在打开任何数据库连接之前,必须设置Database.logError。这包括您的应用程序使用GRDB打开的连接,以及由其他工具(如第三方库)打开的连接。连接已打开后设置它是一种SQLite的不当使用,且没有任何效果。

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

Unicode

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

然而,SQLite 没有任何针对 Unicode 的字符串转换或比较功能。

Unicode 函数

SQLite 的内置函数 UPPERLOWER 不是 Unicode 兼容的

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

GRDB 通过调用 Swift 内置的字符串函数(capitalizedlowercaseduppercasedlocalizedCapitalizedlocalizedLowercasedlocalizedUppercased)扩展 SQLite 的 SQL 函数SQL 函数

// "JÉRÔME"
let uppercased = DatabaseFunction.uppercase
try String.fetchOne(db, sql: "SELECT \(uppercased.name)('Jérôme')")

这些 Unicode 兼容的字符串函数也方便地在查询接口中使用。

Player.select(nameColumn.uppercased)

字符串比较

SQLite 在许多情况下比较字符串:根据字符串列对行进行排序,或使用比较运算符(如 =<=)。

比较结果来自一个排序函数或者排序。SQLite自带三个不支持Unicode的内置排序:二进制、不区分大小写和删除尾部空格

GRDB还自带五个额外的排序,这些排序利用基于标准的Swift String比较函数和操作符的Unicode感知比较。

  • unicodeCompare(使用内置的<=== Swift运算符)
  • caseInsensitiveCompare
  • localizedCaseInsensitiveCompare
  • localizedCompare
  • localizedStandardCompare

可以将排序应用于表列。然后所有涉及此列的比较都将自动触发比较函数

try db.create(table: "player") { t in
    // Guarantees case-insensitive email unicity
    t.column("email", .text).unique().collate(.nocase)
    
    // Sort names in a localized case insensitive way
    t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
}

// Players are sorted in a localized case insensitive way:
let players = try Player.order(nameColumn).fetchAll(db)

警告:SQLite需要应用程序提供除了二进制、不区分大小写和删除尾部空格之外的任何排序的定义。当数据库文件需要共享或迁移到另一个SQLite库或平台(如您应用程序的Android版本)时,请确保您提供兼容的排序。

如果您无法或不想定义列的比较行为(请参阅上面的警告),您仍然可以在SQL请求和查询接口中使用显式的排序

let collation = DatabaseCollation.localizedCaseInsensitiveCompare
let players = try Player.fetchAll(db,
    sql: "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.
}

// Make the collation available to a database connection
var config = Configuration()
config.prepareDatabase { db in
    db.add(collation: collation)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

内存管理

SQLite和GRDB都使用非必需的内存,有助于它们性能提升。

您可以使用releaseMemory方法回收此内存

// Release as much memory as possible.
dbQueue.releaseMemory()
dbPool.releaseMemory()

此方法将阻塞当前线程,直到所有当前数据库访问完成,并收集内存。

警告:如果在执行长时间读取时调用DatabasePool.releaseMemory(),则直到长时间读取完成并释放内存之前,将无法进行其他读取访问。如果这不符合您的应用需求,请寻找下面的异步选项

您也可以以异步方式释放内存

// On a DatabaseQueue
dbQueue.asyncWriteWithoutTransaction { db in
    db.releaseMemory()
}

// On a DatabasePool
dbPool.releaseMemoryEventually()

DatabasePool.releaseMemoryEventually()不会阻塞当前线程,也不会阻止并发数据库访问。作为这种便利的代价,您不知道内存何时被释放。

在iOS中的内存管理

iOS操作系统喜欢那些消耗内存较少的应用程序。

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

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

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

常见问题解答

常见问题解答:打开连接

常见问题解答:SQL

常见问题解答:常规

常见问题解答:关联

常见问题解答:ValueObservation

常见问题解答:错误

常见问题解答:打开连接

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

首先选择数据库文件合适的位置。基于文档的应用程序将允许用户选择位置。使用数据库作为全局存储的应用程序会更倾向于应用程序支持目录。

下面的示例代码在它专属的目录内创建或打开数据库文件(推荐做法)。在首次运行时,创建一个新的空数据库文件。在后续运行中,数据库文件已经存在,所以只是打开一个连接

// HOW TO create an empty database, or open an existing database file

// Create the "Application Support/MyDatabase" directory
let fileManager = FileManager.default
let appSupportURL = try fileManager.url(
    for: .applicationSupportDirectory, in: .userDomainMask,
    appropriateFor: nil, create: true) 
let directoryURL = appSupportURL.appendingPathComponent("MyDatabase", isDirectory: true)
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)

// Open or create the database
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
let dbQueue = try DatabaseQueue(path: databaseURL.path)

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

对你的资源打开只读连接

// HOW TO open a read-only connection to a database resource

// Get the path to the database resource.
if let dbPath = Bundle.main.path(forResource: "db", ofType: "sqlite")

if let dbPath {
    // If the resource exists, open a read-only connection.
    // Writes are disallowed because resources can not be modified. 
    var config = Configuration()
    config.readonly = true
    let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
} else {
    // The database resource can not be found.
    // Fix your setup, or report the problem to the user. 
}

如何关闭数据库连接?

DatabaseQueueDatabasePool实例被销毁时,数据库连接会自动关闭。

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

常见问题:SQL

如何打印SQL请求?

当您想要调试未提供预期结果的请求时,您可能希望打印实际执行的SQL。

您可以编译请求为一个准备好的Statement

try dbQueue.read { db in
    let request = Player.filter(Column("email") == "[email protected]")
    let statement = try request.makePreparedRequest(db).statement
    print(statement) // SELECT * FROM player WHERE email = ?
    print(statement.arguments) // ["[email protected]"]
}

另一种选择是在连接到数据库时设置一个跟踪函数,该函数会打印出执行的SQL请求。例如,在连接数据库时提供一个跟踪函数。

// Prints all SQL statements
var config = Configuration()
config.prepareDatabase { db in
    db.trace { print($0) }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

try dbQueue.read { db in
    // Prints "SELECT * FROM player WHERE email = ?"
    let players = try Player.filter(Column("email") == "[email protected]").fetchAll(db)
}

如果您想在日志中看到语句参数,例如 '[email protected]',则请使语句参数公开

注意:生成的 SQL 代码可能在 GRDB 版本之间发生变化,恕不另行通知:请勿让您的应用程序依赖任何特定的 SQL 输出。

常规FAQ

如何监控数据库语句的执行时长?

使用 trace(options:_:) 方法,加上 .profile 选项

var config = Configuration()
config.prepareDatabase { db in
    db.trace(options: .profile) { event in
        // Prints all SQL statements with their duration
        print(event)
        
        // Access to detailed profiling information
        if case let .profile(statement, duration) = event, duration > 0.5 {
            print("Slow query: \(statement.sql)")
        }
    }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

try dbQueue.read { db in
    let players = try Player.filter(Column("email") == "[email protected]").fetchAll(db)
    // Prints "0.003s SELECT * FROM player WHERE email = ?"
}

如果您想在日志中看到语句参数,例如 '[email protected]',则请使语句参数公开

什么是实验性功能?

自从GRDB 1.0以来,所有关于语义版本控制的后向兼容性担保都适用:只有在库的下一个主要版本中才会发生重大变化。

虽然存在一个例外:即带有“🔥 EXPERIMENTAL”徽章的“实验特性”。这些特性过于年轻或者缺乏用户反馈,是高级特性。它们尚未稳定。

这些实验特性不受语义版本号的保护,可能在库的两次小版本发布之间出现冲突。为了帮助它们变得更稳定,非常感谢您通过以下链接提供反馈

GRDB支持库进化和ABI稳定性吗?

不,GRDB不支持库进化和ABI稳定性。它唯一保证的是根据语义版本号保持API的稳定性,但对于实验特性是个例外。

然而,您可以通过使用Xcode的“构建库以分发”选项(BUILD_LIBRARY_FOR_DISTRIBUTION)来构建GRDB,这样就可以方便地构建二进制框架。

常见问题:关联

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

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

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

struct Book: TableRecord {
    ...
    static let author = belongsTo(Author.self)
}

struct Author: TableRecord {
    ...
}

然后我们可以编写我们的请求,并仅获取有作者的书籍,丢弃匿名书籍

let books: [Book] = try dbQueue.read { db in
    // SELECT book.* FROM book 
    // JOIN author ON author.id = book.authorID
    let request = Book.joining(required: Book.author)
    return try request.fetchAll(db)
}

请注意,此请求未使用filter方法。实际上,我们没有任何条件要表达在任何列上。我们只需要“要求一本书可以与其作者相连”。

有关相反问题的更多信息,请参阅以下链接:如何过滤记录并仅保留那些没有关联到另一记录的记录

如何筛选记录并仅保留那些与另一记录不相关的记录?

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

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

struct Book: TableRecord {
    ...
    static let author = belongsTo(Author.self)
}

struct Author: TableRecord {
    ...
}

然后我们可以编写我们的请求,并仅获取没有作者的匿名书籍

let books: [Book] = try dbQueue.read { db in
    // SELECT book.* FROM book
    // LEFT JOIN author ON author.id = book.authorID
    // WHERE author.id IS NULL
    let authorAlias = TableAlias()
    let request = Book
        .joining(optional: Book.author.aliased(authorAlias))
        .filter(!authorAlias.exists)
    return try request.fetchAll(db)
}

这个请求使用了一个TableAlias,以便能够过滤最终关联的作者。我们确保bAuthor.primaryKey为nil,这也是它不存在的另一种说法:这本书没有作者。

请参见上方的如何筛选记录并仅保留那些与另一记录相关的记录?

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

假设你有两种记录类型,BookAuthor,并且你希望获取所有带有作者名字的书籍,但不获取完整的相关作者记录。

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

struct Book: Decodable, TableRecord {
    ...
    static let author = belongsTo(Author.self)
}

struct Author: Decodable, TableRecord {
    ...
    enum Columns {
        static let name = Column(CodingKeys.name)
    }
}

然后我们可以编写我们的请求和解析它的临时记录

struct BookInfo: Decodable, FetchableRecord {
    var book: Book
    var authorName: String? // nil when the book is anonymous
    
    static func all() -> QueryInterfaceRequest<BookInfo> {
        // SELECT book.*, author.name AS authorName
        // FROM book
        // LEFT JOIN author ON author.id = book.authorID
        let authorName = Author.Columns.name.forKey(CodingKeys.authorName)
        return Book
            .annotated(withOptional: Book.author.select(authorName))
            .asRequest(of: BookInfo.self)
    }
}

let bookInfos: [BookInfo] = try dbQueue.read { db in
    BookInfo.all().fetchAll(db)
}

通过将请求定义为BookInfo的静态方法,你可以访问私有CodingKeys.authorName,以及经过编译器检查的SQL列名。

通过使用annotated(withOptional:)方法,您可以将作者名字添加到可以由临时记录解析的顶层选择中。

通过使用asRequest(of:),您增强了请求的类型安全性。

常见问题:ValueObservation

为什么 ValueObservation 不发布值变化呢?

有时看起来一个 ValueObservation 并没有通知您期望的变化。

这可能由以下四个原因导致:

  1. 期望的变化没有被提交到数据库。
  2. 期望的变化虽然被提交到数据库,但很快被覆盖了。
  3. 观察被停止了。
  4. 观察没有跟踪期望的数据库区域。

为了回答前两个问题,查看数据库执行的 SQL 语句。这是在您打开数据库连接时进行的。

// Prints all SQL statements
var config = Configuration()
config.prepareDatabase { db in
    db.trace { print("SQL: \($0)") }
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

如果在此之后,您确定期望的变化已经提交到数据库,并且不久后被覆盖,请跟踪观察事件。

let observation = ValueObservation
    .tracking { db in ... }
    .print() // <- trace observation events
let cancellable = observation.start(...)

查看以 cancelfailure 开头的观察日志:可能观察被您的应用程序取消,或者发生错误而失败。

查看以 value 开头的观察日志:确保再次确认期望的值实际上并未被通知,然后被覆盖。

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

例如

  • empty:空区域,跟踪不任何事情并从未触发观察。
  • player(*):完整的 player
  • player(id,name)player 表的 idname
  • player(id,name)[1]player 表中 ID 为 1 的行的 idname
  • player(*),team(*):完整的 playerteam

如果您意外地使用了 ValueObservation.trackingConstantRegion(_:) 方法并且看到跟踪区域与您的期望不一致,请使用 tracking(_:) 修改观察的定义。您应该会看到以 tracked region 开头的日志现在会演变成包括期望的变化,并且您可以获得期望的通知。

如果在所有这些步骤(谢谢您!)之后,观察仍然失败,请在 GitHub 上创建一个议题 并提供 最小可重现示例

常见问题:错误

泛型参数 'T' 无法推断

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

// Generic parameter 'T' could not be inferred
let string = try dbQueue.read { db in
    let result = try String.fetchOne(db, ...)
    return result
}

这是 swift 编译器的一个限制

一般的解决方案是显式声明闭包结果的类型

// General Workaround
let string = try dbQueue.read { db -> String? in
    let result = try String.fetchOne(db, ...)
    return result
}

如有可能,您还可以写一行闭包

// Single-line closure workaround:
let string = try dbQueue.read { db in
    try String.fetchOne(db, ...)
}

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

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

var player = Player(id: nil, name: "Arthur")
try await dbWriter.write { db in
    // Error: Mutation of captured var 'player' in concurrently-executing code
    try player.insert(db)
}
print(player.id) // A non-nil id

在这种情况下,优先使用 insertedsaved 方法

// OK
var player = Player(id: nil, name: "Arthur")
player = try await dbWriter.write { [player] db in
    return try player.inserted(db)
}
print(player.id) // A non-nil id

SQLite 错误 1 "不存在该列"

此错误信息的含义是自解释的:请检查是否存在拼写错误或不存在列名称。

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

在这种情况下,有两种可能的解释

  1. 可能某个列名确实拼写错误或缺失在数据库模式中。

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

  2. 可能应用程序在原始 SQL 查询中将字符 " 用作字符串字面量的分隔符,而不是单引号 '。SQLite 的新版本学会了这种偏离 SQL 标准的错误消息,这就是您看到错误的原因。

    例如,这不是标准的 SQL:UPDATE player SET name = "Arthur"

    标准版本是:UPDATE player SET name = 'Arthur'

    只是巧合的是,旧版本的 SQLite 以前接受过这种非标准的版本。新版本可以以错误的形式拒绝它。

    修复方法是修改应用程序运行的SQL语句:在字符串字面量中将"替换为'

    此时也许应该学习关于语句参数和SQL注入的知识了。

    let name: String = ...
    
    // NOT STANDARD (double quote)
    try db.execute(sql: """
        UPDATE player SET name = "\(name)"
        """)
    
    // STANDARD, BUT STILL NOT RECOMMENDED (single quote)
    try db.execute(sql: "UPDATE player SET name = '\(name)'")
    
    // STANDARD, AND RECOMMENDED (statement arguments)
    try db.execute(sql: "UPDATE player SET name = ?", arguments: [name])

有关更多信息,请参阅“双引号字符串字面量被接受”以及Configuration.acceptsDoubleQuotedStringLiterals

SQLite错误10 "磁盘I/O错误",SQLite错误23 "未授权"}

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

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

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

您还可以通过使用更宽松的文件保护来完全防止这种错误。

SQLite错误21 "在LIKE查询中参数数量不正确"}

当执行类似于的LIKE查询时,可能会收到“参数数量不正确”的错误:

let name = textField.text
let players = try dbQueue.read { db in
    try Player.fetchAll(db, sql: "SELECT * FROM player WHERE name LIKE '%?%'", arguments: [name])
}

问题出在'%?%'模式上。

SQLite只将?解释为参数,如果它是整个值(整数、双精度数、字符串、blob、null)的占位符。在此错误查询中,?只是'%?%'字符串中的一个字符:它不是一个查询参数,也没有以任何方式处理。有关SQLite参数的更多信息,请参阅https://www.sqlite.org/lang_expr.html#varparam

为了修复错误,您可以将请求与模式本身一起提供,而不是名称

let name = textField.text
let players: [Player] = try dbQueue.read { db in
    let pattern = "%\(name)%"
    return try Player.fetchAll(db, sql: "SELECT * FROM player WHERE name LIKE ?", arguments: [pattern])
}

示例代码

  • 文档文档充满了GRDB代码片段。
  • 演示应用程序
  • 打开GRDB.xcworkspace:它包含启用了GRDB的playgrounds,您可以在其中进行测试。
  • groue/SortedDifference:如何将数据库表与JSON有效的负载同步

感谢


URI不改变:人们会改变它们。

支持缺失的SQL函数或操作符

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

高级数据库连接池

本章内容已移动至 此处

事务提交后钩子

本章内容已移动至 此处

异步API

本章内容已移动至 此处

变更跟踪

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

并发处理

本章内容已移动至 此处

自定义值类型

自定义值类型符合DatabaseValueConvertible 协议。

自定义数据库行的解码

本章已更名为 FetchableRecord 之上

自定义持久化方法

本章已被替换为 持久化回调

数据库更改观测

本章已 移动

数据库配置

本章节已移动

数据库队列

本章节已移动

数据库连接池

本章节已移动

数据库快照

本章内容已移动至 此处

数据库写入器和数据库读取器协议

本章节已被删除。请参阅数据库读取器数据库写入器的相关引用。

处理外部连接

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

数据库队列和池的区别

本章内容已移动至 此处

启用 FTS5 支持

从 GRDB 6.7.0 版本开始,FTS5 默认启用。

FetchedRecordsController

GRDB 5 版本中已删除 FetchedRecordsController。

数据库观察章节描述了其他观察数据库的方法。

全文搜索

本章已移动

担保和规则

本章内容已移动至 此处

连接查询支持

本章已用splittingRowAdapters(columnCounts:)的说明文本替代。

迁移

本章已移动

NSNumber和NSDecimalNumber

本章已移动

可持久化协议

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

PersistenceError

此错误已重命名为RecordError

预准备语句

本章内容已移动

行适配器

本章内容已移动

RowConvertible 协议

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

TableMapping 协议

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

交易和保存点

本章内容已移至别处

交易钩子

本章内容已移动至 此处

TransactionObserver 协议

本章内容已移至别处

未安全并发 API

本章内容已移动至 此处

ValueObservation

本章已迁移

ValueObservation 和 DatabaseRegionObservation

本章已被 ValueObservation 和 DatabaseRegionObservation 取代。