ios學習筆記之SQLite初學者手冊(下)

前言

這篇是接著上篇ios學習筆記之SQLite初學者手冊(上)的,對如何優化在執行大量插入語句之后的性能,如何回滾數據以及如何查詢做一簡單的說明。

一 性能優化

1 DML語句 通過綁定參數實現插入語句

準備語句(prepared statement)對象
準備語句(prepared statement)對象一個代表一個簡單SQL語句對象的實例,這個對象通常被稱為“準備語句”或者編譯好的SQL語句

操作歷程:

  1. 使用sqlite3_prepare_v2或相關的函數創建這個對象 如果執行成功,則返回SQLITE_OK,否則返回一個錯誤碼
    2.使用sqlite3_bind_*()給宿主參數(host parameters)綁定值
    3.通過調用sqlite3_step() 一次或多次來執行這個sql
    4.使用sqlite3_reset()重置這個語句,然后回到第2步,這個過程做0次或多次
    5.使用sqlite3_finalize()銷毀這個對象,防止內存泄露

假設我們要插入很多條數據,則第2、3、4步重復執行,第1、5步就不需要重復執行了

示例:
(創建數據庫接上篇)
ios學習筆記之SQLite初學者手冊(上)

為Student類添加方法

//    通過綁定的方式,來插入語句
    func bindInsert() -> () {
        let sql = "insert into t_stu(name,age,score) values(?,?,?)"
//       1 根據sql語句,創建,準備語句,問號代表占位
        
//        參數一:一個已經打開的數據庫
//        參數二:sql字符串 "lsfjfflkf"
//        參數三:即將取出字符串(參數二這個字符串)的長度,例如:2,就是從前面開始,取2個字符
//        -1代表自動計算
//        參數四:預處理語句
//        參數五:根據參數三的長度,取出參數二的值以后,剩余的數字,例如取出了參數二中的ls
//        兩個字符,那么參數五就是fjfflkf
        
        let db = SQLiteTool.shareInstance.db
        
        var stmt:OpaquePointer? = nil
        
        if   sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
            print("預處理失敗")
            return
        }
        
//        2 綁定參數
//        參數一:準備語句
//        參數二:綁定值的索引,索引從1開始
        
        
        sqlite3_bind_int(stmt, 2, 20) // 綁定第二個參數,賦值20
        sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
//        綁定文本
//        參數一:準備語句
//        參數二:綁定值的索引,1
//        參數三:綁定的值
//        參數四:值取出多少長度,-1,取出所有
//        參數五:值的處理方式
//        下面兩個是宏定義,swift里面沒有宏定義
//        (按住command點擊sqlite3_bind_text方法,索索SQLITE_STATIC及SQLITE_TRANSIENT,然后會發現:
//        typedef void (*sqlite3_destructor_type)(void*);
//        #define SQLITE_STATIC      ((sqlite3_destructor_type)0)
//        #define SQLITE_TRANSIENT   ((sqlite3_destructor_type)-1)
//        sqlite3_destructor_type是指向函數的指針
        
//        )
//        SQLITE_STATIC:認為參數是一個常量,不會被釋放,處理方案:不做任何引用
//        SQLITE_TRANSIENT:會對參數進行一個引用
//        
        let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
        
//        此時按住option鍵點擊SQLITE_TRANSIENT會看到:let SQLITE_TRANSIENT: sqlite3_destructor_type,就是說此時SQLITE_TRANSIENT的類型是sqlite3_destructor_type
        
        sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
        
//       3 執行sql語句,準備語句
        if sqlite3_step(stmt) == SQLITE_DONE{
            print("執行成功")
        }
        
//       4 重置語句
        sqlite3_reset(stmt)
        
