效果图附上:
插入数据
删除数据
更新数据
查找数据
分页检索数据
Other数据- 这里时间限制,不演示全部
-
前言
在上一篇简单的介绍了SQLite的基本使用之后,现在我们开始在代码中去完成对数据库的操作。
内容点:
PS:使用C函数完成,不使用第三方框架
- 集成SQLite
- 使用SQLite完成增删改查
- 子线程操作数据库
- 数据库的优化
-
1. 集成
-
1.1先创建一个Swift 3.0工程,名字随意,创建完成后点击项目工程名
-
1.2创建一个桥接文件
command + n
-
1.3导入头文件
输入 #import
-
1.4 然后在
ViewController
中输入sqlite3
,有提示表示集成成功.
在开始之前先说明一下:由于代码的注释都写的很详细了,所以不会逐行解释。
-
2. 使用SQLite完成增删改查
要想完成对数据的增删改查,得有一个文件吧,和上一篇类似,我们应该先创建数据库文件,然后创建表,才能进行增删改查,所以我们第一步是创建一个数据库文件、然后是创建表。然后我们创建一个单例类来专门管理数据库。
-
2.1 创建单例类
使用 command + n
创建一个类SQLManager
,实现单例
/// 单例
static let manager : SQLManager = SQLManager()
class func shareInstance() -> SQLManager{
return manager
}
-
2.2写一个方法创建数据库文件
创建数据库文件,首先需要一个文件名,然后通过文件名得到地址,再通过地址去创建数据库文件
/// 打开数据库
func openDB(DBName: String){
//1、拿到数据库路径
let path = DBName.documentDir()
//打印路径,以便拿到数据文件
print(path)
//2、转化为c字符串
let cPath = path.cString(using: String.Encoding.utf8)
/*
参数一:c字符串,文件路径
参数二:OpaquePointer 一个数据库对象的地址
注意Open方法的特性:如果指定的文件路径已有对应的数据库文件会直接打开,如果没有则会创建在打开
使用Sqlite_OK判断
sqlite3_open(cPath, &dbBase)
*/
/*
#define SQLITE_OK 0 /* Successful result */
*/
if sqlite3_open(cPath, &dbBase) != SQLITE_OK{
print("数据库打开失败")
return
}
}
-
2.3如果你运行没有打印错误提示,那么就是创建成功了,这个时候我们就创建一个表,表的设计如下:
有了 表的设计,我们就可以编写SQL语句了,写一个方法去完成表的创建,在创建表之前有一点我觉得有必要提一下:
在SQLite中除查询,其他的都是使用
exec
去执行,所以先把
exec
封装一下
func execSQL(sql : String) -> Bool {
// 1、先把OC字符串转化为C字符串
let cSQL = sql.cString(using: String.Encoding.utf8)
// 2、执行语句
/// 在SQLite3中,除了查询以外(创建/删除/更新)都是用同一个函数
/*
1. 已经打开的数据库对象
2. 需要执行的SQL语句,c字符串
3. 执行SQL语句之后的回调,一般写nil
4. 是第三个参数的第一个参数,一般传nil
5. 错误信息,一般传nil
SQLITE_API int SQLITE_STDCALL sqlite3_exec(
sqlite3*, /* An open database */
const char *sql, /* SQL to be evaluated */
int (*callback)(void*,int,char**,char**), /* Callback function */
void *, /* 1st argument to callback */
char **errmsg /* Error msg written here */
);
*/
if sqlite3_exec(dbBase, cSQL, nil, nil, nil) != SQLITE_OK {
return false
}
return true
}
创建表
@discardableResult func createTab() -> Bool{
//1、编写SQL语句
let sql = "CREATE TABLE IF NOT EXISTS T_Person\n" +
"(\n" +
"id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
"name TEXT NOT NULL,\n" +
"age INTEGER, \n" +
"money REAL DEFAULT 100.0\n" +
");"
print(sql)
let flag = execSQL(sql: sql)
if !flag {
print("创建表失败")
}
return flag
}
这里要注明一下,为什么SQL语句是这样子组成的,看了打印信息就明白了
CREATE TABLE IF NOT EXISTS T_Person
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
money REAL DEFAULT 100.0
);
-
2.4 增
刚才我们设计了一个表包括id、name、age、money,然后我们要进行插入数据,但是如果所有的数据都用单例来管理,如果我有5个表,并且每个月都有增删改查,那么我的单例会很臃肿,秉着睡的事情谁干的原则,这个时候我们在创建一个类:Person
,这个类自己去管理增删改查。
/// 插入
func insertSQL() -> Bool{
//断言
assert(name != nil, "姓名不能为空")
//1、编写SQL语句
//如果插入的是可选的,那么你会发现你插入的前面有一个Optional,这是很尴尬的事情,
//可以通过 ! 解决
var sql : String = ""
if id == -1 {
sql = "INSERT INTO T_Person (name,age,money) VALUES('\(name!)',\(age == -1 ? 0 : age),\(money == -1 ? 100.0 : money));"
}
else{
sql = "INSERT INTO T_Person (id,name,age,money) VALUES(\(id),'\(name!)',\(age == -1 ? 0 : age),\(money == -1 ? 100.0 : money));"
}
//3、执行语句
return SQLManager.shareInstance().execSQL(sql: sql)
}
-
2.5 删
/// 删除
func deleteSQL() -> Bool{
//1、编写SQL语句
let sql = sqlWithType(sql: "DELETE FROM T_Person", contactStr: "AND")
print("del - \(sql)")
let flag = SQLManager.shareInstance().execSQL(sql: sql)
return false
}
-
2.6 改
/// 更新
func updateSQL() -> Bool{
//断言
assert(name != nil, "姓名不能为空")
//1、编写SQL语句
let sql = "UPDATE T_Person \n" +
"SET name='\(name!)',age=\(age),money=\(money) \n" +
"WHERE id=\(id);";
//3、执行语句
return SQLManager.shareInstance().execSQL(sql: sql)
}
-
2.7 查
class func selectSQL(sql : String) -> [Person]{
//2、获取查询的数据
let dicts = SQLManager.shareInstance().selectSQL(sql: sql)
//3、创建一个数组用于保存模型
var datas = [Person]()
//4、遍历字典数组,生成模型数组
for dict in dicts{
datas.append(Person(dict: dict))
}
//5、返回模型数组
return datas
}
3、完成界面搭建:
项目结构图
作为主入口ViewController
包括了一个Toolbar
、一个TableView
、一个右按钮
。
其中toolBar
又连接着增、删、改、查、排序、分页等界面的入口,在其他中则是一些数据库的优化(插入大量数据的时候如何优化)。
3.1 增
界面包括四个
textfiled
,一个button
,一个label
,一个navigation item
- textfiled用来给用户输入数据
- button 用来执行插入操作
- label用来显示插入状态
- navigation item用于快速插入数据,便于测试
插入按钮代码如下:
这里需要注意的是:类型的判断,ID,age必须是Int类型,name 必须是String、而money必须是浮点型 Double,所以在取值的时候需要进行判断
@IBAction func insertData(_ sender: UIButton) {
//名字为空 不能插入
if nameTextfield.text?.characters.count == 0 {
status.text = statuText + "姓名不能为空"
return
}
//通过textField创建对象
var dict = [String : Any]()
//如果id输入了,就插入ID,否则自增长
if idTextfield.text?.characters.count != 0{
if idTextfield.text!.isAllNum() { //判断是不是纯数字
dict["id"] = (idTextfield.text! as NSString).intValue
}
}
//名字直接添加
dict["name"] = nameTextfield.text!
//如果输入了年龄并且是纯数字才添加
if ageTextfield.text?.characters.count != 0 {
if ageTextfield.text!.isAllNum() { //判断是不是纯数字
dict["age"] = (ageTextfield.text! as NSString).intValue
}
}
if moneyTextfield.text?.characters.count != 0 {
let double : Double = (moneyTextfield.text! as NSString).doubleValue
if double > 0{
dict["money"] = double
}
}
let p = Person(dict: dict)
if !p.insertSQL() {
status.text = statuText + "插入失败"
}else{
status.text = statuText + "插入成功"
}
}
3.2 删
界面包括四个
textfiled
,一个button
,一个label
,
- textfiled用来给用户输入数据
- button 用来执行插入操作
- label用来显示插入状态
删除按钮代码如下:
这里需要注意的是条件的拼接、当没有输入条件的时候默认把整个表所有数据删除,这里我们自定义了一个字典,前面是一个结构体,后面是一个可选的值,因为用户可能输入
@IBAction func deleteData(_ sender: UIButton) {
var dict = [Person_Property : Any?]()
dict[.id] = idTextfield.text
dict[.name] = nameTextfield.text
dict[.age] = ageTextfield.text
dict[.money] = moneyTextfield.text
let p = Person(myDict : dict)
if p.deleteSQL(){
statuLabel.text = statuText + "删除成功"
}
else{
statuLabel.text = statuText + "删除失败"
}
}
构造方法如下:
init(myDict : [Person_Property : Any?]){
super.init()
var dict = [String : Any]()
for (key,value) in myDict {
if (value as! String).characters.count > 0 {
if key.rawValue as String == "id" {
if (value as! String).isAllNum() { //并且全是数字
dict[key.rawValue as String] = Int((value as! NSString).intValue)
}
}else if key.rawValue as String == "name" {
dict[key.rawValue as String] = value
}else if key.rawValue as String == "age" {
if (value as! String).isAllNum() { // 并且全是数字
dict[key.rawValue as String] = Int((value as! NSString).intValue)
}
}else if key.rawValue as String == "money" { //
if (value as! String).isFloatValue() { //并且是浮点型
dict[key.rawValue as String] = (value as! NSString).doubleValue
}
}
}
}
setValuesForKeys(dict)
}
3.3 改
界面包括一个现实所有数据的
tableView
和一个二级页面,点击cell进入二级页面,修改特定的数据
这里我并没有把ID也列进来,因为当数据量达到一定量的时候如果你贸然去修改ID,并且这个ID是主键,
很有可能会造成主键冲突,所以ID就一般原则而言要唯一,且不修改
这里包括一个关闭按钮、界面采用present的方式显示,一个当前用户信息的label以及三个提供给用户输入的textfield,一个button以及一个更新状态的显示
3.4 查 、3.5 排序
由于这两个非常类似,界面布局都一致,所以就都拿到一起了
包括一个label
、一个textView
、一个button
、一个tableView
查找Code
class func queryPersons(condition : String) -> [Person]{
//如果输入的为空,就全部加载
if condition == ""{
return loadPersons()
}
//1、编写SQL语句
let sql = "SELECT * FROM T_Person WHERE \(condition);"
return selectSQL(sql: sql)
}
排序Code
/// 排序
///
/// - Parameter sort: 字段
/// - Returns: 数组
class func querySortPersons(sort : String) -> [Person]{
//如果输入的为空,就全部加载
if sort == "" {
return loadPersons()
}
//1、编写SQL语句
let sql = "SELECT * FROM T_Person ORDER BY \(sort);"
return selectSQL(sql: sql)
}
3.6 分页检索数据
这个界面也很简单,主要包括一个
textField
、三个Button
、一个TableView
- textField 用于输入每页显示的条目数
- 三个按钮分别作用于:开始所有、上/下一页
- tableView主要用于显示数据
检索代码,通过传入的m,n进行搜索,如果还不是很清楚的可以去我的上一篇文章查看。
class func queryLimitPerson(m : Int32, n : Int32) -> [Person]{
if n == 0 {
return loadPersons()
}
let sql = "SELECT * FROM T_Person LIMIT \(m),\(n)"
return selectSQL(sql: sql)
}
三个按钮code
private func limitDatas(m : Int32, n:Int32){
let pdatas = Person.queryLimitPerson(m : m , n: n)
if pdatas.count == 0{
showErrorText()
return
}
datas = pdatas
}
@IBAction func startLimitBtn(_ sender: UIButton) {
index = 0
limitDatas(m: Int32(index) * (textField.text! as NSString).intValue, n: (textField.text! as NSString).intValue)
}
@IBAction func nextBtn(_ sender: Any) {
index += 1
limitDatas(m: Int32(index) * (textField.text! as NSString).intValue, n: (textField.text! as NSString).intValue)
}
@IBAction func preBtn(_ sender: Any) {
index = (index - 1) < 0 ? 0 : (index - 1)
limitDatas(m: Int32(index) * (textField.text! as NSString).intValue, n: (textField.text! as NSString).intValue)
}
4、事务,预编译、线程安全
4.1 事务:
事务(Transaction)是一个对数据库执行工作单元。事务(Transaction)是以逻辑顺序完成的工作单位或序列,可以是由用户手动操作完成,也可以是由某种数据库程序自动完成。
事务(Transaction)是指一个或多个更改数据库的扩展。例如,如果您正在创建一个记录或者更新一个记录或者从表中删除一个记录,那么您正在该表上执行事务。重要的是要控制事务以确保数据的完整性和处理数据库错误。
使用下面的命令来控制事务:
BEGIN TRANSACTION:开始事务处理。
COMMIT:保存更改,或者可以使用 END TRANSACTION 命令。
ROLLBACK:回滚所做的更改。
事务控制命令只与 DML 命令 INSERT、UPDATE 和 DELETE 一起使用。他们不能在创建表或删除表时使用,因为这些操作在数据库中是自动提交的。
- 然后我们在SQLManager中添加事务代码
/// 开启事务
func beginTransaction(){
execSQL(sql: "BEGIN TRANSACTION")
}
/// 提交事务
func commitTransaction(){
execSQL(sql: "COMMIT TRANSACTION")
}
/// 回滚
func rollbackTransaction(){
execSQL(sql: "ROLLBACK TRANSACTION")
}
4.2 预编译
关于预编译的意思,网上很多种,我这里打一个比喻:
你是一个司机,今天下午要去仓库A里面去那2000件货物。
预编译:你打电话给仓库,让仓库管理员给你准备好货物,然后你准时到达,装货走人。
不是预编译:你下午三点到了仓库,再让仓库管理员给你准备货物,清点完成后再装货,在走人。
所以预编译是可以提高效率的。
预编译执行语句:
@discardableResult func batchExecSQL(sql : String , args: CVarArg...) -> Bool{
//1转化为C字符串
let cSql = sql.cString(using: String.Encoding.utf8)!
//2、执行预编译
var stmt : OpaquePointer? = nil
if sqlite3_prepare_v2(dbBase, cSql, -1, &stmt, nil) != SQLITE_OK {
print("预编译失败")
sqlite3_finalize(stmt)
return false
}
//3、进行数据绑定
/*
这里要注意,下标从1开始。
*/
var index : Int32 = 1
/*
sqlite3_bind_XX(句柄, 下标(从1开始), 值)
*/
for objc in args{
if objc is Int{
sqlite3_bind_int64(stmt, index, sqlite3_int64(objc as! Int))
}else if objc is Double{
sqlite3_bind_double(stmt, index, objc as! Double)
}else if objc is String{
//得到字符串
let text = objc as! String
//得到C字符串
let cText = text.cString(using: String.Encoding.utf8)!
/*
sqlite3_bind_text(句柄, 下标, 字符串, 字符串长度 -1 表示系统自己计算, OC传入nil,SWIFT不行)
1 句柄
2 下标
3 C字符串
4 C字符串长度 -1 自动计算
5 OC 传入nil 但是SWIFT不行,因为对象提前释放掉了,会导致插入的数据不对
typedef void (*sqlite3_destructor_type)(void*);
#define SQLITE_STATIC ((sqlite3_destructor_type)0)
#define SQLITE_TRANSIENT ((sqlite3_destructor_type)-1)
第五个参数如果传入SQLITE_STATIC/nil, 那么系统不会保存需要绑定的数据, 如果需要绑定的数据提前释放了, 那么系统就随便绑定一个值
第五个参数如果传入SQLITE_TRANSIENT, 那么系统会对需要绑定的值进行一次copy, 直到绑定成功之后再释放
但是Swift中并不能直接写 SQLITE_TRANSIENT 或者 -1,需要自定义一个SQLITE_TRANSIENT,来覆盖系统的
在 124 行中
*/
sqlite3_bind_text(stmt, index, cText, -1, SQLITE_TRANSIENT)
}
index += 1
}
//4、执行SQL语句
if sqlite3_step(stmt) != SQLITE_DONE {
print("执行SQL语句失败")
return false
}
//5、重置STMT
if sqlite3_reset(stmt) != SQLITE_OK{
print("重置句柄失败")
return false
}
//6、关闭STMT
sqlite3_finalize(stmt)
return true
}
4.2 线程安全
在多线程中操作数据库是有安全隐患的,可能会发生这里在执行插入、另外又在执行删除、更新或者其他的指令,所以多线程操作数据库一定要保证线程安全。
如何保证呢?
- 通过创建一个创行队列
多线程执行操作模式
// MARK - Child Thread
/*
1 一个唯一的对列名
2 优先级
3 队列类型
4
*/
private let dbQueue = DispatchQueue(label: "com.codepgq.github", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency.inherit, target: nil)
//DispatchQueue(label:"com.appcoda.queue2", qos:DispatchQoS.userInitiated)
func execQueueSQL(action : @escaping (_ manager : SQLManager) ->()){
//开一个子线程
DispatchQueue.global().async {
action(self)
}
}
或者通过这种方式创建串行队列
// 创建一个串行队列
fileprivate let dbQueue = DispatchQueue(label: "com.codepgq.github", attributes: [])
4.4OK,了解了上面的知识,我们就搭建最后一个页面:
界面很简单,就不做介绍了,主要是通过判断打开了哪些switch来进行芳芳的选择。
所有的方法如下:
@IBAction func startInsert(_ sender: Any) {
if isSerting {
showErrorText(message : "正在插入")
return
}
isSerting = true
//计算值
let value : Int8 = isOnValue(sw: openTrans) * 100 + 10 * isOnValue(sw: openThread) + isOnValue(sw: openPrepare)
print(NSString.init(format: "value - %03d", value))
/*
001 010 100 110 101 011 000 111
*/
switch value {
case 001:
//开启了预编译
insertDatas(true)
case 010:
//开启了子线程
openChildThread()
case 100:
//开启了事务
openTransaction()
case 011:
//开启了子线程 预编译
openTheadAndPrepare()
case 110:
//开启了事务 子线程
openTransAndTheard()
case 101:
//开启了事务 预编译
openTransAndPrepare()
case 111:
//开启了事务 子线程 预编译
openAll()
default:
//啥都没开
insertDatas(false)
}
}
/// 开启事务和预编译
func openTransAndPrepare(){
SQLManager.shareInstance().beginTransaction()
insertDatas(true)
SQLManager.shareInstance().commitTransaction()
}
/// 开启事务和线程
func openTransAndTheard(){
SQLManager.shareInstance().execQueueSQL { (manager) in
self.insertDatas(false)
}
}
//开启线程和预编译
func openTheadAndPrepare(){
SQLManager.shareInstance().execQueueSQL { (manager) in
self.insertDatas(true)
}
}
//全部打开
func openAll(){
SQLManager.shareInstance().execQueueSQL { (manager) in
manager.beginTransaction()
self.insertDatas(true)
manager.commitTransaction()
}
}
//开启子线程
func openChildThread(){
SQLManager.shareInstance().execQueueSQL { (manager) in
self.insertDatas(false)
}
}
//开启事务
func openTransaction(){
//获取数据库对象
let manager = SQLManager.shareInstance()
//开始事务
manager.beginTransaction()
//插入数据
insertDatas(false)
//提交事务
manager.commitTransaction()
}
//插入数据
private func insertDatas(_ prepare : Bool) {
//得到开始时间
let start = CFAbsoluteTimeGetCurrent()
startLabel.text = "开始时间:\(start)"
print(#function,"\(prepare ? "预编译" : "未开启预编译" )")
//开始插入
for index in 0.. Int8{
return sw.isOn ? 1 : 0
}
什么都没开启的情况下插入10000条数据:
开启了预编译的情况下插入10000条数据:
开启了事务的情况下插入10000条数据:
开启了预编译和事务的情况下插入10000条数据:
开启了线程的时候就不会阻塞UI,可自行测试。
Demo传送门
写的不好见谅,但是如果对你有帮助,那么我就心满意足了。
码字不易、喜欢点一下。