Swift 下使用 SQLite 教程:入门【译】

更新说明:本教程已由 Nikolas Burk 更新为 Xcode 9,iOS 11 和 Swift 4。最初的教程由 Chris Wagner 编写。

这篇教程向你展示了如何在 Swift 平台上使用流行的数据库 SQLite。在软件开发的领域,你需要很长时间才能保存应用数据。在很多情况下,这是以数据结构形式出现的。但是,如何有效的存储它 – 什么是有效的存储?

幸运的是,一些伟大的思想家已经开发出用于在数据库中存储结构化数据和编写语言功能以访问数据的解决方案。SQLite 默认在 iOS 中是可用的。实际上,如果你以前使用过 Core Data,那么你实际上已经使用过 SQLite,因为 Core Data 只是 SQLite 上的一个层封装,它提供了更方便的API。

通过这篇教程,你将学习到如何执行以下数据库的操作:

  • 创建和连接一个数据库
  • 创建一个表
  • 插入一行
  • 更新一行
  • 删除一行
  • 查询数据库
  • 处理 SQLite 错误

在学习如何执行这些基本操作之后,你将看到如何以类似 Swift 的方式将它们包装起来。这将允许你为应用程序编写抽象 API,以便你(大多数)可以避免去直接使用 SQLite 的 C API 的痛苦!:]

最后,我将简要介绍一下流行的开源 Swift 包装器 SQLite.swift,以便你能大致的了解一下底层框架是如何工作的。

注意:数据库,甚至只是 SQLite 本身,都是一个非常大的主题,因此它们大多超出了本教程的范围。本教程假设你对关系数据库意识形态有基本的了解,并且你主要在这里学习如何在 Swift 下使用 SQLite。

入门


下载工程:starter project for this SQLite with Swift tutorial 并打开 SQLiteTutorial.xcworkspace
Project Navigator 打开教程的 playground 文件。

注意:项目打包在 Xcode 工作区中,因为它使用 SQLite3 依赖项作为嵌入式二进制文件。此二进制文件包含你将在本教程中编写的 SQLite 代码的所有功能。

请注意,请将你的 Playground 配置为手动而不是自动运行:

这意味着它只会在你通过点击“运行”按钮的时候执行。

你可能还会看到在页面的顶部我们调用了 destroyPart1Database();你可以放心的忽略这一点,因为 Playground 每次运行的时候都会销毁这个文件。这可以确保在 Swift 教程浏览此 SQLite 时,所有的语句都能成功的执行。

你的 Playground 需要在你的文件系统上编写 SQLite 数据库文件,在终端中运行以下命令以创建游乐场数据库目录:

1
mkdir -p ~/Documents/Shared\ Playground\ Data/SQLiteTutorial

为什么选择 SQLite?


没错,SQLite 不是在 iOS 上保留数据的唯一方法。除了 Core Data,还有许多其他数据持久性替代方案,包括 RealmCouchbase LiteFirebaseNSCoding

每个都有自己的优点和缺点 - 包括 SQLite 本身,数据持久性没有灵丹妙药,作为开发人员,你可以根据应用程序的要求确定哪个选项超过其他选项。

SQLite 确实有一些优点:

  • 随 iOS 一起提供,因此它不会为你的应用程序包增加任何开销。
  • 试过并经过测试; 1.0 版于 2000 年 8 月发布。
  • 开源。
  • 适用于数据库开发人员和管理员的熟悉查询语言。
  • 跨平台

SQLite 的缺点可能是非常主观和自以为是,所以我们将把研究留给你了!:]

C 的 API


这部分 SQLite with Swift 教程将引导你完成最常见和最基本的 SQLite API。你很快就会意识到在 Swift 方法中包装 C API 将是理想的选择,但要紧紧抓住并首先完成 C 代码; 你将在本教程的第二部分做一些包装。

打开连接


在做任何事情之前,你首先需要创建一个数据库连接。
Playground 的“Getting Started”部分下添加以下方法:

1
2
3
4
5
6
7
8
9
10
11
func openDatabase() -> OpaquePointer? {
var db: OpaquePointer? = nil
if sqlite3_open(part1DbPath, &db) == SQLITE_OK {
print("Successfully opened connection to database at \(part1DbPath)")
return db
} else {
print("Unable to open database. Verify that you created the directory described " +
"in the Getting Started section.")
PlaygroundPage.current.finishExecution()
}
}

