ios swift版 sqlite3詳解

iOS中的SQLite3的封裝與詳細應用html

 

SQLite是一個開源的嵌入式關係數據庫,特色是易使用、高效、安全可靠、可移植性強。git

iOS中的本地持久化存儲

NSUserDefault:通常用於存儲小規模數據、業務邏輯弱的數據。github

keychain: 蘋果提供的可逆存儲,由於有着只要app不重裝系統、能夠同步iCloud的特性,通常用來對用戶的標識符或者一些須要加密的小數據進行存儲。sql

歸檔:主要原理是對數據進行序列化、反序列化操做後,寫入、讀出數據。方便便捷易使用,缺點查詢、更改數據耗時耗性能。數據庫

數據庫:主要的有三種sqlite三、core data、realm。其中core data只是xcode對sqlite的界面化的封裝原理類似,realm官方文檔.swift

關於sqlite本文將主要介紹。

SQLite3中主要函數介紹

sqlite3_open(文件路徑,sqlite3 **):文件名若不存在,則會自動建立xcode

sqlite3_close(sqlite3 *):關閉數據庫安全

sqlite3__finalize(sqlite3_stmt *pStmt): 釋放數據庫多線程

sqlite3_errmsg(sqlite3*):輸出數據庫錯誤app

sqlite3__exec(sqlite3 ,const char sql, sqlite3_callback,void ,char *errmsg):

參數1:open函數獲得的指針。
參數2:一條sql語句
參數3:sqlite3_callback是回調,當這條語句執行後,sqlite3會調用你提供的這個函數,回調函數
參數4:void *是本身提供的指針,能夠傳遞任何指針到這裏,這個參數最終會傳到回調函數裏面,若是不須要傳到回調函數裏面,則能夠設置爲NULL
參數5:錯誤信息,當執行失敗時,能夠查閱這個指針

sqlite3_prepare_v2(sqlite3 db,const char zSql, int nByte,sqlite3_stmt ppStmt,const char pzTail):

參數3:表示前面sql語句的長度,若是小於0,sqlite會自動計算它的長度
參數4:sqlite3_stmt指針的指針,解析後的sql語句就放在該結構裏
參數5:通常設爲0

sqlite3_step(sqlite3_stmt*):

參數爲sqlite3_prepare_v2中的sqlite3_stmt
返回SQLITE_ROW 表示成功

sqlite3_bind_text(sqlite3_stmt, int, const char, int n, void()(void)):

參數1:sqlite3_prepare_v2中的sqlite3_stmt
參數2:對應行數
參數3:對應行數的值
參數4:對應行數的值的長度,小於0自動計算長度
參數5:函數指針,主要處理特殊類型的析構

sqlite3_key( sqlite3 db, const void pKey, int nKey)

參數2:密鑰
參數3:密鑰長度

swift與c的類型轉換

int => CInt

char => CChar / CSignedChar

char* => CString

unsigned long = > CUnsignedLong

wchar_t => CWideChar

double => CDouble

T* => CMutablePointer

void* => CMutableVoidPointer

const T* => CConstPointer

const void* => CConstVoidPointer

等等 …
參考地址 http://www.cocoachina.com/industry/20140619/8884.html

建立或者打開數據庫

程序中‘db’不能爲空,若是爲空,表示打開數據庫失敗或者關閉了數據庫。

