Swift FMDB

步骤一:加入 SQLite 库

在 swift 中,如果需要使用 OC 的库,必须添加桥接文件。

其中,libsqlite3.dylib 是 OC 写的 sqlite 库
我们需要创建一个桥接文件 SQLite+Bridge ,在桥接文件中需要导入库:

1
#import "sqlite3.h"

然后在 Build Settings 中搜索 Bridge 配置如下:

步骤二:创建 SQLiteManager 类,该类实现便捷的数据库语句操作(增删改)

创建 SQLiteManager 类,设定其为单粒。

初始化 SQLiteManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* static: 保证在内存中只有一份
* let: 线程安全,只能赋值一次
*/
static let shareInstance: SQLiteManager = SQLiteManager()

override init() {

super.init()

// 0. 获取保存数据库文件的路径
guard let path = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true).last else {
return
}



guard let cFilePath = path.stringByAppendingString("/demo.sqlite").cStringUsingEncoding(NSUTF8StringEncoding) else {
return
}

// 1.定义变量保存被打开的数据库
// var db: COpaquePointer = nil

// 3.创建数据库文件

/**
* 第一个参数:需要打开的数据库的文件的路径
* 第二个参数:用于保存被打开的数据库
* 特点:如果指定的文件路径没有对应的数据库文件,就会自动创建一个新的,如果指定的文件路径有对应的数据库文件,就会直接打开,乳沟打开成功返回 OK
*/
if sqlite3_open(cFilePath, &db) != SQLITE_OK {
return
}

// 2.创建表
createTable()
}

init()方法主要用来创建数据库。

建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func createTable() {
// 1.编写 SQL 语句
let sql = "CREATE TABLE IF NOT EXISTS T_Student3(id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT,age INTEGER);"
guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding) else {
return
}

// 2.执行 SQL 语句
/**
* 第一个参数:一个已经打开的数据库
* 第二个参数:需要执行的 SQL 语句
* 第三个参数:执行 SQL 语句之后的回调
* 第四个参数:回调函数的第一个参数
* 第五个参数:回调函数的第二个额参数,保存错误的指针
*/
if sqlite3_exec(db, cSql, nil, nil, nil) != SQLITE_OK {
print("创建表失败")
}

print("创建表成功")
}

执行 SQL 语句

版本一:

1
2
3
4
5
6
7
8
9
10
11
12
func execSQL(sql: String) -> Bool {
// 将 sql 语句转换成 c 串
guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding) else {
return false
}

if sqlite3_exec(db, cSql, nil, nil, nil) != SQLITE_OK {
return false
}

return true
}

上述写法存在弊端,无法实现参数可变。这对扩展性来说是致命的。FMDB 内部使用的是可变参数实现 SQL 语句的绑定。并且根据不同的类型,绑定不同类型的数据。

首先,关于什么是可变参数请左转Swift 基础知识点中的函数篇

所以考虑到我们可以传入不同类型,不同个数的参数,而依然不用改写代码,因此应该使用如下方法进行数据库操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

let SQLITE_TRANSIENT = unsafeBitCast(-1, sqlite3_destructor_type.self)

func execSQL(sql: String, args: CVarArgType...) -> Bool {
// 将 sql 语句转换成 c 串
guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding) else {
return false
}
// 保存编译的结果集
var statement: COpaquePointer = nil

// 1.对传入的 SQL 语句进行预编译,检查 SQL 语句是否有错误
/**
* 第一个参数:数据库对象
* 第二个参数:需要预编译的 SQL语句
* 第三个参数:第二个的参数的最大长度,如果传入一个小于0的数,系统会自动计算 -1
* 第四个参数:结果集对象,编译没问题会给这个对象赋值
* 第五个参数:没用。
*/
if sqlite3_prepare_v2(db, cSql, -1, &statement, nil) != SQLITE_OK {
return false
}

// 2.将传入的参数绑定到 SQL 语句上

var index: Int32 = 1

// 可变参数本质:数组
for arg in args {

// 2.1 判断当前遍历到的值对应的数据类型
if arg is Int {

let temp = Int32(arg as! Int)
/**
* 第一个参数:预编译时的 statement
* 第二个参数:告诉系统需要将参数绑定到哪个位置 该索引从1开始
* 第三个参数:告诉系统需要绑定的值
*/
sqlite3_bind_int(statement, index, temp)
} else if arg is Double {
let temp = arg as! Double
sqlite3_bind_double(statement, index, temp)
} else if arg is String {
let temp = arg as! String
guard let cTemp = temp.cStringUsingEncoding(NSUTF8StringEncoding) else {
continue
}

/**
* 第一个参数:预编译时的 statement
* 第二个参数:告诉系统需要将参数绑定到哪个位置 该索引从1开始
* 第三个参数:告诉系统需要绑定的值 转成 c 语言
* 第四个参数:传-1系统会自动处理
* 第五个参数:告诉系统如何处理传入的字符串
* 如果传递 SQLITE_STATIC 0 代表告诉系统不需要任何处理,
* 字符串在使用前销毁,就是乱码
*
* 如果传递 SQLITE_TRANSIENT -1 代表告诉系统需要进行 copy,
* 该方法内部会管理它的生命周期,当使用完成之后会自动释放。
*
*/

// Swift 中没有宏定义 不能用 SQLITE_TRANSIENT

sqlite3_bind_text(statement, index, cTemp, -1, SQLITE_TRANSIENT)
} else {
print("二进制")
}

index += 1
}