上面的方法调用 sqlite3_open()。这将打开或创建一个新的数据库文件,如果打开成功,它将会返回一个 OpaquePointer;这是一个用于 C 指针的 Swift 类型,无法直接在 Swift 中表示,当你调用这个方法时,你必须捕获返回的指针才能与数据库进行交互。

许多 SQLite 函数返回 Int32 结果代码。这些代码中的大多数都被定义为 SQLite 库中的常量。例如,SQLITE_OK 表示结果代码 0。在这里你能找到不同的结果代码的列表:on the main SQLite site

要打开数据库,请将下面的代码添加到 Playground

1
let db = openDatabase()

点击 Play 按钮运行 Playground并在控制台查看输出,如果控制台没有打开,请点击 Play 左侧的按钮:

如果 openDatabase() 运行成功,你将看到如下的输出:

1
Successfully opened connection to database at /Users/username/Documents/Shared Playground Data/SQLiteTutorial/Part1.sqlite

这里的 username 是你的 Home 目录。

创建一个表


现在你已经连接到数据库文件,你可以创建一个表。你将使用一个非常简单的表来存储联系人。

这个表将包含两列;Id 是一个 Int 类型并且是一个主键 PRIMARY KEYname 是一个 CHAR(255) 类型。

添加以下字符串,其中包含创建表所需的 SQL 语句:

1
2
3
4
5
let createTableString = """
CREATE TABLE Contact(
Id INT PRIMARY KEY NOT NULL,
Name CHAR(255));
"""

请注意,你正在使用 Swift 4 的便捷多语法来编写此语句!

接下来,添加执行 CREATE TABLE SQL 语句的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func createTable() {
// 1
var createTableStatement: OpaquePointer? = nil
// 2
if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
// 3
if sqlite3_step(createTableStatement) == SQLITE_DONE {
print("Contact table created.")
} else {
print("Contact table could not be created.")
}
} else {
print("CREATE TABLE statement could not be prepared.")
}
// 4
sqlite3_finalize(createTableStatement)
}

我们来一步一步的分析:

  1. 首先,在下一步中创建一个指针。
  2. sqlite3_prepare_v2() 将 SQL 语句编译为字节代码并返回状态代码 - 在对数据库执行任意语句之前的重要步骤。如果你有兴趣,可以在这里找到更多信息。检查返回的状态代码以确保语句编译成功。如果是,则该过程转到步骤3; 否则,你打印一条消息,指出该语句无法编译。
  3. sqlite3_step() 运行已编译的语句。在这种情况下,你只需“步进”一次,因为此语句只有一个结果。稍后在本教程中,你将看到何时需要多次执行单个语句。
  4. 你必须始终在编译语句上调用 sqlite3_finalize() 以删除它并避免资源泄漏。一旦声明完成,你就不应该再次使用它。

现在,将以下方法调用添加到 Playground

1
createTable()

运行你的 Playground;你应该看到控制台输出中出现以下内容:

1
Contact table created.

现在你有了一个表,是时候向它添加一些数据了。你将添加 Id 为 1 且 Name 为“Ray”的单行。

插入一些数据


将以下 SQL 语句添加到 Playground 的底部:

1
let insertStatementString = "INSERT INTO Contact (Id, Name) VALUES (?, ?);"

如果你没有太多的 SQL 经验,这可能看起来有点奇怪。为什么 values 由问号代表?

在使用 sqlite3_prepare_v2() 编译语句时,请记住上面的内容?这个 ? 语法告诉编译器在实际执行语句时将提供实际值。

这有性能方面的考虑,并且允许你提前编译语句,这可以提高性能,因为编译是一项代价高昂的操作。然后可以使用不同的值重复使用已编译的语句。

接下来,在你的 Playground 中创建以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func insert() {
var insertStatement: OpaquePointer? = nil

// 1
if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
let id: Int32 = 1
let name: NSString = "Ray"

// 2
sqlite3_bind_int(insertStatement, 1, id)
// 3
sqlite3_bind_text(insertStatement, 2, name.utf8String, -1, nil)

// 4
if sqlite3_step(insertStatement) == SQLITE_DONE {
print("Successfully inserted row.")
} else {
print("Could not insert row.")
}
} else {
print("INSERT statement could not be prepared.")
}
// 5
sqlite3_finalize(insertStatement)
}

