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

版本记录

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

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。

开始

首先看一下写作环境

Swift 4, iOS 11, Xcode 9

本文将学习如何在Swift项目中使用SQLite数据库,包括插入,更新和删除行。

这个带有Swift的SQLite文章向您展示了如何在Swift中使用流行的数据库平台。 在软件开发领域,您需要很长时间才能保留应用数据。 在许多情况下,这是以数据结构的形式出现的。 但是,如何有效地存储它?

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

在整个文章中,您将学习如何执行以下数据库操作:

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

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

最后,我将简要介绍流行的开源Swift包装器SQLite.swift,以便您基本了解底层框架如何在包装器中工作。

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

打开已经编制好的SQLite的入门项目并打开SQLiteTutorial.xcworkspace。 从Project Navigator中打开Tutorial playground

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

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

这意味着它只会在您通过点击Play按钮显式调用执行时执行。

您可能还会在页面顶部看到destroyPart1Database()调用;您可以放心地忽略这一点,因为每次playground运行时都会销毁数据库文件。 这可确保在使用Swift教程浏览此SQLite时,所有语句都能成功执行。

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

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

Why Should I Choose SQLite? - 我为什么要选择SQLite?

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

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

SQLite确实有一些优点:

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

SQLite的缺点可能是非常主观的,就把研究留给你了!


The C API - C API

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

1. Opening a Connection - 打开连接

在做任何事情之前,您首先需要创建一个数据库连接。

playground开始部分下添加以下方法:

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

let db = openDatabase()

Play按钮运行playground并观看控制台输出。 如果控制台未打开,请按play按钮左侧的按钮:

如果openDatabase()成功,您将看到如下输出:

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

其中username是您的Home目录。

2. Creating a Table - 创建表

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

该表将包含两列; Id,是INTPRIMARY KEY;和Name,这是一个CHAR(255)

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

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

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

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

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()运行已编译的语句。 在这种情况下,您只需“步进”一次,因为此语句只有一个结果。 稍后在这个带有Swift教程的SQLite中,您将看到何时需要多次执行单个语句。
  • 4) 您必须始终在编译语句上调用sqlite3_finalize()以删除它并避免资源泄漏。 一旦声明完成,您就不应该再次使用它。

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

createTable()

Run你的playground,您应该看到控制台输出中出现以下内容:

Contact table created.

现在您有了一个表,是时候向它添加一些数据了。 您将添加Id1且名称为Ray的单行。

3. Inserting Some Data - 插入一些数据

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

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

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

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

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

接下来,在您的playground中创建以下方法:

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) 一如既往,最终确定声明。如果您要插入多个联系人,则可能会保留该语句并使用不同的值重新使用它。

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

insert()

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

Successfully inserted row.

4. Challenge: Multiple Inserts - 挑战:多个插入

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

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

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语句在每个遍历结束时重置,以便下一个可以使用它。

5. Querying Contacts - 查询联系人

既然你已经插入了一两行,那么确定它们真的很好用!

将以下内容添加到playground

let queryStatementString = "SELECT * FROM Contact;"

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

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

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

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

query()

Run你的playground,您将在控制台中看到以下输出:

Query Result:
1 | Ray

W00t! 看起来你的数据是进入数据库的!

6. Challenge: Printing Every Row - 挑战:打印每一行

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

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,循环将中断。

7. Updating Contacts - 更新联系人

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

首先,创建UPDATE语句:

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

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

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

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)
}

这与您之前看到的类似:prepare, step, finalize!将以下内容添加到您的playground

update()
query()

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

Successfully updated row.
Query Result:
1 | Chris

恭喜您更新第一行! 这多么容易。

8. Deleting Contacts - 删除联系人

下面看一下删除您创建的行。 再次,您将使用熟悉的prepare, step, and finalize

将以下内容添加到playground

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

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

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)
}

你现在掌握了吗?Prepare, step, and finalize

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

delete()
query()

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

Successfully deleted row.
Query returned no results

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

9. Handling Errors - 处理错误

到目前为止,希望你已经设法避免SQLite错误。 但是,当你进行没有意义的调用,或者根本无法编译时,错误将会到来。

在发生这些事情时处理错误消息可以节省大量的开发时间,它还使您有机会向用户显示有意义的错误消息。

将以下语句 - 固定格式错误 - 添加到您的playground

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

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

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) 准备语句,该语句将失败并且不应返回SQLITE_OK
  • 2) 使用sqlite3_errmsg()从数据库中获取错误消息。 此函数返回最近错误的文本描述。 然后,您将错误打印到控制台;
  • 3) 一如既往,最终finalize

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

prepareMalformedQuery()

Run你的playground,您应该在控制台中看到以下输出:

Query could not be prepared! no such table: Things

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

10. Closing the Database Connection - 关闭数据库连接

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

调用close函数,如下所示:

sqlite3_close(db)

Run你的playground;您应该在playground的右侧结果视图中看到状态代码0;这表示SQLITE_OK,这意味着您的关闭调用成功。

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

在下一节中,您将利用所学内容,并了解如何在Swift中包含其中一些调用。

后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容