SQLite
简介
http://www.sqlite.org/
- 是一款轻型的数据库
- 设计目标是
嵌入式 的
- 占用资源少
- 处理速度快
- 当前版本 3.8.10.2,MAC 内置已经安装了 SQLite
什么是 SQLite?
-
SQLite 是一个进程内的库,本质上就是一个文件 ,是一个 SQL 数据库引擎,具有:
- 自给自足
- 无服务器
- 零配置
- 不需要安装或管理
- 一个完整的 SQLite 数据库就是一个单一的磁盘文件
- 轻量级
- 完全配置时小于 400K,省略可选功能配置时小于250K
- 事务性支持
-
而服务端使用的数据库,如:Orcal ,SQL Server ,MySQL ...则需要独立的服务器,安装,配置,维护……
关系型数据库的特点
- 一个
字段(COL) 存储一个值,类似于对象的一个属性
- 一
行(ROW) 存储一条记录,类似于一个对象
- 一个
表(TABLE) 存储一系列数据,类似于对象数组
- 多个
表 之间存在一定关系 ,类似于对象之间的关系,例如:一条微博数据中包含用户记录
术语
- 字段(
Field/Col ):一个字段存储一个值,可以存储INTEGER ,REAL ,TEXT ,BLOB ,NULL 五种类型的数据
- SQLite 在存储时,本质上并不区分准确的数据类型
- 主键:
Primary Key ,唯一 标示一条记录的字段,具有以下特点:
- 名字:xxx_id
- 类型:Integer
- 自动增长
- 准确数值由数据库决定,程序员不用关心
- 外键:
Foreign Key ,对应其他关系表的标示,利用外键 可以和另外一个表 建立起"关系"
开发数据库的步骤
- 建立数据库 -> 有存储数据的文件
- 创建数据表 -> 每一张数据表存储一类数据
- 利用
SQL 命令 实现增/删/查/改,并在 UI 中显示
移动应用中使用数据库的好处
- 将网络数据存储在本地,不用每次都加载,减少用户网络流量开销
- 对本地数据进行查询
SQLite 命令
DDL - 数据定义语言
命令 |
描述 |
|
CREATE
创建一个新的表,一个表的视图,或者数据库中的其他对象
ALTER
修改数据库中的某个已有的数据库对象,比如一个表
DROP
删除整个表,或者表的视图,或者数据库中的其他对象
DML - 数据操作语言
命令 |
描述 |
INSERT
新增
UPDATE
修改
DELETE
删除
DQL - 数据查询语言
命令 |
描述 |
SELECT
查询
常用 SQL
创建表
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;
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
- 由
sqlite3_open
函数创建并返回
- 在使用其他
SQLite
接口函数之前,必须先获得database_connnection
对象
-
prepared_statement
核心接口
-
sqlite3_open
- 可以打开已经存在的数据库文件
- 如果数据库不存在,可以创建新的数据库文件
- 返回的
database_connection
对象是其他SQLite APIs
的句柄参数
- 可以在多个线程之间共享该对象指针
-
sqlite3_prepare
- 将
SQL
文本转换为prepared_statement
对象
- 不会
执行
指定的SQL
语句
- 只是将
SQL
文本初始化为待
执行的状态
-
sqlite3_step
- 执行一次
sqlite3_prepare
函数返回的prepared_statement
对象
- 执行完该函数后,
prepared_statement
对象的内部指针将指向其返回结果集的第一行
- 如果要获得后续的数据行,则需要不断地调用该函数,直到所有的数据行遍历完毕
- 对于
INSERT
、UPDATE
和DELETE
等DML
语句,执行一次即可完成
-
sqlite3_column
- 用于获取当前行指定列的数据
- 以下函数分别对应不同的数据类型
sqlite3_column_blob
sqlite3_column_bytes
sqlite3_column_bytes16
sqlite3_column_double
sqlite3_column_int
sqlite3_column_int64
sqlite3_column_text
sqlite3_column_text16
sqlite3_column_type
sqlite3_column_value
sqlite3_column_count
-
sqlite3_finalize
- 销毁
prepared_statement
对象,否则会造成内存泄露
-
sqlite3_close
- 关闭之前打开的
database_connection
对象
- 所有和该对象相关的
prepared_statements
对象都必须在此之前被销毁
Swift 中使用 SQLite
准备工作
- 添加
libsqlite3.tbd
- 创建
SQLite-Bridge.h
SQLite3
框架是一套C
语言的框架,因此需要添加桥接文件
- 选择
项目
-TARGETS
-Build Settings
,搜索Bridg
- 在
Objective-C Bridging Header
中输入项目名/SQLite-Bridge.h
编译测试
SQLiteManager
与网络接口的独立类似,数据库的底层操作,也应该有一个独立的对象单独负责
SQLiteManager
单例
- 新建
SQLiteManager.swift
,并且实现以下代码:
class SQLiteManager {
static let sharedManager = SQLiteManager()
}
数据库访问操作需求
- 建立数据库 -> 有存储数据的文件
- 创建数据表 -> 每一张数据表存储一类数据
- 利用
SQL 命令
实现增/删/查/改,并在 UI 中显示
建立&打开数据库
private var db: COpaquePointer = nil
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("打开数据库成功")
}
代码小结
- 建立数据库需要给定完整的数据库路径
sqlite3_open
函数会打开数据库,如果数据库不存在,会新建一个空的数据库
,并且返回数据库指针(句柄)
- 后续的所有数据库操作,都基于此
数据库句柄
进行
打开数据库
func application(application: UIApplication,didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
SQLiteManager.sharedManager.openDB("my.db")
return true
}
代码小结
SQLite
数据库是直接保存在沙盒中的一个文件,只有当前应用程序可以使用
- 在移动端开发时,数据库通常是以
持久式
连接方式使用的
- 所谓
持久式连接
指的是只做一次打开数据库
的操作,永远不做关闭
数据库的操作,从而可以提高数据库的访问效率
创建数据表
- 如果是第一次运行,打开数据库之后,只能得到一个空的数据,没有任何的数据表
- 为了让数据库正常使用,在第一次打开数据库后,需要执行
创表
操作
注意:创表操作本质上是通过执行SQL
语句实现的
func execSQL(sql: String) -> Bool {
return sqlite3_exec(db,sql,nil,nil) == SQLITE_OK
}
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)
}
if createTable() {
print("创表成功")
} else {
print("创表失败")
db = nil
}
代码小结
- 创表
SQL
可以从Navicat
中粘贴,然后做一些处理
- 将
"
替换成'
- 在每一行后面增加一个
n
防止字符串拼接因为缺少空格造成SQL
语句错误
- 在
表名
前添加IF NOT EXISTS
防止因为数据表存在出现错误
数据模型
class Person: NSObject {
var id: Int = 0
var name: String?
var age: Int = 0
init(dict: [String: AnyObject]) {
super.init()
setValuesForKeysWithDictionary(dict)
}
}
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())
}
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())
}
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
}
- 在
SQLiteManager
中添加查询语句,准备结果集
/// 执行 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
}
-
代码小结
- 这一部分的工作可以看作是对字符串的 SQL 语句进行编译,并且检查是否存在语法问题
- 编译成功后通过
sqlite3_step
执行 SQL,每执行一次,获取一条记录
- 通过
while
循环直至执行完毕
- 注意,指令执行完毕后需要释放
-
单步执行
var index = 0
while sqlite3_step(stmt) == SQLITE_ROW {
print(index++)
}
while sqlite3_step(stmt) == SQLITE_ROW {
let colCount = sqlite3_column_count(stmt)
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 {
let cName = sqlite3_column_name(stmt,col)
let name = String(CString: cName,encoding: NSUTF8StringEncoding)!
let type = sqlite3_column_type(stmt,col)
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 对象数组
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)
- 而绑定字符串的函数必须写成(OC中可以使用
NULL
,是因为OC
中以 @"" 定义的函数都是保存在静态区的)
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)
func queueUpdate(action: (manager: SQLiteManager) -> ()) {
dispatch_async(queue) { [unowned self] in
self.beginTransaction()
action(manager: self)
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
使用框架
官网地址
https://github.com/ccgus/fmdb
直接拖拽
- 将 fmdb 文件夹拖入项目
- 建立桥接文件
- 将 Swift extensions 拖入项目
Podfile
use_frameworks!
pod 'FMDB',:git => 'https://github.com/robertmryan/fmdb.git'
代码演练
- 除了查询都使用
executeUpdate
- 查询使用
executeQuery
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
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!