//        5 釋放準備語句
        sqlite3_finalize(stmt)
    }
    
    func insertStudent() -> () {
        
        let sql = "insert into t_stu(name,age,score) values('\(name)',\(age),\(score))"
        
        if SQLiteTool.shareInstance.execute(sql: sql) {
            print("插入行成功")
        }else{
            print("插入行失敗")
        }
        
        
    }

在控制器中測試

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {   
        let stu = Student(name:"wangwu",age:39,score:99)
        stu.bindInsert()
        
    }

刷新Navicat Premium,發現插入成功

相比較上篇里講的給我一條語句sql,然后去執行sqlite3_exec(db, sql, nil, nil, nil)語句,我們的綁定參數確實是復雜了很多,但是sqlite3_exec其實就是對綁定參數方法的一個封裝,當我們插入很多條語句時,分別查看兩種方法的性能,那么肯定是綁定參數這種方法性能更好。

下面在控制器中對兩種方法進行執行時間測試:
用sqlite3_exec方法:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        
        let stu = Student(name:"wangwu",age:39,score:99)
        
        let beginTime = CFAbsoluteTimeGetCurrent()
        
    
        for i in 0..<1000{
            stu.insertStudent()
        }
        
        let endTime = CFAbsoluteTimeGetCurrent()
        print(endTime - beginTime)
    }

測試三次,點擊三次屏幕,(將所有的print("插入行成功")等都注釋掉,因為這個很耗時)結果:

   執行成功
    0.5521399974823
    0.540975987911224
    0.548547983169556

用綁定參數方法:
對bindInsert()方法進行修改,將第2、3、4步放入循環

//    通過綁定的方式,來插入語句
    func bindInsert() -> () {
        let sql = "insert into t_stu(name,age,score) values(?,?,?)"

        
        let db = SQLiteTool.shareInstance.db
        
        var stmt:OpaquePointer? = nil
        
        if   sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
            print("預處理失敗")
            return
        }
        


        for i in 0..<1000{
            //        2 綁定參數
            let newI = Int32(i)  // 將i轉換成Int32類型
            sqlite3_bind_int(stmt, 2, newI) // 綁定第二個參數,賦值i
            sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
            let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
            
            sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
            
            //       3 執行sql語句,準備語句
            if sqlite3_step(stmt) == SQLITE_DONE{
//                print("執行成功")
            }
            
            //       4 重置語句
            sqlite3_reset(stmt)
        }
        
//        5 釋放準備語句
        sqlite3_finalize(stmt)
    }
    
    func insertStudent() -> () {
        
        let sql = "insert into t_stu(name,age,score) values('\(name)',\(age),\(score))"
        
        if SQLiteTool.shareInstance.execute(sql: sql) {
//            print("插入行成功")
//        }else{
//            print("插入行失敗")
        }  
    }

控制器

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        
        let stu = Student(name:"wangwu",age:39,score:99)
        
        let beginTime = CFAbsoluteTimeGetCurrent()
        
    
        stu.bindInsert()
        
        let endTime = CFAbsoluteTimeGetCurrent()
        print(endTime - beginTime)
    }

結果:

//    執行成功
//    0.494072020053864
//    0.520428955554962
//    0.516857028007507
//    0.521933019161224

多次測試,發現綁定參數方法略微快點,這還不是我們想要的,如果繼續優化,該怎么辦呢???其實,如果使用sqlite3_exec或者,sqlite3_step()來執行sql語句,會自動開啟一個”事務“,然后,自動提交”事務“,那么針對這個問題,我們可以這樣解決:只需要手動開啟事務,手動提交任務,這時候,函數內部,就不會自動開啟和提交事務,這樣可以大大提高執行效率。

2 手動開啟事務

為SQLiteTool類增加方法

// 手動開啟事務
func beginTransaction() -> () {
        let sql = "begin transaction"
        execute(sql: sql)
    }
    
// 手動關閉事務
    func commitTransation() -> () {
        let sql = "commit transaction"
        execute(sql: sql)
    }

將其插入到Student的綁定代碼中

