数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)

版本记录

版本号 时间
V1.0 2018.10.12 星期五

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,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(_ :)可以抛出错误,然后使用guardsqlite3_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实例,您准备一个语句,绑定值,执行和完成。 同样,使用deferguardthrow的强大组合可以让您充分利用现代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,并自行决定它是否在您的个人代码工具箱中占有一席之地。

我没有涉及的一件事是调试。 在许多情况下,您需要某种数据库浏览器才能看到底层发生了什么。 有许多不同的应用程序,从免费和开源到付费的闭源和商业支持。 这里有几个要看一看,但快速谷歌搜索将显示更多:

您还可以通过键入sqlite3 file.db直接从终端访问SQLite数据库。 从那里,您可以使用.help命令查看命令列表,或者您可以直接在提示符下开始执行SQL语句。 有关命令行SQLite客户端的更多信息可以在on the main SQLite site上找到。

后记

本篇主要讲述了一个简单的基于SQLite持久化方案示例 - SQLite With Swift,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容