版本記錄
版本號 | 時間 |
---|---|
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,感興趣的給個贊或者關注~~~