SQLite的介紹
在iOS存儲資料的方式
- 為什麼要存儲資料?
- 在開發中展示的大部分資料是從網絡伺服器中加載出來的
- 但是如果所有的資料每次都從網絡請求,則會有相應的問題.
- 浪費使用者的流量
- 在使用者沒有網絡時,無法正常顯示頁面
- 将網絡資料存儲在本地,不用每次都加載
- 減少使用者網絡流量開銷
- 使用者在沒有網絡時,依然可以展示一些資料
- 存儲方式
- Plist(NSArray\NSDictionary)
- Preference(偏好設定\NSUserDefaults)
- NSCoding(NSKeyedArchiver\NSkeyedUnarchiver)
- SQLite
- Core Data
- 在所有的存儲方式中,SQLite速度最快,效率最高.
SQLite的介紹
- 什麼是SQLite
- SQLite是一款輕型的嵌入式關系資料庫
- 它占用資源非常的低,在嵌入式裝置中,可能隻需要幾百K的記憶體就夠了
- 目前廣泛應用于移動裝置中存儲資料(Android/iOS)
- 處理資料的速度非常快,效率非常高
- 什麼是資料庫
- 資料庫(Database)是按照資料結構來組織、存儲和管理資料的倉庫(類似于excel表格)
- 資料庫可以分為2大種類(了解)
- 關系型資料庫(主流)
- 對象型資料庫
- 常見的關系資料庫
- PC端:Oracle、MySQL、SQL Server、Access、DB2
- 嵌入式\移動用戶端:SQLite
關系型資料庫
- 關系資料庫和Excel表格非常非常相似
- 一個Excel檔案可以了解成一個資料庫
- 在一個Excel檔案中可以有多張表
- 一張表中可以有多組資料
- 每組資料都有自己對應的屬性值
- 關系資料庫的特點
- 一個
存儲一個值,類似于對象的一個屬性字段(COL)
- 一
存儲一條記錄,類似于一個對象行(ROW)
- 一個
存儲一系列資料,類似于對象數組表(TABLE)
- 多個
之間存在一定表
,類似于對象之間的關系,例如:一條微網誌資料中包含使用者記錄關系
- 一個
SQLite的使用
- SQLite的使用步驟
- 建立一個資料庫->一個用于存儲資料的檔案
- 資料庫中建立表->一個表中用于記錄一系列資料
- 建立表時需要指定該表有哪些字段
- 比如學生表有:學号/姓名/年齡/身高等
- 在表中添加資料
- 之後對表進行增删改查操作
- 增:給表中添加資料
- 删:從表中删除資料
- 改:修改表中原有資料
- 查:查詢表中原有資料
- 學習SQLite
- 真正在使用SQLite時,一定是通過代碼來使用的
- 但是在真正開始代碼前可以先學習SQLite的基本操作
- 在Mac中如何學習呢?
- 可以通過指令行
- 可以通過圖形化界面工具(推薦)
- Navicat軟體的介紹
- Navicat是一款著名的資料庫管理軟體,支援大部分主流資料庫(包括SQLite)
- 收費軟體(4998RMB)
術語
- 字段(
):一個字段存儲一個值,可以存儲Field/Col
,INTEGER
,REAL
,TEXT
,BLOB
五種類型的資料NULL
- SQLite 在存儲時,本質上并不區分準确的資料類型
- 主鍵:
Primary Key
- 為什麼需要主鍵?
- 如果t_student表中就name和age兩個字段,而且有些記錄的name和age字段的值都一樣時,那麼就沒法區分這些資料,造成資料庫的記錄不唯一,這樣就不友善管理資料
- 良好的資料庫程式設計規範應該要保證每條記錄的唯一性,為此,增加了主鍵限制
- 也就是說,每張表都必須有一個主鍵,用來辨別記錄的唯一性
- 主鍵的設計原則
- 主鍵應當是對使用者沒有意義的
- 永遠也不要更新主鍵
- 主鍵應當由計算機自動更新增長
- 主鍵不能為空
- 為什麼需要主鍵?
一、常用 SQL操作
如何在程式中使用SQLite
- 真實使用SQLite是在程式中使用的
- 如何在程式運作過程中操作資料庫中的資料
- 那得先學會使用SQL語句
- 什麼是SQL
- SQL(structured query language):結構化查詢語言
- SQL是一種對資料庫中的資料進行
和定義
的語言操作
- SQL語言簡潔,文法簡單,好學好用
- 什麼是SQL語句
- 使用SQL語言編寫出來的句子\代碼,就是SQL語句
- 在程式運作過程中,要想操作(增删改查,CRUD)資料庫中的資料,必須使用SQL語句
- SQL語句的特點
- 不區分大小寫(比如資料庫認為user和UsEr是一樣的)
- 每條語句都必須以分号
結尾;
- SQL中的常用關鍵字
- select、insert、update、delete、from、create、where、desc、order、by、group、table、alter、view、index等等
- 資料庫中不可以使用關鍵字來命名表、字段
SQL語句的種類
- 資料定義語句(DDL:Data Definition Language)
- 包括create和drop等操作
- 在資料庫中建立新表或删除表(create table或 drop table)
- 資料操作語句(DML:Data Manipulation Language)
- 包括insert、update、delete等操作
- 上面的3種操作分别用于添加、修改、删除表中的資料
- 資料查詢語句(DQL:Data Query Language)
- 可以用于查詢獲得表中的資料
- 關鍵字select是DQL(也是所有SQL)用得最多的操作
- 其他DQL常用的關鍵字有where,order by,group by和having
二、DDL語句的演練
删除表
- 删除表的格式
- 語句說明
- DROP TABLE:删除表
- IF EXISTS:存在則删除
- '表名':要删除的表的名稱
建立表
- 建立表的格式
CREATE TABLE IF NOT EXISTS '表名' (
'字段名' 類型(INTEGER, REAL, TEXT, BLOB)
NOT NULL 不允許為空
PRIMARY KEY 主鍵
AUTOINCREMENT 自增長,
'字段名2' 類型,
...
)
- 具體用法
CREATE TABLE IF NOT EXISTS 't_student' (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT,
"age" INTEGER,
"height" REAL
)
- 語句說明
- CREATE TABLE:建立一張表
- IF NOT EXISTS:不存在則建立
- 't_student':表的名稱
- NOT NULL:不允許為空
- PRIMARY KEY:主鍵
- AUTOINCREMENT:自動增加
- 'id' INTEGER:有一個ID字段,類型是INTEGER
三、DML語句的演練
插入資料
- 插入資料的用法
- 語句說明
- INSERT INTO: 插入資料
- 't_student': 在哪一個表中插入資料
- (資料的字段): 給哪些字段插入資料
- VALUES ('why', 18, 1.88): 插入的具體值
更新資料
- 更新資料的寫法:
- 語句說明
- UPDATE: 跟新資料
- 't_student': 在哪一個表中更新資料
- SET 字段 = '值': 更新怎樣的資料
- WHERE 條件判斷: 更新哪些資料
- 具體用法
UPDATE t_student
SET name = 'MM'
WHERE age = ;
UPDATE t_student
SET name = 'WW'
WHERE age is ;
UPDATE t_student
SET name = 'XXOO'
WHERE age < ;
UPDATE t_student
SET name = 'NNMM'
WHERE age < and score > ;
/*更新記錄的name*/
UPDATE t_student SET name = 'zhangsan';
删除資料
- 删除資料的用法
DELETE FROM t_student;
DELETE FROM t_student WHERE age < ;
- 用法說明
- DELETE FROM: 從表中删除資料
- t_student : 表名
- 可以跟條件也可以不跟:不跟表示删除所有的資料
四、DQL語句的演練
DQL語句
- 查詢語句
查詢語句
1.基本查詢
SELECT * FROM t_student;
2.查詢某些字段
SELECT name, age FROM t_student;
3.通過條件判斷來查詢對應的資料(年齡大于等于25)
SELECT * FROM t_student WHERE age >= 25;
4.通過條件判斷來查詢對應的資料(名字以l開頭),使用like關鍵字
SELECT * FROM t_student WHERE name like '%l%';
5.計算個數
1>計算一共多少列
SELECT count(*) FROM t_student;
2>計算某一個列個數
SELECT count(age) FROM t_student;
6.排序
1> 升序 ASC (預設是升序)
SELECT * FROM t_student ORDER BY age;
2> 降序 DESC
SELECT * FROM t_student ORDER BY age DESC;
3> 按照年齡升序排序,如果年齡相同,按照名字的降序排列
7.起别名
1> 給列起别名(as可以省略)
SELECT name AS myName, age AS myAge FROM t_student;
2> 給表起别名
SELECT s.name, s.age FROM t_student as s;
8.limit
格式:SELECT * FROM t_student LIMIT 數字1,數字2;
1>數字1的意思是前面跳過多少條資料
2>數字2的意思是本次查詢多少條資料
SELECT * FROM t_student LIMIT 9, 3;
SELECT * FROM t_student LIMIT 5; 跳過0條資料,取5條資料
五、SQLite核心對象
核心對象 & 核心接口
核心對象
- database_connection
- 由
函數建立并傳回sqlite3_open
- 在使用其他 SQLite 接口函數之前,必須先獲得database_connnection對象
- 由
- prepared_statement
- 可以簡單的将它視為編譯後的SQL語句
核心接口
- 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.dylib
- 建立
SQLite-Bridge.h
-
架構是一套SQLite3
語言的架構,是以需要添加橋接檔案C
-
- 選擇
-項目
-TARGETS
,搜尋Build Settings
Bridg
- 在
中輸入Objective-C Bridging Header
項目名/SQLite-Bridge.h
- 如果之前設定過橋接檔案,可以直接使用
編譯測試
SQLiteManager
與網絡接口的獨立類似,資料庫的底層操作,也應該有一個獨立的對象單獨負責
SQLiteManager
單例
SQLiteManager
- 建立
,并且實作以下代碼:SQLiteManager.swift
class SQLiteManager {
private static let instance = SQLiteManager()
class func sharedSQLiteManager() -> SQLiteManager {
return instance
}
}
資料庫通路操作需求
- 建立資料庫 -> 有存儲資料的檔案
- 建立資料表 -> 每一張資料表存儲一類資料
- 利用
實作增/删/查/改,并在 UI 中顯示SQL 指令
建立&打開資料庫
/// 全局資料庫指針
private var db: COpaquePointer = nil
/// 打開資料庫
///
/// :param: dbname 資料庫檔案名
///
/// :returns: 是否成功
func openDB(dbname: String) -> Bool {
// 資料庫檔案的完整路徑
let filePath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last?.stringByAppendingPathComponent(dbname)
// 打開資料庫,如果資料庫不存在會建立
if sqlite3_open(filePath!.cStringUsingEncoding(NSUTF8StringEncoding)!, &db) != SQLITE_OK {
println("資料庫建立失敗")
return false
}
println("資料庫建立成功 \(filePath)")
return true
}
代碼小結
- 建立資料庫需要給定完整的資料庫路徑
- 資料庫路徑名是 C 語言的字元串,需要使用
轉換cStringUsingEncoding
-
函數會打開資料庫,如果資料庫不存在,會sqlite3_open
,并且傳回資料庫指針(句柄)建立一個空的資料庫
- 後續的所有資料庫操作,都針基于此
進行資料庫句柄
打開資料庫
- 在
中添加以下代碼AppDelegate
SQLiteManager.sharedSQLiteManager().openDB("my.db")
代碼小結
-
資料庫是直接儲存在沙盒中的一個檔案,隻有目前應用程式可以使用SQLite
- 在移動端開發時,資料庫通常是以
連接配接方式使用的持久式
- 所謂
指的是隻做一次持久式連接配接
的操作,永遠不做打開資料庫
資料庫的操作,進而可以提高資料庫的通路效率關閉
建立資料表
- 如果是第一次運作,打開資料庫之後,隻能得到一個空的資料,沒有任何的資料表
- 為了讓資料庫正常使用,在第一次打開資料庫後,需要執行
操作創表
注意:創表操作本質上是通過執行 SQL
語句實作的
- 執行
語句函數SQL
/// 執行 SQL
///
/// :param: sql SQL 語句
///
/// :returns: 是否成功
func execSQL(sql: String) -> Bool {
let cSQL = sql.cStringUsingEncoding(NSUTF8StringEncoding)!
/**
參數
1. 已經打開的資料庫句柄
2. 要執行(evaluated)的 SQL 語句
3. 執行完成後的回調,通常為 nil
4. 回調函數的第一個參數,通常為 nil
5. 錯誤,通常為 nil
*/
return sqlite3_exec(db, cSQL, nil, nil, nil) == SQLITE_OK
}
- 建立資料表
/// 建立資料表
private func createTables() -> Bool {
let sql = "CREATE TABLE IF NOT EXISTS T_Book ( \n" +
"'id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \n" +
"'name' TEXT NOT NULL, \n" +
"'price' REAL NOT NULL \n" +
")"
return execSQL(sql)
}
- 調整
函數openDB
println("資料庫建立成功 \(filePath)")
// 建立資料表
let result = createTables()
if !result {
db = nil
}
println("創表" + (result ? "成功": "失敗"))
return result
代碼小結
- 創表
可以從SQL
中粘貼,然後做一些處理Navicat
- 将
替換成"
'
- 在每一行後面增加一個
防止字元串拼接因為缺少空格造成\n
語句錯誤SQL
- 在
前添加表名
防止因為資料表存在出現錯誤IF NOT EXISTS
- 将
新增資料
建立 Book
模型
Book
class Book: NSObject {
var name: String
var price: Double
init(name: String, price: Double) {
self.name = name
self.price = price
}
/// 将目前對象資訊插入資料庫
///
/// :returns: 是否成功
func insertIntoDB() -> Bool {
let sql = "INSERT INTO T_Book (name, price) VALUES ('\(name)', \(price))"
return SQLiteManager.sharedSQLiteManager().execSQL(sql)
}
}
- 測試插入資料
/// 插入一本書
func insertOneBook() {
if Book(name: "iPhone", price: ).insertIntoDB() {
println("插入成功")
}
}
- 測試批量插入
/// 插入10000本書
func insertBooks() {
let start = CFAbsoluteTimeGetCurrent()
println("開始...")
for in ..< {
let n = "iPhone + \(arc4random_uniform())"
let p = + Double(arc4random_uniform())
Book(name: n, price: p).insertIntoDB()
}
println("完成 \(CFAbsoluteTimeGetCurrent() - start)")
}
八、查詢資料
準備 SQL 語句
/// 執行 SQL 獲得結果集合
///
/// :param: sql 加載資料
///
/// :returns: 結果集合
func execRecordSet(sql: String) -> [AnyObject]? {
let cSQL = sql.cStringUsingEncoding(NSUTF8StringEncoding)!
// 編譯後的 SQL 語句對象
var stmt: COpaquePointer = nil
/**
準備 SQL 語句
參數
1. 已經打開的資料庫句柄
2. 要執行的 SQL
3. 以位元組為機關的 SQL 最大長度,傳入 -1 會自動計算
4. SQL 語句位址
5. 未使用的指針位址,通常傳入 nil
*/
var recordSet: [AnyObject]?
if sqlite3_prepare_v2(db, cSQL, -1, &stmt, nil) == SQLITE_OK {
// 判斷是否讀取到記錄
while sqlite3_step(stmt) == SQLITE_ROW {
println("獲得記錄")
}
}
// 釋放語句對象
sqlite3_finalize(stmt)
return recordSet
}
代碼小結
- 這一部分的工作可以看作是對字元串的 SQL 語句進行編譯,并且檢查是否存在文法問題
- 編譯成功後通過
執行 SQL,每執行一次,擷取一條記錄sqlite3_step
- 通過
循環直至執行完畢while
- 注意,指令執行完畢後需要釋放
- 在
模型中增加測試函數Book
/// 加載書
class func loadBooks() {
let sql = "SELECT name, price FROM T_Book;"
SQLiteManager.sharedSQLiteManager().execRecordSet(sql)
}
擷取記錄行中的内容
- 周遊每一列的資訊
// 判斷是否讀取到記錄
while sqlite3_step(stmt) == SQLITE_ROW {
println("列數 \(sqlite3_column_count(stmt))")
// 列數
let colCount = sqlite3_column_count(stmt)
for i in ..<colCount {
let type = sqlite3_column_type(stmt, i)
let name = sqlite3_column_name(stmt, i)
print(" \(i) 列 \(type) \(name)")
switch type {
case SQLITE_INTEGER:
let i = sqlite3_column_int64(stmt, i)
println("int \(i)")
case SQLITE_FLOAT:
let d = sqlite3_column_double(stmt, i)
println("double \(d)")
case SQLITE_BLOB:
let b = sqlite3_column_blob(stmt, i)
println("blob \(b)")
case SQLITE_NULL:
println(NSNull())
case SQLITE3_TEXT:
let t = sqlite3_column_text(stmt, i)
println("text \(t)")
default:
println("不支援的類型 \(type)")
}
}
}
- 擷取字段名稱
let cname = sqlite3_column_name(stmt, i)
let colName = String(CString: cname, encoding: NSUTF8StringEncoding)
- 定義記錄字典
// 定義記錄字典
var record = [String: AnyObject]()
- 将解析内容添加到字典
for i in ..<colCount {
// 字段名稱
let cname = sqlite3_column_name(stmt, i)
let colName = String(CString: cname, encoding: NSUTF8StringEncoding)!
let type = sqlite3_column_type(stmt, i)
print(" \(i) 列 \(type) \(colName)")
switch type {
case SQLITE_INTEGER:
record[colName] = Int(sqlite3_column_int64(stmt, i))
case SQLITE_FLOAT:
record[colName] = sqlite3_column_double(stmt, i)
case SQLITE_NULL:
println(NSNull())
record[colName] = NSNull()
case SQLITE3_TEXT:
let t = UnsafePointer<Int8>(sqlite3_column_text(stmt, i))
record[colName] = String(CString: t, encoding: NSUTF8StringEncoding)
default:
println("不支援的類型 \(type)")
}
}
- 抽取函數
/// 從 stmt 中擷取記錄字典
///
/// :param: stmt stmt
///
/// :returns: 字典資訊
private func recordDict(stmt: COpaquePointer) -> [String: AnyObject]? {
// 列數
let colCount = sqlite3_column_count(stmt)
if colCount <= 0 {
return nil
}
// 定義記錄字典
var record = [String: AnyObject]()
for i in 0..<colCount {
// 字段名稱
let cname = sqlite3_column_name(stmt, i)
let colName = String(CString: cname, encoding: NSUTF8StringEncoding)!
let type = sqlite3_column_type(stmt, i)
print(" \(i) 列 \(type) \(colName)")
switch type {
case SQLITE_INTEGER:
record[colName] = Int(sqlite3_column_int64(stmt, i))
case SQLITE_FLOAT:
record[colName] = sqlite3_column_double(stmt, i)
case SQLITE_NULL:
println(NSNull())
record[colName] = NSNull()
case SQLITE3_TEXT:
let t = UnsafePointer<Int8>(sqlite3_column_text(stmt, i))
record[colName] = String(CString: t, encoding: NSUTF8StringEncoding)
default:
println("不支援的類型 \(type)")
}
}
return record
}
- 提取結果集函數
/// 執行 SQL 獲得結果集合
///
/// :param: sql 加載資料
///
/// :returns: 結果集合
func execRecordSet(sql: String) -> [[String: AnyObject]]? {
let cSQL = sql.cStringUsingEncoding(NSUTF8StringEncoding)!
// 編譯後的 SQL 語句對象
var stmt: COpaquePointer = nil
/**
準備 SQL 語句
參數
1. 已經打開的資料庫句柄
2. 要執行的 SQL
3. 以位元組為機關的 SQL 最大長度,傳入 -1 會自動計算
4. SQL 語句位址
5. 未使用的指針位址,通常傳入 nil
*/
var recordList: [[String: AnyObject]]?
if sqlite3_prepare_v2(db, cSQL, -1, &stmt, nil) == SQLITE_OK {
// 執行個體化數組
recordList = [[String: AnyObject]]()
// 判斷是否讀取到記錄
while sqlite3_step(stmt) == SQLITE_ROW {
if let record = recordDict(stmt) {
recordList!.append(record)
}
}
}
// 釋放語句對象
sqlite3_finalize(stmt)
return recordList
}
- 修改加載書函數
/// 加載書
class func loadBooks() -> [Book]? {
let sql = "SELECT name, price FROM T_Book;"
if let list = SQLiteManager.sharedSQLiteManager().execRecordSet(sql) {
var books = [Book]()
for record in list {
let n = record["name"] as! String
let p = record["price"] as! Double
books.append(Book(name: n, price: p))
}
return books
}
return nil
}
九、批量插入
在 SQLite 中如果要批量插入資料,通常需要引入 事務的概念
事務
- 在準備做
,首先開啟一個事務,儲存操作前的資料庫的狀态大規模資料操作前
- 開始資料操作
- 如果資料操作成功,
事務,讓資料庫更新到資料操作後的狀态送出
- 如果資料操作失敗,
事務,讓資料庫還原到操作前的狀态復原
- 事務處理函數
/// 開啟事務
///
/// :returns: 是否成功
func beginTransaction() -> Bool {
return sqlite3_exec(db, "BEGIN TRANSACTION;", nil, nil, nil) == SQLITE_OK
}
/// 送出事務
///
/// :returns: 是否成功
func commitTransaction() -> Bool {
let result = sqlite3_exec(db, "COMMIT TRANSACTION;", nil, nil, nil) == SQLITE_OK
if !result {
let errmsg = String.fromCString(sqlite3_errmsg(db))
println("送出錯誤 \(errmsg)")
}
return result
}
/// 復原事務
///
/// :returns: 是否成功
func rollbackTransaction() -> Bool {
return sqlite3_exec(db, "ROLLBACK TRANSACTION;", nil, nil, nil) == SQLITE_OK
}