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,还有许多其他数据持久性替代方案,包括 Realm,Couchbase Lite,Firebase 和 NSCoding。
每个都有自己的优点和缺点 - 包括 SQLite 本身,数据持久性没有灵丹妙药,作为开发人员,你可以根据应用程序的要求确定哪个选项超过其他选项。
SQLite 确实有一些优点:
- 随 iOS 一起提供,因此它不会为你的应用程序包增加任何开销。
- 试过并经过测试; 1.0 版于 2000 年 8 月发布。
- 开源。
- 适用于数据库开发人员和管理员的熟悉查询语言。
- 跨平台
SQLite 的缺点可能是非常主观和自以为是,所以我们将把研究留给你了!:]
C 的 API
这部分 SQLite with Swift 教程将引导你完成最常见和最基本的 SQLite API。你很快就会意识到在 Swift 方法中包装 C API 将是理想的选择,但要紧紧抓住并首先完成 C 代码; 你将在本教程的第二部分做一些包装。
打开连接
在做任何事情之前,你首先需要创建一个数据库连接。
在 Playground
的“Getting Started”部分下添加以下方法:
1 | func openDatabase() -> OpaquePointer? { |
上面的方法调用 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 KEY
;name
是一个 CHAR(255)
类型。
添加以下字符串,其中包含创建表所需的 SQL 语句:
1 | let createTableString = """ |
请注意,你正在使用 Swift 4 的便捷多语法来编写此语句!
接下来,添加执行 CREATE TABLE
SQL 语句的方法:
1 | func createTable() { |
我们来一步一步的分析:
- 首先,在下一步中创建一个指针。
sqlite3_prepare_v2()
将 SQL 语句编译为字节代码并返回状态代码 - 在对数据库执行任意语句之前的重要步骤。如果你有兴趣,可以在这里找到更多信息。检查返回的状态代码以确保语句编译成功。如果是,则该过程转到步骤3; 否则,你打印一条消息,指出该语句无法编译。sqlite3_step()
运行已编译的语句。在这种情况下,你只需“步进”一次,因为此语句只有一个结果。稍后在本教程中,你将看到何时需要多次执行单个语句。- 你必须始终在编译语句上调用
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 | func insert() { |
以下是上述方法的工作原理:
- 首先,编译语句并验证一切正常;
- 在这里,你为值定义一个
?
占位符。函数的名称 -sqlite3_bind_int()
- 意味着你将Int
值绑定到语句。函数的第一个参数是要绑定的语句,而第二个参数是基于非零的索引?
的位置。第三个也是最后一个参数是值本身。此绑定调用返回状态代码,但现在你认为它成功; - 执行相同的绑定过程,但这次是文本值。此次调用还有两个附加参数;出于本教程的目的,你可以简单地为它们传递
-1
和nil
。如果你愿意,可这里此处阅读有关绑定参数的更多信息; - 使用
sqlite3_step()
函数执行语句并验证它是否已完成; - 一如既往,最后执行
finalize
语句。如果你要插入多个联系人,则可能会保留该语句并使用不同的值重新使用它。
接下来,通过将以下内容添加到 Playground
中来调用你的新方法:
1 | insert() |
运行你的 Playground
并验证你在控制台输出中看到以下内容:
1 | Successfully inserted row. |
挑战:多个插入
挑战的时间!你的任务是更新 insert()
以插入联系人数组。
作为提示,你需要在再次执行之前调用 sqlite3_reset()
将已编译的语句重置回其初始状态。
1 | func insert() { |
正如你所看到的,代码与你已有的代码非常相似,但具有以下显着差异:
- 现在有一系列联系人,而不是一个常数;
- 对每个联系人列出一次数组;
- 现在,索引是从枚举的索引生成的,该索引对应于数组中联系人姓名的位置;
- SQL 语句在每个枚举结束时重置,以便下一个可以使用它。
查询联系人
既然你已经插入了一两行,那么确定它们真的存在就确实很好!:]
将以下内容添加到 Playground
:
1 | let queryStatementString = "SELECT * FROM Contact;" |
此查询只是从联系人表中检索所有记录。使用 *
表示将返回所有列。
添加以下方法以执行查询:
1 | func query() { |
依次记录每个编号的评论:
- 准备声明。
- 执行该语句。请注意,你现在正在检查状态代码
SQLITE_ROW
,这意味着你在逐步执行结果时检索了一行。 - 是时候从返回的行中读取值了。根据你对表的结构和查询的了解,你可以逐列访问行的值。第一列是
Int
,因此你使用sqlite3_column_int()
并传入语句和从零开始的列索引。你将返回的值分配给本地范围的id
常量。 - 接下来,从
Name
列中获取文本值。由于 C API,这有点乱。首先,将值捕获为queryResultCol1
,以便在下一行将其转换为正确的 Swift 字符串。 - 打印出结果。
- 执行
finalize
语句。
现在,通过将以下内容添加到 Playground
的底部来调用你的新方法:
1 | query() |
运行你的 Playground
,你将会在控制台看到如下的输出:
1 | Query Result: |
W00t!看起来你的数据已经录入到数据库中!
挑战:打印每一行
你的任务是更新 query()
以打印出表中的每个联系人。
1 | func query() { |
请注意,不是像前面那样使用单个步骤来检索第一行,而是这次使用 while
循环来执行步骤,只要返回代码是 SQLITE_ROW
就会发生。当你到达最后一行时,返回代码将通过 SQLITE_DONE
,循环将中断。
更新联系人
下一个自然的进展是更新现有行。你应该开始看到一种模式出现了。
首先,创建 UPDATE
语句:
1 | let updateStatementString = "UPDATE Contact SET Name = 'Chris' WHERE Id = 1;" |
在这里使用真正的值来代替占位符 ?
。通常你会使用占位符并执行适当的语句绑定,但为了简洁起见,你可以在这里跳过它。
接下来,将以下方法添加到 Playground
:
1 | func update() { |
这与你之前看到的类似:prepare
,step
,finalize
!并将以下内容添加到你的 Playground
:
1 | update() |
这将执行你的新方法,然后调用你先前定义的 query()
方法,以便你可以看到结果:
1 | Successfully updated row. |
恭喜你成功更新一行数据!非常容易的对吧?:]
删除联系人
成为 SQLite
忍者的最后一步是删除你创建的行。再次,你将使用熟悉的 prepare
,step
和 finalize
。
将以下的内容添加到 Playground
:
1 | let deleteStatementStirng = "DELETE FROM Contact WHERE Id = 1;" |
现在添加以下方法来执行语句:
1 | func delete() { |
你现在感觉到了吗?Prepare
,step
和 finalize
!:]
执行这个新方法,然后调用 query()
,如下所示:
1 | delete() |
现在运行你的 Playground
,你应该在你的控制台中看到以下输出:
1 | Successfully deleted row. |
注意:如果你完成了上面的“多个插入”挑战,由于表中仍存在其他数据,因此输出可能与上面的内容略有不同。
处理错误
到目前为止,希望你已经设法避免 SQLite 错误。但是,当你调用一个没有意义的函数,或者根本无法编译时,就将会出现错误。在发生这些事情时处理错误消息可以节省大量的开发时间;
它还使你有机会向用户显示有意义的错误消息。将以下声明 - 这个错误是故意的 - 添加到你的 Playground:
1 | let malformedQueryString = "SELECT Stuff from Things WHERE Whatever;" |
现在添加一个方法来执行这个格式错误的语句:
1 | func prepareMalformedQuery() { |
以下是你将如何强制执行错误:
Prepare
语句,这将会发生错误而且不应该返回SQLITE_OK
;- 使用
sqlite3_errmsg()
从数据库中获取错误消息;此函数返回最近错误的文本描述。然后,你将错误打印到控制台; - 一如既往,
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 | enum SQLiteError: Error { |
这是一个自定义的错误枚举,涵盖了你正在使用的四个可能失败的主要操作。请注意,每个 case
都有一个关联值 message
。
包装数据库连接
另外一个不那么 Swifty
的方面就是使用那些恶心的 OpaquePointer
类型。在自己的类中包装数据库的连接指针,如下所示:
1 | class SQLiteDatabase { |
这样看起来好多了,当你需要一个数据库连接的时候,你能创建一个更有意义的 SQLiteDatabase
的引用,而不是 OpaquePointer
。
你会注意到,初始化是 fileprivate
;那是因为你不希望你的 Swift 开发者传入那个 OpaquePointer
。相反,你让他们用数据库文件的路径实例化这个类。
将以下静态方法添加到 SQLiteDatabase
,如下所示:
1 | static func open(path: String) throws -> SQLiteDatabase { |
这里发生了这些事情;
- 尝试在提供的路径上打开数据库;
- 如果成功,则返回
SQLiteDatabase
的新实例; - 否则,如果状态代码不是
SQLITE_OK
,则推迟关闭数据库并抛出错误。
现在,您可以使用更清晰的语法创建和打开数据库连接。
将下面的代码添加到你的 PLayground:
1 | let db: SQLiteDatabase |
Ah,我太喜欢 Swift 了。这里,尝试打开数据库的代码被包装在 do-try-catch
中,并且 SQLite 会将错误的信息传递给 catch
块儿,这要感谢你之前创建的自定义枚举。
运行你的 Playground 并查看控制台的输出;你将会看到如下的内容:
1 | Successfully opened connection to database. |
现在,您可以使用并检查数据库实例作为正确且有意义的类型。
在继续编写执行语句之前,如果 SQLiteDatabase
允许您轻松访问SQLite错误消息,那将是很好的。
将以下计算属性添加到 SQLiteDatabase
:
1 | fileprivate var errorMessage: String { |
在这里,您添加了一个计算属性,它只返回 SQLite 知道的最新错误。如果没有错误,它只会返回一条声明的通用消息。
包装 Prepare 语句的调用
既然你经常这样做,像其他方法一样包装它将会更有意义。在你进行开发并且向 SQLiteDatabase
中添加功能时,你会用到类的扩展。
添加如下的扩展,将来的方法将使用它来调用 SQL 语句上的 sqlite3_prepare_v2()
:
1 | extension SQLiteDatabase { |
这里你定义的 prepareStatement(_:)
函数能够抛出错误,然后当 sqlite3_prepare_v2()
出错时使用 guard
语句来抛出错误。就像之前一样,你将 SQLite中 的错误消息传递给自定义枚举的相关案例。
创建 Contact 结构体
在这些例子中,你将使用与之前相同的 Contact
表,因此,定义一个适当的结构来表示联系人是有意义的。将以下内容添加到你的 Playground:
1 | struct Contact { |
包装表的创建
您将完成与以前相同的数据库任务,但这次您将使用更 Swifter 的方法。
要创建一个表,你需要一个 CREATE TABLE
的 SQL 语句。Contact
定义自己的 CREATE TABLE
语句是有意义的。
为了这个目的,我们我们创建下面的协议:
1 | protocol SQLTable { |
现在,扩展 Contact
,然后遵守这个协议:
1 | extension Contact: SQLTable { |
现在,你可以编写以下方法来接受一个符合 SQLTable
的类型来创建表:
1 | extension SQLiteDatabase { |
这里来分析发生了什么:
prepareStatement()
会抛出错误,所以你必须使用try
语句。你并没有在do-try-catch
块儿中执行此操作,因为这个方法本身会抛出错误,所以任何来自prepareStatement()
的错误都会简单的抛出给调用者createTable()
;- 凭借
defer
的力量,无论此方法如何退出其执行范围,你都可以确保你的sqlite3_finalize
语句始终最终执行; guard
能让你写的检查 SQL 状态代码更具可读性。
通过将以下的代码添加到 Playground 来尝试尝试新的方法:
1 | do { |
在这里,您只需尝试创建联系人,并捕获错误(如果有的话)。
运行你的 Playground;你将会在你的控制台看到如下的输出:
1 | Contact table created. |
太棒了!这不是一个更清洁的 API 吗?
包装数据插入
沿着右边移动,是时候向你的 Contact
表中插入一条数据了。添加如下代码:
1 | extension SQLiteDatabase { |
既然你已经得到了你的 SQLegs - 看看我在那里做了什么?:] - 这段代码不应该太令人惊讶。给定一个 Contact
实例,你准备一个语句,绑定值,执行然后 finalize
操作。同样,使用 defer
,guard
和 throw
的强大组合可以让您充分利用现代语言 Swift 的功能。
编写代码来调用这个新方法,如下所示:
1 | do { |
运行你的 Playground;你将会在你的控制台看到如下的输出:
1 | Successfully inserted row. |
包装读的操作
包装起来(抱歉,我无法抗拒!)的这部分是用 Swift 创建的数据库查询。
添加以下方法以查询联系人的数据库:
1 | extension SQLiteDatabase { |
此方法只接受联系人的 id
并返回该联系人,如果没有该 id
的联系人,则返回 nil
。同样,这些语句现在应该有些熟悉了。
写一个查询第一个联系人的代码:
1 | let first = db.contact(id: 1) |
运行你的 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