步骤一:加入 SQLite 库 在 swift 中,如果需要使用 OC 的库,必须添加桥接文件。
其中,libsqlite3.dylib 是 OC 写的 sqlite 库 我们需要创建一个桥接文件 SQLite+Bridge ,在桥接文件中需要导入库:
然后在 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 shareInstance: SQLiteManager = SQLiteManager ()override init () { super .init () guard let path = NSSearchPathForDirectoriesInDomains (.CachesDirectory , NSSearchPathDomainMask .UserDomainMask , true ).last else { return } guard let cFilePath = path.stringByAppendingString("/demo.sqlite" ).cStringUsingEncoding(NSUTF8StringEncoding ) else { return } if sqlite3_open(cFilePath, &db) != SQLITE_OK { return } 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 () { 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 } 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 { 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 { guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding ) else { return false } var statement: COpaquePointer = nil if sqlite3_prepare_v2(db, cSql, -1 , &statement, nil ) != SQLITE_OK { return false } var index: Int32 = 1 for arg in args { if arg is Int { let temp = Int32 (arg as ! Int ) 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 } sqlite3_bind_text(statement, index, cTemp, -1 , SQLITE_TRANSIENT ) } else { print ("二进制" ) } index += 1 } if sqlite3_step(statement) != SQLITE_DONE { return false } 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 () { let manager = SQLiteManager .shareInstance let sql = "INSERT INTO T_Student3(name, age)VALUES(?, ?);" 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 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?) { let manager = SQLiteManager .shareInstance let start = CFAbsoluteTimeGetCurrent () 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 func execQuery (sql: String) -> [[String : AnyObject ]]? { guard let cSql = sql.cStringUsingEncoding(NSUTF8StringEncoding ) else { return nil } var statement: COpaquePointer = nil if sqlite3_prepare_v2(db, cSql, -1 , &statement, nil ) != SQLITE_OK { return nil } var dicts = [[String : AnyObject ]]() while sqlite3_step(statement) == SQLITE_ROW { var record = [String : AnyObject ]() let count = sqlite3_column_count(statement) for i in 0 ..<count { let cName = sqlite3_column_name(statement, i) guard let name = String (CString : cName, encoding: NSUTF8StringEncoding ) else { continue } let type = sqlite3_column_type(statement, i) 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
FMDatabaseQueue
使用方式:Swift 中推荐使用 cocoapods 集成 如果要手动集成,则需要三步:
导入 FMDB 代码到工程中
创建桥接文件,导入 sqlite3.h 导入 FMDB.h
将 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)" ) } }