@discardableResult   private func openDB() -> Bool{    
if db == nil {        
 let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/\(DB_NAME)"  print(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])        
 if sqlite3_open(path.cString(using: String.Encoding.utf8)!,&db) != SQLITE_OK {            closeDb()            
  return false  }else {            //對數據庫進行加密   sqlite3_key(db, SAFE_KEY.cString(using: String.Encoding.utf8), Int32(SAFE_KEY.characters.count))  }  sqlite3_busy_handler(db, { (ptr,count) in   usleep(500000)//若是獲取不到鎖,表示數據庫繁忙,等待0.5秒   print("sqlite is locak now,can not write/read.")            
  return 1   //回調函數返回值爲1,則將不斷嘗試操做數據庫。   }, &db)  }        
return true}

經過sql執行數據庫操做

因爲防止多線程操做數據庫,每次執行數據庫操做添加同步鎖。
sqlite3_exec函數基本上支持全部的數據庫執行語句除了含有特殊類型的數據(二進制),含有特殊類型的數據會採用另外一種方式處理下面會闡述。

@discardableResult public  func execSql(sql:String)->Bool {
 objc_sync_enter(self)
 if  !self.openDB() {
  objc_sync_exit(self)
  return false
 }
 var err: UnsafeMutablePointer<Int8>? = nil
 if sqlite3_exec(db,sql.cString(using: String.Encoding.utf8)!,nil,nil,&err) != SQLITE_OK {
  if let error = String(validatingUTF8:sqlite3_errmsg(db)) {
   print("execute failed to execute  Error: \(error)")
  }
  objc_sync_exit(self)
  return false
 }

 objc_sync_exit(self)
 return true
}

查詢數據庫

按照數據庫的行數依次查詢,輸出sql條件的全部數據

public  func querySql(sql:String) -> [[String:Any]]? {
 objc_sync_enter(self)
 if  !self.openDB() {
  objc_sync_exit(self)
  return nil
 }
 var arr:[[String:Any]] = []
 var  statement: OpaquePointer? = nil
 if sqlite3_prepare_v2(db,sql.cString(using: String.Encoding.utf8)!,-1,&statement,nil) == SQLITE_OK {
  while sqlite3_step(statement) == SQLITE_ROW {
   let columns = sqlite3_column_count(statement)
   var row:[String:Any] = Dictionary()
   for i in 0..<columns {
    let type = sqlite3_column_type(statement, i)
    let chars = UnsafePointer<CChar>(sqlite3_column_name(statement, i))
    let name =  String.init(cString: chars!, encoding: String.Encoding.utf8)

    var value: Any
    switch type {
     case SQLITE_INTEGER:
      value = sqlite3_column_int(statement, i)
     case SQLITE_FLOAT:
      value = sqlite3_column_double(statement, i)
     case SQLITE_TEXT:
      let chars = UnsafePointer<CUnsignedChar>(sqlite3_column_text(statement, i))
      value = String.init(cString: chars!)

     case SQLITE_BLOB:
      let data = sqlite3_column_blob(statement, i)
      let size = sqlite3_column_bytes(statement, i)
      value = NSData(bytes:data, length:Int(size))
     default:
       value = ""
        ()
      }    row.updateValue(value, forKey: "\(name!)")    }    arr.append(row)    }  }  sqlite3_finalize(statement) objc_sync_exit(self) if arr.count == 0 {    return nil }else{  return arr } }

引入事務,加快數據庫寫入

事務(Transaction)是一個對數據庫執行工做單元。事務(Transaction)是以邏輯順序完成的工做單位或序列,能夠是由用戶手動操做完成,也能夠是由某種數據庫程序自動完成。
事務(Transaction)是指一個或多個更改數據庫的擴展。例如,若是您正在建立一個記錄或者更新一個記錄或者從表中刪除一個記錄,那麼您正在該表上執行事務。重要的是要控制事務以確保數據的完整性和處理數據庫錯誤。
實際上,您能夠把許多的 SQLite 查詢聯合成一組,把全部這些放在一塊兒做爲事務的一部分進行執行

BEGIN TRANSACTION 開啓一個事務

COMMIT TRANSACTION 提交事務是否成功

ROLLBACK TRANSACTION 回滾事務,當數據庫事務操做失敗後,還原以前的操做。

注意:事務並不能批量優化查詢速度。

public func doTransaction(exec: ((_ db:OpaquePointer)->())?) {
 objc_sync_enter(self)
 if  !self.openDB() {
  objc_sync_exit(self)
  return
 }
 if exec != nil {
  var err: UnsafeMutablePointer<Int8>? = nil  if sqlite3_exec(db, "BEGIN TRANSACTION", nil, nil, &err) == SQLITE_OK {     exec!(db!)    if sqlite3_exec(db, "COMMIT TRANSACTION", nil, nil, &err) == SQLITE_OK {      print("提交事務成功")
   }else {     print("提交事務失敗緣由\(err)")    if let error = String(validatingUTF8:sqlite3_errmsg(db)) {    print("execute failed to execute  Error: \(error)")    }    if sqlite3_exec(db, "ROLLBACK TRANSACTION", nil, nil, &err) == SQLITE_OK {    print("回滾事務成功")    }  } }else {  if sqlite3_exec(db, "ROLLBACK TRANSACTION", nil, nil, &err) == SQLITE_OK {   print("回滾事務成功")   }   }  sqlite3_free(err) }  objc_sync_exit(self) }

SQLite3支持有限的 ALTER TABLE 操做

SQLite 有有限地 ALTER TABLE 支持。你可使用它來在表的末尾增長一列,可更改表的名稱。 若是須要對錶結構作更復雜的改變,則必須從新建表。重建時能夠先將已存在的數據放到一個臨時表中,刪除原表, 建立新表,而後將數據從臨時表中複製回來。在增長表列時,需注意:由於app在市場上存在許多版本,各個版本的數據庫表的結構可能存在梯度的差別,代碼中使用就須要加入版本控制了。例如代碼中添加一個‘id’字段。

let defaults = UserDefaults.standard
let version = defaults.value(forKey: USER_SQL_VERSION)//控制刪除數據庫的版本記錄
let update = defaults.value(forKey: USER_SQL_UPDATE)//控制增長數據庫字段的版本記錄
if let version = version ,(version as! String) == USER_SQL_VERSION_CODE {
 if let update = update as? String{
  if  let intUpdate = Int(update.replacingOccurrences(of: ".", with: "")) {
   if intUpdate > 100 {

    if let _ = SQLiteTable.shared.querySql(sql: "select id from \(USER_TABLENAME)"){

    } else {
     SQLiteTable.shared.execSql(sql: "ALTER TABLE \(USER_TABLENAME)  ADD COLUMN  \("id") INTEGER DEFAULT 0 IF NOT EXISTS")

    }

   }
  }

 }

 defaults.setValue(USER_SQL_UPDATE_CODE, forKey: USER_SQL_UPDATE)
 }else {
  SQLiteTable.shared.dropTable(tableName: USER_TABLENAME)
  defaults.setValue(USER_SQL_VERSION_CODE, forKey: USER_SQL_VERSION)
}

SQLite3中添加索引

   /// 建立一張表
    ///
    /// - Parameters:
    ///   - tableName: 表名
    ///   - data: 數據字段
    ///   - dataArray: 添加索引的字段
    /// - Returns: 是否成功   
public func createTable(tableName:String, andColoumName data:[String:String] ,andAddIndex dataArray:[String]) -> Bool {
 let result = self.createTable(tableName: tableName, andColoumName: data)
 dataArray.forEach { (str) in
  self.execSql(sql: "CREATE INDEX IF NOT EXISTS index_\(str)  ON \(tableName) (\(str))")
 }

  return result

}

處理Dada類型數據

sqlite3__exec函數並非萬能的,它就沒法處理二進制數據。處理二進制數據,須要的是另外一種方法。
先對錶的數據解析而後綁定到結構體中,而後對數據庫進行INSERT DELETE 或者 UPDATE操做。

private func bindSqlType(sql:String, params:[Any]?) -> OpaquePointer? {

 if  !self.openDB() {
  return nil
 }

  var stmt:OpaquePointer? = nil
  let someCharChar = unsafeBitCast(-1, to:sqlite3_destructor_type.self)
  let result = sqlite3_prepare_v2(db, sql.cString(using: String.Encoding.utf8)!, -1, &stmt, nil)
  if result != SQLITE_OK {
  sqlite3_finalize(stmt)
  if let error = String(validatingUTF8:sqlite3_errmsg(db)) {
    print("execute failed to execute  Error: \(error)")
  }
  return nil
 }

 if let  params = params {

  let count = CInt(params.count)
  if sqlite3_bind_parameter_count(stmt)  == count {
   var result:CInt = 0

  for index in 1...count {

   if let txt = params[index-1] as? String {
    result = sqlite3_bind_text(stmt, CInt(index), txt, -1, someCharChar)
   } else if let data = params[index-1] as? NSData {
    result = sqlite3_bind_blob(stmt, CInt(index), data.bytes, CInt(data.length), someCharChar)
   }else if let val = params[index-1] as? Double {
    result = sqlite3_bind_double(stmt, CInt(index), CDouble(val))
   } else if let val = params[index-1] as? Int {
    result = sqlite3_bind_int64(stmt, index, Int64(val))
   } else {
    result = sqlite3_bind_null(stmt, CInt(index))
   }

   if result != SQLITE_OK {
    sqlite3_finalize(stmt)
    if let error = String(validatingUTF8:sqlite3_errmsg(db)) {
       print("execute failed to execute  Error: \(error)")
      }
      return nil
     }
    }
   }

  }
 return stmt
}

SQLite3的一些小坑

1.SQL 標準規定,在字符串中,單引號須要使用逃逸字符(」),即在一行中使用兩個單引號。

2.每次操做完數據庫記得關閉數據庫,防止多個數據庫混淆。

SQLite3中查詢效率優化

1.添加索引 在demo中測試發現,插入50000行數據,同時作查詢時發現,添加索引耗時76s,不添加花費256s。添加索引需注意:

添加索引的原則是爲了查詢更加快,可是一張表內不能建立太多索引,由於索引只增長了相應的 select 的效率,但同時也下降了 insert 及 update 的效率,一個表的索引數最好不要超過6個。

在使用索引字段做爲條件時,若是該索引是複合索引,那麼必須使用到該索引中的第一個字段做爲條件時才能保證系統使用該索引,不然該索引將不會被使 用,而且應儘量的讓字段順序與索引順序相一致。

2.在使用索引字段時,須要避免使用OR/BETWEEN/LIKE這些語法,這樣會避免使用索引而對錶進行全局掃描。如demo中使用like語法與‘==’插入50000行數據,同時作查詢時,對比發現分別耗時546s和76s。

SQLite3加密

使用SQLCipher連接地址這個第三方庫對數據庫進行加密。

sqlite3_key 加密函數

sqlite3_rekey 修改密碼

最後

點擊連接地址進入觀看完整代碼。

具體請參考: https://github.com/tianjifou/CoreSQLite3.git;

相關文章
相關標籤/搜索