//    通過綁定的方式,來插入語句
    func bindInsert() -> () {
        let sql = "insert into t_stu(name,age,score) values(?,?,?)"

        
        let db = SQLiteTool.shareInstance.db
        
        var stmt:OpaquePointer? = nil
        
        if   sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK{
            print("預處理失敗")
            return
        }
        

//        手動開啟事務
        SQLiteTool.shareInstance.beginTransaction()

        for i in 0..<1000{
            //        2 綁定參數
            let newI = Int32(i)// 將i轉換成Int32類型
            
            sqlite3_bind_int(stmt, 2, newI) // 綁定第二個參數,賦值i
            sqlite3_bind_double(stmt, 3, 60.0) // 綁定第三個參數,賦值60.0
            let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
            
            sqlite3_bind_text(stmt, 1, "qingyun", -1, SQLITE_TRANSIENT) //綁定第一個參數
            
            //       3 執行sql語句,準備語句
            if sqlite3_step(stmt) == SQLITE_DONE{
//                print("執行成功")
            }
            
            //       4 重置語句
            sqlite3_reset(stmt)
        }
        
//        提交事務
        SQLiteTool.shareInstance.commitTransation()
        
//        5 釋放準備語句
        sqlite3_finalize(stmt)
    }

在控制器里面執行

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        
        let stu = Student(name:"wangwu",age:39,score:99)
        
        let beginTime = CFAbsoluteTimeGetCurrent()
        
    
        stu.bindInsert()
        
        let endTime = CFAbsoluteTimeGetCurrent()
        print(endTime - beginTime)
    }
    

結果:

    執行成功
    0.00568002462387085

可以看出效率大大提高了。

二 回滾數據

當我們做多條修改時,假如有的語句修改成功,而有的修改失敗,那么最終我們是否要提交呢?在數據庫里,我們只有當全部數據修改成功時再整體提交,如果部分執行失敗,就不提交,保持修改之前的數據。

示例:

我們先在表t_stu中增加字段money,然后手動輸入兩條數據

在Student類里添加更新數據的類方法

class func update(sql:String) -> (Bool) {
        
     return   SQLiteTool.shareInstance.execute(sql: sql)
    }

在SQLiteTool類添加回滾的語句

func rollbackTransaction() -> () {
        let sql = "rollback transaction"
        execute(sql: sql)
    }

控制器:故意將語句“let result2 = Student.update(sql: "update t_stu set money1 = money + 10 where name = 'lisi'")” 中的money錯寫成money1

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

//        zhangsan money -10
        
        SQLiteTool.shareInstance.beginTransaction()
        
        let result1 = Student.update(sql: "update t_stu set money = money - 10 where name = 'zhangsan'") // 將張三的錢減10
        
//        lisi  money +10
        
        let result2 = Student.update(sql: "update t_stu set money1 = money + 10 where name = 'lisi'") // 將李四的錢加10
        
        if result1 && result2 {
            SQLiteTool.shareInstance.commitTransation()  // 當兩條修改語句全部正確執行,才會提交
            print("修改數據成功,提交")
        }else{
            SQLiteTool.shareInstance.rollbackTransaction() // 只要有一條操作失敗,就回滾,滾到修改之前的數據
            print("修改數據失敗,回滾")
        }
        
    }

刷新數據庫,發現數據沒變。但是如果我們將錯寫的money1改回money,則提交成功。

三 DQL查詢語句

1 代碼

為Student方法添加兩個新的類方法,queryAllStmt方法是通過綁定來進行查詢,queryAll是通過sqlite3_exec進行查詢