以下是上述方法的工作原理:

  1. 首先,编译语句并验证一切正常;
  2. 在这里,你为值定义一个 ? 占位符。函数的名称 - sqlite3_bind_int() - 意味着你将 Int 值绑定到语句。函数的第一个参数是要绑定的语句,而第二个参数是基于非零的索引 ? 的位置。第三个也是最后一个参数是值本身。此绑定调用返回状态代码,但现在你认为它成功;
  3. 执行相同的绑定过程,但这次是文本值。此次调用还有两个附加参数;出于本教程的目的,你可以简单地为它们传递 -1nil。如果你愿意,可这里此处阅读有关绑定参数的更多信息;
  4. 使用 sqlite3_step()函数执行语句并验证它是否已完成;
  5. 一如既往,最后执行 finalize 语句。如果你要插入多个联系人,则可能会保留该语句并使用不同的值重新使用它。

接下来,通过将以下内容添加到 Playground 中来调用你的新方法:

1
insert()

运行你的 Playground 并验证你在控制台输出中看到以下内容:

1
Successfully inserted row.

挑战:多个插入


挑战的时间!你的任务是更新 insert() 以插入联系人数组。

作为提示,你需要在再次执行之前调用 sqlite3_reset() 将已编译的语句重置回其初始状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func insert() {

var insertStatement: OpaquePointer? = nil
// 1
let names: [NSString] = ["Ray", "Chris", "Martha", "Danielle"]

if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {

// 2
for (index, name) in names.enumerated() {
// 3
let id = Int32(index + 1)
sqlite3_bind_int(insertStatement, 1, id)
sqlite3_bind_text(insertStatement, 2, name.utf8String, -1, nil)

if sqlite3_step(insertStatement) == SQLITE_DONE {
print("Successfully inserted row.")
} else {
print("Could not insert row.")
}
// 4
sqlite3_reset(insertStatement)
}

sqlite3_finalize(insertStatement)
} else {
print("INSERT statement could not be prepared.")
}
}

正如你所看到的,代码与你已有的代码非常相似,但具有以下显着差异:

  1. 现在有一系列联系人,而不是一个常数;
  2. 对每个联系人列出一次数组;
  3. 现在,索引是从枚举的索引生成的,该索引对应于数组中联系人姓名的位置;
  4. SQL 语句在每个枚举结束时重置,以便下一个可以使用它。

查询联系人


既然你已经插入了一两行,那么确定它们真的存在就确实很好!:]

将以下内容添加到 Playground

1
let queryStatementString = "SELECT * FROM Contact;"

此查询只是从联系人表中检索所有记录。使用 * 表示将返回所有列。

添加以下方法以执行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func query() {
var queryStatement: OpaquePointer? = nil
// 1
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
// 2
if sqlite3_step(queryStatement) == SQLITE_ROW {
// 3
let id = sqlite3_column_int(queryStatement, 0)

// 4
let queryResultCol1 = sqlite3_column_text(queryStatement, 1)
let name = String(cString: queryResultCol1!)

// 5
print("Query Result:")
print("\(id) | \(name)")

} else {
print("Query returned no results")
}
} else {
print("SELECT statement could not be prepared")
}

// 6
sqlite3_finalize(queryStatement)
}

依次记录每个编号的评论:

  1. 准备声明。
  2. 执行该语句。请注意,你现在正在检查状态代码 SQLITE_ROW,这意味着你在逐步执行结果时检索了一行。
  3. 是时候从返回的行中读取值了。根据你对表的结构和查询的了解,你可以逐列访问行的值。第一列是 Int,因此你使用 sqlite3_column_int()并传入语句和从零开始的列索引。你将返回的值分配给本地范围的 id 常量。
  4. 接下来,从 Name 列中获取文本值。由于 C API,这有点乱。首先,将值捕获为 queryResultCol1,以便在下一行将其转换为正确的 Swift 字符串。
  5. 打印出结果。
  6. 执行 finalize 语句。