// 3. 执行 SQL 语句 (如果是绑定的 SQL 语句 需要用 step 执行)
if sqlite3_step(statement) != SQLITE_DONE {
return false
}

// 4.关闭 statement
sqlite3_finalize(statement)
return true
}

知识点:1 anyObject

CVarArgType 表示 anyObject? 表示任一类型,这里其实用 any 或者 anyObject 都可以。
但是用 any 会更好一些。

any 和 anyObject 的区别:
AnyObject 可以代表任何 class 类型的实例
Any 可以表示任意类型,甚至包括方法 (func) 类型

在 OC 中,编译器不会对声明为 id 的变量进行类型检查,它可以表示任意类的实例。这时 OC 动态特性的体现。
Swift 最主要的用途依然是使用 Cocoa 框架开发 app。所以讲原来 id 的概念使用了一个类似的,可以代表任意 class 类型的 AnyObject 来进行替代。

但是 Swift 中编译器不仅不会对 AnyObject 实例的方法调用做出检查,还会在所有的 AnyObject 方法调用中返回 Optional 的结果。所以在 Swift 环境下使用起来非常麻烦,还必须先确定 AnyObject 真正的类型并进行转换以后再进行调用。

原来某个 API 返回的是一个 id,在 Swift 中就将被映射为 AnyObject? (因为 id 是可以指向 nil 的,所以需要一个 Optional)

any 则可以代表所有类型,包括 struct 和 enum

知识点2:注意 Swift 中的字符串绑定

由于 Swift 中没有宏这个概念,因此无法打出 OC 中的宏。在 stackoverflow 上,寻得解决方案:

1
let SQLITE_TRANSIENT = unsafeBitCast(-1, sqlite3_destructor_type.self)

sqlite3_bind_text(statement, index, cTemp, -1, SQLITE_TRANSIENT)

在我们要保存的类中就可以直接使用封装的方法:

1
2
3
4
5
6
7
8
9
10
11
12
// 保存当前学生对象
func insertStudent() {

// 1.拿到 SQL 管理者
let manager = SQLiteManager.shareInstance

// 2.编写 SQL 语句
let sql = "INSERT INTO T_Student3(name, age)VALUES(?, ?);"

// 3.执行 SQL 语句
manager.execSQL(sql, args: name, age)
}

但是上述方法也有弊端,就是当插入大批量的数据时,耗时非常久。大概 10000 条数据 耗时在5秒或者6秒左右。所以我们要优化代码。 耗时的原因是:每次执行 SQL 语句,系统就会自动开启事务,当 SQL 语句执行完毕,系统就会自动提交事务并关闭事务。因此,在执行插入多条数据时,我们只需进行一次打开,提交并关闭一次事务。

只要我们手动开启了事务,系统就不会再自动帮我们开启事务了,只要我们自己提交了事务,系统就不会再自动帮我们提交事务了。

因此,我们需要自己开启事务并提交和关闭事务。

首先,需要在 SQLManager 类中加入开启事务、关闭事务、回滚事务的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// MARK: -
// MARK: 提供外部接口 管理 事务
// MARK: -

/**
* 开启事务
*/
func beginTransaction() {
let sql = "BEGIN TRANSACTION;";
execSQL(sql)
}

/**
* 提交事务
*/
func commitTransaction() {
let sql = "COMMIT TRANSACTION;";
execSQL(sql)
}

/**
* 回滚事务
*/
func rollbackTransaction() {
let sql = "ROLLBACK TRANSACTION;";
execSQL(sql)
}

其次,在每次插入多条数据之前,需要手动调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// 1.创建 SQLite 管理对象
let manager = SQLiteManager.shareInstance

// 记录时间
let start = CFAbsoluteTimeGetCurrent()

// 2.开启事务
manager.beginTransaction()

for i in 0..<10000 {
let stu = Student(name: "mm + \(i)", age: 20 + i)
stu.insertStudent()
}

let end = CFAbsoluteTimeGetCurrent()
// 提交事务
manager.commitTransaction()

print("耗时\(end - start)秒")
}

前后对比,使用手动事务提交和不使用手动事务提交的差距:

1
2
创建表成功
耗时7.7067089676857秒
1
2
创建表成功
耗时0.114471018314362秒

可以看到性能提高了很多倍。

对于事务这个概念,其实是保证线程安全的。
也就是说如果执行多个 SQL 语句当做一个事务的话,一旦在执行 SQL 语句中途出现任何差错,导致语句无法正确执行下去。则可以通过事务的回滚能力回归到执行 SQL 语句之前的初始状态。这样保证一个事务能够完整的执行或者不执行。

rollBack 是在中途数据可能出现问题的地方,插入

