版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.12 星期五 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
SQLite With Swift
作为Swift开发人员,您可能会对上一篇中发生的事情感到有些不安。 那个C API有点痛苦,但好消息是你可以利用Swift的力量并封装那些C例程来让事情变得更容易。
对于SQLite with Swift
教程的这一部分,单击playground
底部的Making it Swift
链接打开此部分的playground
:
Wrapping Errors - 封装错误
作为Swift开发人员,从C API
获取错误有点尴尬。 在这个勇敢的新世界中检查结果代码然后调用另一种方法是没有意义的。 如果可以失败的方法抛出错误会更有意义。
将以下内容添加到您的playground
:
enum SQLiteError: Error {
case OpenDatabase(message: String)
case Prepare(message: String)
case Step(message: String)
case Bind(message: String)
}
这是一个自定义的Error
枚举,涵盖了您正在使用的四个可能失败的主要操作。 请注意每个case
如何具有将保存错误消息的关联值。
Wrapping the Database Connection - 封装数据库连接
另一个不那么Swifty的方面是使用那些OpaquePointer
类型。
在自己的类中包装数据库连接指针,如下所示:
class SQLiteDatabase {
fileprivate let dbPointer: OpaquePointer?
fileprivate init(dbPointer: OpaquePointer?) {
self.dbPointer = dbPointer
}
deinit {
sqlite3_close(dbPointer)
}
}
这看起来好多了。 当您需要数据库连接时,可以创建对更有意义的SQLiteDatabase
类型的引用,而不是OpaquePointer。
您会注意到初始化程序是fileprivate
;那是因为你不希望你的Swift开发人员传入那个OpaquePointer
。 相反,您让它们使用数据库文件的路径实例化此类。
将以下静态方法添加到SQLiteDatabase
,如下所示:
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
,则defer
关闭数据库并抛出错误。
现在,您可以使用更清晰的语法创建和打开数据库连接。
将以下内容添加到您的playground
:
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()
}
更喜欢Swift。 这里,打开数据库的尝试包含在do-try-catch
块中,并且由于您之前添加的自定义枚举,SQLite的错误消息将传递给catch
块。
Run你的playground
,看看控制台输出;你会看到如下内容:
Successfully opened connection to database.
现在,您可以作为正确且有意义的类型使用并检查db
实例。
在继续编写执行语句的方法之前,如果SQLiteDatabase
允许您轻松访问SQLite
错误消息,那就太好了。
将以下计算属性添加到SQLiteDatabase
:
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
知道的最新错误。 如果没有错误,它只会返回一条说明同样多的通用消息。
Wrapping the Prepare Call - 封装Prepare调用
由于您经常用这个,因此像其他方法一样包装它是有意义的。 在向前发展并向SQLiteDatabase
类添加功能时,您将使用类扩展。
添加以下扩展(将来的方法将使用它)在SQL语句上调用sqlite3_prepare_v2()
:
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(_ :)
可以抛出错误,然后使用guard
在sqlite3_prepare_v2()
失败时抛出该错误。 就像以前一样,您将SQLite中的错误消息传递给自定义枚举的相关case
。
Creating a Contact Struct - 创建Contact结构体
在这些示例中,您将使用与以前相同的Contact
表,因此定义适当的struct
来表示联系人是有意义的。 将以下内容添加到playground
:
struct Contact {
let id: Int32
let name: NSString
}
Wrapping the Table Creation - 包装表创建
您将完成与以前相同的数据库任务,但这次您将使用“Swifter”
方法。
要创建表,需要CREATE TABLE
SQL语句。 Contact
定义自己的CREATE TABLE
语句是有意义的。
为此目的创建以下协议:
protocol SQLTable {
static var createStatement: String { get }
}
现在,扩展Contact
以提供对此新协议的遵守:
extension Contact: SQLTable {
static var createStatement: String {
return """
CREATE TABLE Contact(
Id INT PRIMARY KEY NOT NULL,
Name CHAR(255)
);
"""
}
}
现在,您可以编写以下方法来接受符合SQLTable
的类型来创建表:
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
,无论此方法如何退出其范围,您都可以确保您的语句始终最终finalized
; - 3)
guard
允许您为SQLite
状态代码编写更具表现力的检查。
通过将以下内容添加到您的playground
,尝试尝试新方法:
do {
try db.createTable(table: Contact.self)
} catch {
print(db.errorMessage)
}
在这里,您只需尝试创建Contact
,并捕获错误(如果有)。
Run你的playground
;您应该会在控制台中看到以下内容:
Contact table created.
太棒了! 这不是一个更简洁的API吗?
Wrapping Insertions - 包装插入
向右移动,是时候在Contact
表中插入一行了。 添加以下方法:
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
实例,您准备一个语句,绑定值,执行和完成。 同样,使用defer
,guard
和throw
的强大组合可以让您充分利用现代Swift语言功能。
编写代码来调用这个新方法,如下所示:
do {
try db.insertContact(contact: Contact(id: 1, name: "Ray"))
} catch {
print(db.errorMessage)
}
Run你的playground
,您应该在控制台中看到以下内容:
Successfully inserted row.
Wrapping Reads - 包装读取
关于创建Swift包装器的部分是查询数据库。
添加以下方法以查询联系人的数据库:
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
并返回该联系人,如果没有与该联系人的联系人,则返回nil
。 同样,这些陈述现在应该有些熟悉。
编写代码来查询第一个联系人:
let first = db.contact(id: 1)
print("\(first?.id) \(first?.name)")
Run你的playground
,您应该在控制台中看到以下输出:
Optional(1) Optional(Ray)
到目前为止,您可能已经确定了一些可以通用方式创建的调用,并将它们应用于完全不同的表。 上述练习的目的是展示如何使用Swift来包装低级C API。 对于SQLite来说,这不是一项简单的任务;SQLite有很多错综复杂的内容,这里没有涉及。
你可能会想“没有人已经为此创建了一个封装器吗?” - 让我现在回答你的问题!
Introducing SQLite.swift - 介绍SQLite.swift
Stephen Celis慷慨地为SQLite编写了一个名为SQLite.swift的全功能Swift包装器。 如果您认为SQLite适合您应用中的数据存储,我强烈建议您查看一下。
SQLite.swift
提供了一种表达表格的表达方式,让您可以开始使用SQLite - 而无需担心SQLite的许多底层细节和特性。 您甚至可以考虑包装SQLite.swift
本身,为您的应用程序的域模型创建一个高级API。
查看编写良好的README.md for SQLite.swift,并自行决定它是否在您的个人代码工具箱中占有一席之地。
我没有涉及的一件事是调试。 在许多情况下,您需要某种数据库浏览器才能看到底层发生了什么。 有许多不同的应用程序,从免费和开源到付费的闭源和商业支持。 这里有几个要看一看,但快速谷歌搜索将显示更多:
- DB Browser for SQLite - 免费
- SQLPro - 19.99美元
您还可以通过键入sqlite3 file.db
直接从终端访问SQLite
数据库。 从那里,您可以使用.help
命令查看命令列表,或者您可以直接在提示符下开始执行SQL语句。 有关命令行SQLite客户端的更多信息可以在on the main SQLite site上找到。
后记
本篇主要讲述了一个简单的基于SQLite持久化方案示例 - SQLite With Swift,感兴趣的给个赞或者关注~~~