class func queryAllStmt() -> () {
//        準備語句 歷程
        let sql = "select * from t_stu "
        let db = SQLiteTool.shareInstance.db
        
        
//        一.創建準備語句
//        參數一:一個已經打開的數據庫
//        參數二:sql語句
//        參數三:字符串,取的長度,-1代表全選
//        參數四:準備語句的指針
//        參數五:剩余待取的字符串
        var stmt:OpaquePointer? = nil
        
        
        if  sqlite3_prepare_v2(db, sql, -1, &stmt, nil) != SQLITE_OK {
            print("準備預處理語句失敗")
            return
        }
        
//        二.綁定參數(這一步可以省略)
//        三.執行準備語句
//        sqlite3_step 的作用 執行DQL語句時,會把執行得到的結果放到準備語句(stmt)中
        while  sqlite3_step(stmt) == SQLITE_ROW {
//            先判斷,如果還有數據(整行數據),則繼續執行,while循環是一行行取
//            讀取數據
//           1. 從準備語句里讀取
            let count = sqlite3_column_count(stmt) // sqlite3_column_count(stmt)是用來計算預處理語句里,一共有多少列的
            for i in 0..<count{
//                for循環是一列列取
                
//               2. 取出列的名稱
              let columnName = sqlite3_column_name(stmt, i)
                let columnNameStr = String(cString:columnName!,encoding:String.Encoding.utf8)
                print(columnNameStr as Any)
                
//                3.取出列的值
//                不同的類型,是通過不同的函數獲取
//                3.1 獲取這一列的類型
                let type = sqlite3_column_type(stmt, i)
                
//                3.2 根據不同的類型,使用不同的函數
                if type == SQLITE_INTEGER {
                    let value = sqlite3_column_int(stmt, i)
                    print(value)
                }
                
                if type == SQLITE_FLOAT {
                    let value = sqlite3_column_double(stmt, i)
                    print(value)
                }
                
                if type == SQLITE_TEXT {
//                 value:UnsafePointer<UInt8>?
                    let value = sqlite3_column_text(stmt, i)
                    let valueStr = String(cString: value!)
                    
                    print(valueStr)
                }
            }
            
            
        }
        
//        四.重置準備語句(這一步可以省略)
//        五.釋放準備語句
        sqlite3_finalize(stmt)
        
    }
    
    
    
   class   func queryAll() -> () {
        let sql = "select * from t_stu"
        let db = SQLiteTool.shareInstance.db
        
        
//        參數一:一個打開的數據庫
//        參數二:sql語句
//        參數三:回調代碼塊
//             參數1:傳遞過來的值
//             參數2:列的個數
//             參數3:值的數組
//             參數4:列名稱的數組
//             返回值:0 繼續查詢 1:終止查詢
//        參數四:傳遞到參數三里面的第一個參數
//        參數五:錯誤信息
        
        sqlite3_exec(db, sql, { (firstValue, columnCount, values, columnNames) -> Int32 in
            let count = Int(columnCount)
            for i in 0..<count{
//                列的名稱
                let columnName = columnNames?[i]
                let columnNameStr = String(cString:columnName!,encoding:String.Encoding.utf8)
//                值
                let value = values?[i]
                let valueStr = String(cString:value!,encoding:String.Encoding.utf8)
//                let valueStr = String.Encoding.utf8
                print(columnNameStr as Any,valueStr as Any)
            }
            return 0
            
        }, nil, nil)
        
        
    }

在控制器分別調用兩個方法

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {


//        Student.queryAll()
        Student.queryAllStmt()
        
    }

結果:

Optional("id")
1
Optional("name")
zhangsan
Optional("age")
10
Optional("score")
99.0
Optional("money")
100.0
Optional("id")
2
Optional("name")
lisi
Optional("age")
9
Optional("score")
88.0
Optional("money")
100.0
2 將綁定方法與sqlite3_exec方法進行對比

①.使用后者,解析出來的都是字符串,而使用綁定的方法基本類型都可以解析
②.使用后者,無法插入二進制,因為插入二進制時,是當做字符串進行解析的,而使用綁定的方法,可以在綁定參數時,綁定二進制(sqlite3_bind_blob)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容