现在,通过将以下内容添加到 Playground 的底部来调用你的新方法:

1
query()

运行你的 Playground,你将会在控制台看到如下的输出:

1
2
Query Result:
1 | Ray

W00t!看起来你的数据已经录入到数据库中!

挑战:打印每一行


你的任务是更新 query() 以打印出表中的每个联系人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func query() {
var queryStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {

while (sqlite3_step(queryStatement) == SQLITE_ROW) {
let id = sqlite3_column_int(queryStatement, 0)
let queryResultCol1 = sqlite3_column_text(queryStatement, 1)
let name = String(cString: queryResultCol1!)
print("Query Result:")
print("\(id) | \(name)")
}

} else {
print("SELECT statement could not be prepared")
}
sqlite3_finalize(queryStatement)
}

请注意,不是像前面那样使用单个步骤来检索第一行,而是这次使用 while 循环来执行步骤,只要返回代码是 SQLITE_ROW 就会发生。当你到达最后一行时,返回代码将通过 SQLITE_DONE,循环将中断。

更新联系人


下一个自然的进展是更新现有行。你应该开始看到一种模式出现了。

首先,创建 UPDATE 语句:

1
let updateStatementString = "UPDATE Contact SET Name = 'Chris' WHERE Id = 1;"

在这里使用真正的值来代替占位符 。通常你会使用占位符并执行适当的语句绑定,但为了简洁起见,你可以在这里跳过它。

接下来,将以下方法添加到 Playground

1
2
3
4
5
6
7
8
9
10
11
12
13
func update() {
var updateStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
if sqlite3_step(updateStatement) == SQLITE_DONE {
print("Successfully updated row.")
} else {
print("Could not update row.")
}
} else {
print("UPDATE statement could not be prepared")
}
sqlite3_finalize(updateStatement)
}

这与你之前看到的类似:preparestepfinalize!并将以下内容添加到你的 Playground

1
2
update()
query()

这将执行你的新方法,然后调用你先前定义的 query() 方法,以便你可以看到结果:

1
2
3
Successfully updated row.
Query Result:
1 | Chris

恭喜你成功更新一行数据!非常容易的对吧?:]

删除联系人


成为 SQLite 忍者的最后一步是删除你创建的行。再次,你将使用熟悉的 preparestepfinalize

将以下的内容添加到 Playground

1
let deleteStatementStirng = "DELETE FROM Contact WHERE Id = 1;"

现在添加以下方法来执行语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func delete() {
var deleteStatement: OpaquePointer? = nil
if sqlite3_prepare_v2(db, deleteStatementStirng, -1, &deleteStatement, nil) == SQLITE_OK {
if sqlite3_step(deleteStatement) == SQLITE_DONE {
print("Successfully deleted row.")
} else {
print("Could not delete row.")
}
} else {
print("DELETE statement could not be prepared")
}

sqlite3_finalize(deleteStatement)
}

你现在感觉到了吗?Preparestepfinalize!:]

执行这个新方法,然后调用 query(),如下所示:

1
2
delete()
query()

现在运行你的 Playground,你应该在你的控制台中看到以下输出:

1
2
Successfully deleted row.
Query returned no results

注意:如果你完成了上面的“多个插入”挑战,由于表中仍存在其他数据,因此输出可能与上面的内容略有不同。

处理错误


到目前为止,希望你已经设法避免 SQLite 错误。但是,当你调用一个没有意义的函数,或者根本无法编译时,就将会出现错误。在发生这些事情时处理错误消息可以节省大量的开发时间;
它还使你有机会向用户显示有意义的错误消息。将以下声明 - 这个错误是故意的 - 添加到你的 Playground:

1
let malformedQueryString = "SELECT Stuff from Things WHERE Whatever;"

现在添加一个方法来执行这个格式错误的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func prepareMalformedQuery() {
var malformedStatement: OpaquePointer? = nil
// 1
if sqlite3_prepare_v2(db, malformedQueryString, -1, &malformedStatement, nil) == SQLITE_OK {
print("This should not have happened.")
} else {
// 2
let errorMessage = String.init(cString: sqlite3_errmsg(db))
print("Query could not be prepared! \(errorMessage)")
}

// 3
sqlite3_finalize(malformedStatement)
}