1
2
3
4
5
6
7
/**
* 回滚事务
*/
func rollbackTransaction() {
let sql = "ROLLBACK TRANSACTION;";
execSQL(sql)
}

则,如果真的出现了问题,数据就会回到最初未执行 SQL 语句的地方。

查询

查询语句与其他的 SQL 语句执行时有不同。

首先,与执行 SQL 语句,如果执行成功即返回 SQLITE_OK。则会将句柄给到我们。
通过句柄来遍历数据库,获取数据库中的数据。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// MARK: -
// MARK: 查询
// MARK: -

func execQuery(sql: String) -> [[String: AnyObject]]? {
// 1. 将 SQL 语句转换为 C 语言字符串
guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding) else {
return nil
}

// 创建句柄
var statement: COpaquePointer = nil

// 2.预编译 SQL 语句,检查是否有错,如果正确会将句柄返回
if sqlite3_prepare_v2(db, cSql, -1, &statement, nil) != SQLITE_OK {
return nil
}

// 创建一个数组,保存所有行的数据
var dicts = [[String: AnyObject]]()

// 3.利用 statement 对象取出一行数据(一条记录)
while sqlite3_step(statement) == SQLITE_ROW {

var record = [String: AnyObject]()

// 4.利用 statement 取出当前一共有多少列
let count = sqlite3_column_count(statement)

// 5.遍历当前行每一列
for i in 0..<count {
// 5.1 取出当前列的名称
let cName = sqlite3_column_name(statement, i)
guard let name = String(CString: cName, encoding: NSUTF8StringEncoding) else {
continue
}

// 5.2 取出当前列的类型
let type = sqlite3_column_type(statement, i)

// 5.3 判断当前列的类型
switch type {
case SQLITE_INTEGER:
let value = Int(sqlite3_column_int(statement, i))
record[name] = value
case SQLITE_FLOAT:
let value = sqlite3_column_double(statement, i)
record[name] = value
case SQLITE3_TEXT:

let cStr = UnsafePointer<Int8>(sqlite3_column_text(statement, i))
let str = String(CString: cStr, encoding: NSUTF8StringEncoding)
record[name] = str
default:
print("二进制")
}
}
// 将当前行的字典存储到数组中
dicts.append(record)
}
return dicts
}

利用句柄取出一条数据:

1
sqlite3_step(statement) == SQLITE_ROW

首先通过句柄来读取出当前一共有多少列,几多少个字段。

1
let count = sqlite3_column_count(statement)

然后遍历所有字段,取出每个字段的名称(这里需要注意返回值类型):

1
let cName = sqlite3_column_name(statement, i)

cName 返回值类型为:UnsafePointer<Int8>
关于 UnsafePointer,可查看《Swifter》第三章 13个课题 UnsafePointer

这里其实 cName 是一个指向 Int8 类型的指针。也就是 c 字符串,存储时需要转成 String 类型。

取出当前列(字段)类型:

1
let type = sqlite3_column_type(statement, i)

判断当前列(字段)类型
分别取值,并存入数组。

FMDB

FMDB 是 iOS 平台的 SQLite 数据库框架
FMDB 以 OC 的方式封装了 SQLite 的 C 语言 API

FMDB 使用起来面向对象。提供了多线程安全的数据库操作。

主要有三个主要的类:FMDatabase

  • 一个 FMDatabase 对象代表了一个单独的 SQLite 数据库,用来执行数据库语句

FMResultSet

  • 使用 FMDatabase 执行查询后的结果集

FMDatabaseQueue

使用方式:Swift 中推荐使用 cocoapods 集成
如果要手动集成,则需要三步:

  1. 导入 FMDB 代码到工程中
  2. 创建桥接文件,导入 sqlite3.h 导入 FMDB.h
  3. 将 Swift extensions 文件内容导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var db: FMDatabase!

static let shareInstance = SQLiteManager()

override init() {
super.init()

guard let path = NSSearchPathForDirectoriesInDomains(.CachesDirectory, NSSearchPathDomainMask.UserDomainMask, true).last else {
return
}

let filePath = (path as NSString).stringByAppendingPathComponent("demo.sqlite")

db = FMDatabase(path: filePath)

if db.open() == false {
return
}

let sql = "CREATE TABLE IF NOT EXISTS T_Student3(id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT,age INTEGER);"

db.executeUpdate(sql, withArgumentsInArray: nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func insertStudent() {
let sql = "INSERT INTO T_Student3(name, age)VALUES(?, ?);"

SQLiteManager.shareInstance.db.executeUpdate(sql, withArgumentsInArray: [name, age])
}

class func loadStudent() {

let sql = "SELECT * FROM T_Student3;"

let resultSet = SQLiteManager.shareInstance.db.executeQuery(sql, withArgumentsInArray: nil)

while resultSet.next() {
let id = resultSet.intForColumn("id")
let name = resultSet.stringForColumn("name")
let age = resultSet.intForColumn("age")

print("id = \(id), name = \(name), age = \(age)")
}
}
文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2016/06/01/Swift-FMDB/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论