sqlite
简介
什么是 sqlite?
关系型数据库的特点
- 一个
字段(COL)
存储一个值,类似于对象的一个属性 - 一
行(ROW)
存储一条记录,类似于一个对象 - 一个
表(TABLE)
存储一系列数据,类似于对象数组 - 多个
表
之间存在一定关系
,类似于对象之间的关系,例如:一条微博数据中包含用户记录
术语
- 字段(
Field/Col
):一个字段存储一个值,可以存储INTEGER
,REAL
,TEXT
,BLOB
,NULL
五种类型的数据- sqlite 在存储时,本质上并不区分准确的数据类型
- 主键:
Primary Key
,唯一
标示一条记录的字段,具有以下特点: - 外键:
Foreign Key
,对应其他关系表的标示,利用外键
可以和另外一个表
建立起"关系"- 方便数据维护
- 节约存储空间
开发数据库的步骤
移动应用中使用数据库的好处
sqlite 命令
DDL - 数据定义语言
命令 | 描述 |
---|---|
CREATE | 创建一个新的表,一个表的视图,或者数据库中的其他对象 |
ALTER | 修改数据库中的某个已有的数据库对象,比如一个表 |
DROP | 删除整个表,或者表的视图,或者数据库中的其他对象 |
- 不需要记忆,可以直接从客户端软件复制/粘贴
DML - 数据操作语言
命令 | 描述 |
---|---|
INSERT | 新增 |
UPDATE | 修改 |
DELETE | 删除 |
- 需要掌握,语法固定,简单
DQL - 数据查询语言
命令 | 描述 |
---|---|
SELECT | 查询 |
- 需要掌握一些简单的查询指令
常用 sql
创建表
/* 创建数据表 CREATE TABLE '表名' ( '字段名' 类型(INTEGER,REAL,TEXT,BLOB) NOT NULL 不允许为空 PRIMARY KEY 主键 AUTOINCREMENT 自增长,'字段名2' 类型,... ) 注意:在开发中,如果是从 Navicat 粘贴的 sql,需要自己添加一个指令 IF NOT EXISTS 加在表名前,如果数据表已经存在,就什么也不做 */
CREATE TABLE IF NOT EXISTS "T_Person" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"name" TEXT,"age" INTEGER,"heigth" REAL ) /* 简单约束 */ CREATE TABLE IF NOT EXISTS t_student ( id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT,age INTEGER );
CREATE TABLE IF NOT EXISTS t_student ( id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT UNIQUE,age INTEGER );
/* 添加主键 */
CREATE TABLE IF NOT EXISTS t_student ( id INTEGER PRIMARY KEY AUTOINCREMENT,age INTEGER,score REAL );
/* 添加主键 */
CREATE TABLE IF NOT EXISTS t_student ( id INTEGER,score REAL,PRIMARY KEY(id) );
插入
INSERT INTO t_student (age,score,name) VALUES ('28',100,'zhangsan');
INSERT INTO t_student (name,age) VALUES ('lisi','28');
INSERT INTO t_student (score) VALUES (100);
修改
UPDATE t_student SET name = 'MM' WHERE age = 10;
UPDATE t_student SET name = 'WW' WHERE age is 7;
UPDATE t_student SET name = 'XXOO' WHERE age < 20;
UPDATE t_student SET name = 'NNMM' WHERE age < 50 and score > 10;
/*更新记录的name*/
UPDATE t_student SET name = 'zhangsan';
删除
DELETE FROM t_student;
DELETE FROM t_student WHERE age < 50;
查询
/* 分页 */
SELECT * FROM t_student ORDER BY id ASC LIMIT 30,10;
/* 排序 */
SELECT * FROM t_student WHERE score > 50 ORDER BY age DESC;
SELECT * FROM t_student WHERE score < 50 ORDER BY age ASC,score DESC;
/* 计量 */
SELECT COUNT(*) FROM t_student WHERE age > 50;
/* 别名 */
SELECT name as myName,age as myAge,score as myscore FROM t_student;
SELECT name myName,age myAge,score myscore FROM t_student;
SELECT s.name myName,s.age myAge,s.score myscore FROM t_student s WHERE s.age > 50;
/* 查询 */
SELECT name,age,score FROM t_student;
SELECT * FROM t_student;
删除表
/*删除表*/
DROP TABLE IF EXISTS t_student;
sqlite核心对象
核心对象 & 核心接口
核心对象
-
database_connection
-
prepared_statement
- 可以简单的将它视为编译后的sql语句
核心接口
-
sqlite3_open
-
sqlite3_prepare
-
sqlite3_step
-
sqlite3_column
-
sqlite3_finalize
- 销毁
prepared_statement
对象,否则会造成内存泄露
- 销毁
-
sqlite3_close
- 关闭之前打开的
database_connection
对象 - 所有和该对象相关的
prepared_statements
对象都必须在此之前被销毁
- 关闭之前打开的
Swift 中使用 sqlite
准备工作
- 添加
libsqlite3.tbd
- 创建
sqlite-Bridge.h
- 选择
项目
-TARGETS
-Build Settings
,搜索Bridg
- 在
Objective-C Bridging Header
中输入项目名/sqlite-Bridge.h
- 如果之前设置过桥接文件,可以直接使用
编译测试
sqliteManager
与网络接口的独立类似,数据库的底层操作,也应该有一个独立的对象单独负责
sqliteManager
单例
/// sqlite 管理器
class sqliteManager {
/// 单例
static let sharedManager = sqliteManager()
}
数据库访问操作需求
建立&打开数据库
/// 数据库句柄
private var db: COpaquePointer = nil
/// 打开数据库
///
/// - parameter dbname: 数据库文件名
func openDB(dbname: String) {
let path = (NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.UserDomainMask,true).last! as NSString).stringByAppendingPathComponent(dbname)
print(path)
if sqlite3_open(path,&db) != sqlITE_OK {
print("打开数据库失败")
return
}
print("打开数据库成功")
}
代码小结
打开数据库
func application(application: UIApplication,didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
sqliteManager.sharedManager.openDB("my.db")
return true
}
代码小结
sqlite
数据库是直接保存在沙盒中的一个文件,只有当前应用程序可以使用- 在移动端开发时,数据库通常是以
持久式
连接方式使用的 - 所谓
持久式连接
指的是只做一次打开数据库
的操作,永远不做关闭
数据库的操作,从而可以提高数据库的访问效率
创建数据表
注意:创表操作本质上是通过执行
sql
语句实现的
/// 执行 sql
///
/// - parameter sql: sql
///
/// - returns: 是否成功
func execsql(sql: String) -> Bool {
/** 参数 1. 数据库句柄 2. 要执行的 sql 语句 3. 执行完成后的回调,通常为 nil 4. 回调函数第一个参数的地址,通常为 nil 5. 错误信息地址,通常为 nil */
return sqlite3_exec(db,sql,nil,nil) == sqlITE_OK
}
- 创建数据表
/// 创建数据表
///
/// - returns: 是否成功
private func createTable() -> Bool {
let sql = "CREATE TABLE IF NOT EXISTS T_Person \n" +
"('id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n" +
"'name' TEXT,\n" +
"'age' INTEGER);"
print(sql)
return execsql(sql)
}
- 调整
openDB
函数
if createTable() {
print("创表成功")
} else {
print("创表失败")
db = nil
}
代码小结
- 创表
sql
可以从Navicat
中粘贴,然后做一些处理
数据模型
- 建立
Person
模型
class Person: NSObject {
/// id
var id: Int = 0
/// 姓名
var name: String?
/// 年龄
var age: Int = 0
/// 使用字典创建 Person 对象
init(dict: [String: AnyObject]) {
super.init()
setValuesForKeysWithDictionary(dict)
}
}
- 新增数据
/// 将当前对象插入到数据库
///
/// - returns: 是否成功
func insertPerson() -> Bool {
assert(name != nil,"姓名不能为空")
let sql = "INSERT INTO T_Person (name,age) VALUES ('\(name!)',\(age));"
return sqliteManager.sharedManager.execsql(sql)
}
/// 测试插入数据
func demoInsert() {
print(Person(dict: ["name": "zhangsan","age": 18]).insertPerson())
}
- 更新记录
/// 更新当前对象在数据库中的记录
///
/// - returns: 是否成功
func updatePerson() -> Bool {
assert(name != nil,"姓名不能为空")
assert(id > 0,"ID 不正确")
let sql = "UPDATE T_Person SET name = '\(name!)',age = \(age) WHERE id = \(id);"
return sqliteManager.sharedManager.execsql(sql)
}
/// 测试更新记录
func demoUpdate() {
print(Person(dict: ["id": 1,"name": "lisi","age": 20]).updatePerson())
}
- 删除数据
/// 删除当前对象在数据库中的记录
///
/// - returns: 是否成功
func deletePerson() -> Bool {
assert(id > 0,"ID 不正确")
let sql = "DELETE FROM T_Person WHERE ID = \(id);"
return sqliteManager.sharedManager.execsql(sql)
}
/// 测试删除记录
func demoDelete() {
print(Person(dict: ["id": 1,"age": 20]).deletePerson())
}
- 测试批量插入
/// 测试批量插入数据
func insertManyPerson() {
print("开始")
let start = CFAbsoluteTimeGetCurrent()
for i in 0..<100000 {
Person(dict: ["name": "lisi-\(i)","age": Int(arc4random_uniform(10)) + 20]).insertPerson()
}
print(CFAbsoluteTimeGetCurrent() - start)
}
非常耗时,大概需要1分钟左右
查询数据
- 准备伪代码
/// 加载 Person 对象数组 class func loadPersons() -> [Person]? { // 1. 从数据库获取字典数组 sqliteManager.sharedManager.execRecordSet("SELECT id,name,age FROM T_Person;") // 2. 遍历数组,字典转模型 return nil }
/// 执行 sql 返回结果集 /// /// - parameter sql: sql /// /// - returns: 字典数组 func execRecordSet(sql: String) -> [[String: AnyObject]]? { // 1. 准备(预编译) sql var stmt: COpaquePointer = nil /** 1. 已经打开的数据库句柄 2. 要执行的 sql 3. 以字节为单位的 sql 最大长度,传入 -1 会自动计算 4. sql 语句地址 5. 未使用的指针地址,通常传入 nil */ if sqlite3_prepare_v2(db,-1,&stmt,nil) != sqlITE_OK { print("准备 sql 失败") return nil } print("OK") // 释放语句 sqlite3_finalize(stmt) return nil }
-
代码小结
-
单步执行
// 2. 单步执行获取结果集内容
var index = 0
while sqlite3_step(stmt) == sqlITE_ROW {
print(index++)
}
- 遍历
stmt
中的列数
以及每列的列名
&数据类型
// 2. 单步执行获取结果集内容
while sqlite3_step(stmt) == sqlITE_ROW {
// 1> 结果集列数
let colCount = sqlite3_column_count(stmt)
// 2> 遍历每一列
for col in 0..<colCount {
let cName = sqlite3_column_name(stmt,col)
let name = String(CString: cName,encoding: NSUTF8StringEncoding)!
print(name + "\t",appendNewline: false)
}
print("\n",appendNewline: false)
}
- 根据
数据类型
获取数据
for col in 0..<colCount {
// 1) 字段名
let cName = sqlite3_column_name(stmt,col)
let name = String(CString: cName,encoding: NSUTF8StringEncoding)!
// 2) 字段类型
let type = sqlite3_column_type(stmt,col)
// 3) 根据类型获取字段内容
var v: AnyObject? = nil
switch type {
case sqlITE_INTEGER:
v = Int(sqlite3_column_int64(stmt,col))
case sqlITE_FLOAT:
v = sqlite3_column_double(stmt,col)
case sqlITE3_TEXT:
let cText = UnsafePointer<Int8>(sqlite3_column_text(stmt,col))
v = String(CString: cText,encoding: NSUTF8StringEncoding)
case sqlITE_NULL:
v = NSNull()
default:
print("不支持的格式")
}
print(name + "\t" + String(type) + "\t \(v) \t",appendNewline: false)
}
print("\n",appendNewline: false)
- 抽取创建结果集代码
/// 从 stmt 获取记录字典 /// /// - parameter stmt: stmt /// /// - returns: 返回记录集字典 private func recordDict(stmt: COpaquePointer) -> [String: AnyObject] { // 1> 结果集列数 let colCount = sqlite3_column_count(stmt) // 2> 遍历每一列 - 创建字典 var record = [String: AnyObject]() for col in 0..<colCount { // 1) 字段名 let cName = sqlite3_column_name(stmt,col) let name = String(CString: cName,encoding: NSUTF8StringEncoding)! // 2) 字段类型 let type = sqlite3_column_type(stmt,col) // 3) 根据类型获取字段内容 var v: AnyObject? = nil switch type { case sqlITE_INTEGER: v = Int(sqlite3_column_int64(stmt,col)) case sqlITE_FLOAT: v = sqlite3_column_double(stmt,col) case sqlITE3_TEXT: let cText = UnsafePointer<Int8>(sqlite3_column_text(stmt,col)) v = String(CString: cText,encoding: NSUTF8StringEncoding) case sqlITE_NULL: v = NSNull() default: print("不支持的格式") } record[name] = v } return record }
- 重构后的代码
/// 执行 sql 返回结果集 /// /// - parameter sql: sql /// /// - returns: 字典数组 func execRecordSet(sql: String) -> [[String: AnyObject]]? { // 1. 准备(预编译) sql var stmt: COpaquePointer = nil if sqlite3_prepare_v2(db,nil) != sqlITE_OK { print("准备 sql 失败") return nil } // 2. 单步执行获取结果集内容 // 2.1 结果集 var recordset = [[String: AnyObject]]() // 2.2 遍历结果集 while sqlite3_step(stmt) == sqlITE_ROW { recordset.append(recordDict(stmt)) } // 3. 释放语句 sqlite3_finalize(stmt) return recordset }
- 在
Person
模型中加载Person
列表
/// 加载 Person 对象数组 class func loadPersons() -> [Person]? { // 1. 从数据库获取字典数组 guard let array = sqliteManager.sharedManager.execRecordSet("SELECT id,age FROM T_Person;") else { return nil } // 2. 遍历数组,字典转模型 var persons = [Person]() for dict in array { persons.append(Person(dict: dict)) } return persons }
批量插入
在 sqlite 中如果要批量插入数据,通常需要引入
事务的概念
事务
- 事务处理函数
/// 开启事务
func beginTransaction() -> Bool {
return execsql("BEGIN TRANSACTION;")
}
/// 提交事务
func commitTransaction() -> Bool {
return execsql("COMMIT TRANSACTION;")
}
/// 回滚事务
func rollBackTransaction() -> Bool {
return execsql("ROLLBACK TRANSACTION;")
}
/// 插入许多人
private func insertManyPerson() {
print("开始")
let start = CFAbsoluteTimeGetCurrent()
sqliteManager.sharedsqliteManager.beginTransaction()
for i in 0..<100000 {
let person = Person(dict: ["name": "lisi-" + String(i),"age": 18,"height": 1.8])
person.insertPerson()
}
sqliteManager.sharedsqliteManager.commitTransaction()
print("结束 " + String(CFAbsoluteTimeGetCurrent() - start))
}
测试结果不到 4s
- 测试回滚
/// 插入许多人
private func insertManyPerson() {
print("开始")
let start = CFAbsoluteTimeGetCurrent()
sqliteManager.sharedsqliteManager.beginTransaction()
for i in 0..<100000 {
let person = Person(dict: ["name": "lisi-" + String(i),"height": 1.8])
person.insertPerson()
if i == 10000 {
sqliteManager.sharedsqliteManager.rollBackTransaction()
break
}
}
sqliteManager.sharedsqliteManager.commitTransaction()
print("结束 " + String(CFAbsoluteTimeGetCurrent() - start))
}
批量更新
- 批量更新函数 - 绑定参数
func batchUpdate(sql: String,params: CVarArgType...) -> Bool {
let csql = sql.cStringUsingEncoding(NSUTF8StringEncoding)!
var stmt: COpaquePointer = nil
if sqlite3_prepare_v2(db,csql,-1,nil) == sqlITE_OK {
// 绑定参数
var col: Int32 = 1
for arg in params {
if arg is Int {
sqlite3_bind_int64(stmt,col,sqlite3_int64(arg as! Int))
} else if arg is Double {
sqlite3_bind_double(stmt,(arg as! Double))
} else if arg is String {
let cStr = (arg as! String).cStringUsingEncoding(NSUTF8StringEncoding)
sqlite3_bind_text(stmt,cStr!,sqlITE_TRANSIENT)
} else if arg is NSNull {
sqlite3_bind_null(stmt,col)
}
col++
}
}
sqlite3_finalize(stmt)
return true
}
绑定字符串
- 如果第5个参数传递
NULL
或者sqlITE_STATIC
常量,sqlite 会假定这块buffer
是静态内存,或者客户应用程序会小心的管理和释放这块buffer
,所以sqlite放手不管 -
如果第5个参数传递的是
sqlITE_TRANSIENT
常量,则sqlite会在内部复制这块buffer的内容。这就允许客户应用程序在调用完bind
函数之后,立刻释放这块buffer
(或者是一块栈上的buffer
在离开作用域之后自动销毁)。sqlite会自动在合适的时机释放它内部复制的这块buffer
-
由于在 sqlite.h 中
sqlITE_TRANSIENT
是以宏的形式定义的,而在 swift 中无法直接利用宏传递函数指针,因此需要使用以下代码转换一下
swift 1.2
private let sqlITE_TRANSIENT = sqlite3_destructor_type(COpaquePointer(bitPattern: -1))
swift 2.0
private let sqlITE_TRANSIENT = unsafeBitCast(-1,sqlite3_destructor_type.self)
sqlite3_bind_text(stmt,index,cStr,sqlITE_TRANSIENT)
- 单步执行
var result = true
if sqlite3_step(stmt) != sqlITE_DONE {
print("插入错误")
result = false
}
// 语句复位
if sqlite3_reset(stmt) != sqlITE_OK {
print("语句复位错误")
result = false
}
sqlite3_finalize(stmt)
return result
注意:执行结束后,一定要对语句进行复位,以便后续查询语句能够继续执行
函数小结
- 列数的计数从 1 开始
- 对于数据更新操作,单步执行正确的结果是
sqlITE_DONE
- 每单步执行之后,需要做一次
reset
操作
使用预编译 sql 批量插入数据
- 批量插入数据
/// 批量插入
private func batchInsert() {
print("开始")
let start = CFAbsoluteTimeGetCurrent()
let manager = sqliteManager.sharedsqliteManager
let sql = "INSERT INTO T_Person (name,height) VALUES (?,?,?);"
// 开启事务
manager.beginTransaction()
for _ in 0..<10000 {
if !manager.batchUpdate(sql,params: "zhangsan",18,1.8) {
manager.rollBackTransaction()
break
}
}
manager.commitTransaction()
print("结束 " + String(CFAbsoluteTimeGetCurrent() - start))
}
运行测试,执行结果只需要 0.1s
多线程
- 定义队列
/// 操作队列
private let queue = dispatch_queue_create("com.itheima.sqlite",DISPATCH_QUEUE_SERIAL)
- 队列执行
/// 队列更新
///
/// :param: action 在后台执行的任务
func queueUpdate(action: (manager: sqliteManager) -> ()) {
dispatch_async(queue) { [unowned self] in
// 1. 开启事务
self.beginTransaction()
action(manager: self)
// 2. 提交事务
self.commitTransaction()
}
}
- 测试后台更新
private func queueUpdate() {
print("开始")
let start = CFAbsoluteTimeGetCurrent()
sqliteManager.sharedsqliteManager.queueUpdate { (manager) -> () in
let sql = "INSERT INTO T_Person (name,?);"
for i in 0..<10000 {
if !manager.batchUpdate(sql,1.8) {
manager.rollBackTransaction()
break
}
if i == 1000 {
manager.rollBackTransaction()
break
}
}
print(NSThread.currentThread())
print("结束 " + String(CFAbsoluteTimeGetCurrent() - start))
}
}
注意:sqlite 数据库不允许同时并发写入输入,如果用多线程,也必须使用串行队列进行操作
FMDB
使用框架
官网地址
直接拖拽
Podfile
- 不推荐
use_frameworks! pod 'FMDB',:git => 'https://github.com/robertmryan/fmdb.git'
代码演练
let documentsFolder = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0] as String let path = documentsFolder.stringByAppendingPathComponent("test.sqlite") let database = FMDatabase(path: path) if !database.open() { println("Unable to open database") return } if !database.executeUpdate("create table test(x text,y text,z text)",withArgumentsInArray: nil) { println("create table Failed: \(database.lastErrorMessage())") } if !database.executeUpdate("insert into test (x,y,z) values (?,?)",withArgumentsInArray: ["a","b","c"]) { println("insert 1 table Failed: \(database.lastErrorMessage())") } if !database.executeUpdate("insert into test (x,withArgumentsInArray: ["e","f","g"]) { println("insert 2 table Failed: \(database.lastErrorMessage())") } if let rs = database.executeQuery("select x,z from test",withArgumentsInArray: nil) { while rs.next() { let x = rs.stringForColumn("x") let y = rs.stringForColumn("y") let z = rs.stringForColumn("z") println("x = \(x); y = \(y); z = \(z)") } } else { println("select Failed: \(database.lastErrorMessage())") } database.close()
- 队列演练
let queue = FMDatabaseQueue(path: "/Users/liufan/Desktop/my.db") let sql = "insert into t_person (name,age) VALUES (?,?);" queue.inTransaction { (db,rollBack) -> Void in db.executeUpdate(sql,"lisi",28) db.executeUpdate(sql,"wangwu",48) rollBack.memory = true } queue.inDatabase { (db) -> Void in if let result = db.executeQuery("select * from t_person") { while result.next() { let name = result.objectForColumnName("name") let age = result.intForColumn("age") print("\(name) \(age)") } } }
要设置 rollBack 可以使用
rollBack.memory = true