以下是你将如何强制执行错误:

  1. Prepare 语句,这将会发生错误而且不应该返回 SQLITE_OK
  2. 使用 sqlite3_errmsg() 从数据库中获取错误消息;此函数返回最近错误的文本描述。然后,你将错误打印到控制台;
  3. 一如既往,finalize

调用该方法以查看错误消息:

1

prepareMalformedQuery()

运行你的 Playground,你将会在控制台看到如下的输出:

1
Query could not be prepared! no such table: Things

嗯,这实际上很有帮助 - 你显然无法在不存在的表上运行 SELECT 语句!

关闭数据库连接


完成数据库连接后,你将负责关闭它。但请注意 - 在成功关闭数据库之前,必须执行许多操作,如SQLite文档中所述。

调用 close 函数,如下所示:

1

sqlite3_close(db)

运行你的 Playground;你应该在 Playground 的右侧结果视图中看到状态代码 0;这表示 SQLITE_OK,这意味着数据库关闭成功。

你已经成功创建了一个数据库,添加了一个表,向表中添加了行,查询并更新了这些行,甚至删除了一行 - 所有这些都使用了 Swift 的 SQLite C API。做得好!

在下一节中,你将学习如何在 Swift 中使用 SQLite。

SQLite 与 Swift


作为 Swift 开发人员,你可能会对本教程第一部分中发生的事情感到有些不安。那个 C API 有点痛苦,但好消息是你可以利用 Swift 的力量包装那些 C 例程来让事情变得更容易。

对于本教程的这部分内容,点击 Playground 底部的 Making it Swift 连接打开这部分的 Playground。

包装错误


作为一个 Swift 开发者,从 C API 捕获错误有点尴尬。在这个美丽的新世界中,检查结果码然后调用另一个方法是没有意义的。如果方法能够抛出错误那将会更有意义。

将下面的代码添加到你的 Playground:

1
2
3
4
5
6
enum SQLiteError: Error {
case OpenDatabase(message: String)
case Prepare(message: String)
case Step(message: String)
case Bind(message: String)
}

这是一个自定义的错误枚举,涵盖了你正在使用的四个可能失败的主要操作。请注意,每个 case 都有一个关联值 message

包装数据库连接


另外一个不那么 Swifty 的方面就是使用那些恶心的 OpaquePointer 类型。在自己的类中包装数据库的连接指针,如下所示:

1
2
3
4
5
6
7
8
9
10
11
class SQLiteDatabase {
fileprivate let dbPointer: OpaquePointer?

fileprivate init(dbPointer: OpaquePointer?) {
self.dbPointer = dbPointer
}

deinit {
sqlite3_close(dbPointer)
}
}

这样看起来好多了,当你需要一个数据库连接的时候,你能创建一个更有意义的 SQLiteDatabase 的引用,而不是 OpaquePointer

你会注意到,初始化是 fileprivate;那是因为你不希望你的 Swift 开发者传入那个 OpaquePointer。相反,你让他们用数据库文件的路径实例化这个类。

将以下静态方法添加到 SQLiteDatabase,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static func open(path: String) throws -> SQLiteDatabase {
var db: OpaquePointer? = nil
// 1
if sqlite3_open(path, &db) == SQLITE_OK {
// 2
return SQLiteDatabase(dbPointer: db)
} else {
// 3
defer {
if db != nil {
sqlite3_close(db)
}
}

if let errorPointer = sqlite3_errmsg(db) {
let message = String.init(cString: errorPointer)
throw SQLiteError.OpenDatabase(message: message)
} else {
throw SQLiteError.OpenDatabase(message: "No error message provided from sqlite.")
}
}
}

这里发生了这些事情;

  1. 尝试在提供的路径上打开数据库;
  2. 如果成功,则返回 SQLiteDatabase 的新实例;
  3. 否则,如果状态代码不是 SQLITE_OK,则推迟关闭数据库并抛出错误。

现在,您可以使用更清晰的语法创建和打开数据库连接。

将下面的代码添加到你的 PLayground:

1
2
3
4
5
6
7
8
let db: SQLiteDatabase
do {
db = try SQLiteDatabase.open(path: part2DbPath)
print("Successfully opened connection to database.")
} catch SQLiteError.OpenDatabase(let message) {
print("Unable to open database. Verify that you created the directory described in the Getting Started section.")
PlaygroundPage.current.finishExecution()
}

Ah,我太喜欢 Swift 了。这里,尝试打开数据库的代码被包装在 do-try-catch 中,并且 SQLite 会将错误的信息传递给 catch 块儿,这要感谢你之前创建的自定义枚举。

运行你的 Playground 并查看控制台的输出;你将会看到如下的内容:

1
Successfully opened connection to database.

现在,您可以使用并检查数据库实例作为正确且有意义的类型。

在继续编写执行语句之前,如果 SQLiteDatabase 允许您轻松访问SQLite错误消息,那将是很好的。

将以下计算属性添加到 SQLiteDatabase

1
2
3
4
5
6
7
8
fileprivate var errorMessage: String {
if let errorPointer = sqlite3_errmsg(dbPointer) {
let errorMessage = String(cString: errorPointer)
return errorMessage
} else {
return "No error message provided from sqlite."
}
}

在这里,您添加了一个计算属性,它只返回 SQLite 知道的最新错误。如果没有错误,它只会返回一条声明的通用消息。

包装 Prepare 语句的调用


既然你经常这样做,像其他方法一样包装它将会更有意义。在你进行开发并且向 SQLiteDatabase 中添加功能时,你会用到类的扩展。

添加如下的扩展,将来的方法将使用它来调用 SQL 语句上的 sqlite3_prepare_v2()

1
2
3
4
5
6
7
8
9
10
extension SQLiteDatabase {
func prepareStatement(sql: String) throws -> OpaquePointer? {
var statement: OpaquePointer? = nil
guard sqlite3_prepare_v2(dbPointer, sql, -1, &statement, nil) == SQLITE_OK else {
throw SQLiteError.Prepare(message: errorMessage)
}

return statement
}
}

这里你定义的 prepareStatement(_:) 函数能够抛出错误,然后当 sqlite3_prepare_v2() 出错时使用 guard 语句来抛出错误。就像之前一样,你将 SQLite中 的错误消息传递给自定义枚举的相关案例。

创建 Contact 结构体


在这些例子中,你将使用与之前相同的 Contact 表,因此,定义一个适当的结构来表示联系人是有意义的。将以下内容添加到你的 Playground:

1
2
3
4
struct Contact {
let id: Int32
let name: NSString
}

包装表的创建


您将完成与以前相同的数据库任务,但这次您将使用更 Swifter 的方法。

要创建一个表,你需要一个 CREATE TABLE 的 SQL 语句。Contact 定义自己的 CREATE TABLE 语句是有意义的。

为了这个目的,我们我们创建下面的协议:

1
2
3
protocol SQLTable {
static var createStatement: String { get }
}

现在,扩展 Contact,然后遵守这个协议:

1
2
3
4
5
6
7
8
9
10
extension Contact: SQLTable {
static var createStatement: String {
return """
CREATE TABLE Contact(
Id INT PRIMARY KEY NOT NULL,
Name CHAR(255)
);
"""
}
}

现在,你可以编写以下方法来接受一个符合 SQLTable 的类型来创建表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension SQLiteDatabase {
func createTable(table: SQLTable.Type) throws {
// 1
let createTableStatement = try prepareStatement(sql: table.createStatement)
// 2
defer {
sqlite3_finalize(createTableStatement)
}
// 3
guard sqlite3_step(createTableStatement) == SQLITE_DONE else {
throw SQLiteError.Step(message: errorMessage)
}
print("\(table) table created.")
}
}

这里来分析发生了什么:

  1. prepareStatement() 会抛出错误,所以你必须使用 try 语句。你并没有在 do-try-catch 块儿中执行此操作,因为这个方法本身会抛出错误,所以任何来自 prepareStatement() 的错误都会简单的抛出给调用者 createTable()
  2. 凭借 defer 的力量,无论此方法如何退出其执行范围,你都可以确保你的 sqlite3_finalize 语句始终最终执行;
  3. guard 能让你写的检查 SQL 状态代码更具可读性。

通过将以下的代码添加到 Playground 来尝试尝试新的方法:

1
2
3
4
5
do {
try db.createTable(table: Contact.self)
} catch {
print(db.errorMessage)
}

在这里,您只需尝试创建联系人,并捕获错误(如果有的话)。

运行你的 Playground;你将会在你的控制台看到如下的输出:

1
Contact table created.

太棒了!这不是一个更清洁的 API 吗?

包装数据插入


沿着右边移动,是时候向你的 Contact 表中插入一条数据了。添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
extension SQLiteDatabase {
func insertContact(contact: Contact) throws {
let insertSql = "INSERT INTO Contact (Id, Name) VALUES (?, ?);"
let insertStatement = try prepareStatement(sql: insertSql)
defer {
sqlite3_finalize(insertStatement)
}

let name: NSString = contact.name
guard sqlite3_bind_int(insertStatement, 1, contact.id) == SQLITE_OK &&
sqlite3_bind_text(insertStatement, 2, name.utf8String, -1, nil) == SQLITE_OK else {
throw SQLiteError.Bind(message: errorMessage)
}

guard sqlite3_step(insertStatement) == SQLITE_DONE else {
throw SQLiteError.Step(message: errorMessage)
}

print("Successfully inserted row.")
}
}

既然你已经得到了你的 SQLegs - 看看我在那里做了什么?:] - 这段代码不应该太令人惊讶。给定一个 Contact 实例,你准备一个语句,绑定值,执行然后 finalize 操作。同样,使用 deferguardthrow 的强大组合可以让您充分利用现代语言 Swift 的功能。

编写代码来调用这个新方法,如下所示:

1
2
3
4
5
do {
try db.insertContact(contact: Contact(id: 1, name: "Ray"))
} catch {
print(db.errorMessage)
}

运行你的 Playground;你将会在你的控制台看到如下的输出:

1
Successfully inserted row.

包装读的操作


包装起来(抱歉,我无法抗拒!)的这部分是用 Swift 创建的数据库查询。

添加以下方法以查询联系人的数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
extension SQLiteDatabase {
func contact(id: Int32) -> Contact? {
let querySql = "SELECT * FROM Contact WHERE Id = ?;"
guard let queryStatement = try? prepareStatement(sql: querySql) else {
return nil
}

defer {
sqlite3_finalize(queryStatement)
}

guard sqlite3_bind_int(queryStatement, 1, id) == SQLITE_OK else {
return nil
}

guard sqlite3_step(queryStatement) == SQLITE_ROW else {
return nil
}

let id = sqlite3_column_int(queryStatement, 0)

let queryResultCol1 = sqlite3_column_text(queryStatement, 1)
let name = String(cString: queryResultCol1!) as NSString

return Contact(id: id, name: name)
}
}

此方法只接受联系人的 id 并返回该联系人,如果没有该 id 的联系人,则返回 nil。同样,这些语句现在应该有些熟悉了。

写一个查询第一个联系人的代码:

1
2
let first = db.contact(id: 1)
print("\(first?.id) \(first?.name)")

运行你的 Playground;你应该能在控制台看到如下的输出:

1
Optional(1) Optional(Ray)

到目前为止,您可能已经确定了一些可以用通用方式创建的调用,并将它们应用于完全不同的表。上述练习的目的是展示如何使用 Swift 来包装低级 C 的 API。对于 SQLite 来说,这不是一项简单的任务;SQLite 有很多错综复杂的内容,这里没有涉及。

你可能会想“没有人已经为此创建了一个包装器吗?” - 让我现在回答你的问题!

SQLite.swift 的介绍


Stephen Celis 慷慨地为 SQLite 编写了一个名为 SQLite.swift 的全功能 Swift 包装器。如果您认为 SQLite 适合您应用中的数据存储,我强烈建议您查看一下。

SQLite.swift 提供了一种表示表的表达方式,让您可以开始使用 SQLite - 而无需担心 SQLite 的许多底层细节和特性。您甚至可以考虑包装SQLite.swift 本身,为您的应用程序的域模型创建一个高级 API。

查看编写良好的 README.md for SQLite.swift,并自行决定它是否在您的个人代码工具箱中占有一席之地。

原文地址:SQLite With Swift Tutorial: Getting Started