SQLite
iOS 系統支援很常見的資料庫 SQLite ,這是一個輕量的關聯式資料庫管理系統( Relational Database Management System ,縮寫為 RDBMS ),所有的資料內容其實就是一個檔案,而且絕大部分的 SQL 指令都可以使用。
以下會先介紹在應用程式中如何加入 SQLite ,接著會介紹如何新增、讀取、更新與刪除資料,最後會將 SQLite 功能獨立寫在一個類別中,以便日後其他應用程式需要時可以重複使用。
本節內容已假設你已經會使用簡單的 SQL 指令,所以不會詳細介紹如何使用,如果尚未熟悉,請先了解相關內容再繼續本節的範例。
加入 SQLite
其實 Swift 本身是無法直接使用 SQLite 的功能,必須要利用 Objective-C (在 Swift 之前用來編寫 iOS 應用程式的程式語言)來與 SQLite 連結。
首先在 Xcode 裡,新建一個 Single View Application 類型的專案,取名為 ExSQLite 。
以下會介紹如何加入的步驟:
加入 sqlite 函式庫
▼ 預設的應用程式沒有引入 sqlite 函式庫,所以需要手動加入,先找到TARGETS > ExSQLite > General > Linked Frameworks and Libraries
,並按下加號+
,如下圖:
▼ 這邊會列出來所有可以加入的函式庫,所以填入sqlite
來過濾,最後會出現兩個類似的函式庫,實際上兩個指的是一樣的東西,所以選一個加入就好,這邊選取libsqlite3.tbd
,按下Add
加入:
▼ 如果有順利加入的話, sqlite 函式庫就會出現在列表中,如下圖:
新增 header 檔案
▼ 前面提到需要利用 Objective-C 來與 SQLite (也就是前一步驟加入的 sqlite 函式庫)連結,所以接著必須為應用程式加上一個 header 檔案以連結。先以新增檔案的方式加入,但請注意選擇檔案類型時,要選擇iOS > Source > Header File
,如下圖:
▼ 接著將檔案命名為 BridgeHeader.h ( .h 就是一個 header 檔案),請記得要勾選 Targets ,如下圖:
建立好 header 檔案後,請點開這隻檔案,並填入一行程式,如下:
#include "sqlite3.h"
這隻 header 檔案就是用來引入 sqlite 函式庫,所以填寫這一行程式即可。
與 Objective-C 連結
▼ 最後要將 Swift 與前一步驟設置好的 header 檔案連結起來,首先找到TARGETS > ExSQLite > Build Settings > Objective-C Bridging Header
,這邊有很多東西可以設定,所以你可以在右上角的搜尋框輸入bridg
來過濾,如下圖:
▼ 接著對下圖標示1
的地方點兩下,會彈出一個輸入框,填入 header 檔案的路徑與名稱(請記得路徑也要填):
▼ 如果順利加入的話,會顯示如下圖:
這樣便完成了加入 SQLite 的步驟,你可以先試著 Build (cmd + b
)專案,看看有沒有錯誤,如果有的話請檢查前面步驟有沒有做錯,如果沒有錯誤的話就是順利加入 SQLite 了。
使用 SQLite
接著進入到程式碼的部份,首先會需要宣告一個變數來儲存 SQLite 的連線資訊,型別為COpaquePointer
,後續的資料庫操作都會需要這個變數:
var db :COpaquePointer = nil
資料庫檔案路徑
前面有提過 SQLite 其實操作的是一個檔案,所以要先取得這個資料庫檔案的路徑:
// 資料庫檔案的路徑
let urls = NSFileManager.defaultManager()
.URLsForDirectory(
.DocumentDirectory,
inDomains: .UserDomainMask)
let sqlitePath = urls[urls.count-1].absoluteString
+ "sqlite3.db"
上述程式會取得應用程式的 Documents 目錄,這是開放給開發者儲存檔案的路徑,有任何需要儲存的檔案都是放在這裡,而sqlite3.db
則是這個資料庫檔案名稱,你也可以命名為db.sqlite
之類,其他可供辨識的檔案名稱。
如果沒有這個檔案,系統會自動嘗試建立起來。
開啟資料庫連線
接著要與資料庫連線,會用到前面兩個步驟宣告的變數:
if sqlite3_open(sqlitePath, &db) == SQLITE_OK {
print("資料庫連線成功")
} else {
print("資料庫連線失敗")
}
使用sqlite3_open()
函式來連線,請注意第二個參數前面必須加上&
,這是一個指標的概念(與前面章節提過的輸入輸出參數 In-Out Parameters 類似),函式內使用的就是傳入參數db
本身,所以稍後操作資料庫時可以直接使用這個db
變數。
建立資料表
建立一個名為 students 的資料表,欄位分別為 id, name, height ,欄位類型依序為 integer, text, double :
let sql = "create table if not exists students "
+ "( id integer primary key autoincrement, "
+ "name text, height double)" as NSString
if sqlite3_exec(db, sql.UTF8String, nil, nil, nil)
== SQLITE_OK{
print("建立資料表成功")
}
使用sqlite3_exec()
函式來建立資料表,第一個參數就是前面建立資料庫連線後的db
,第二個參數就是 SQL 指令,這邊會先轉成NSString
型別,再將文字編碼轉成 UTF8。
如果返回為SQLITE_OK
,則表示建立成功。
Hint
- 如果要查看模擬器的 SQLite 檔案,可以使用桌機的 Firefox 瀏覽器的一個套件 SQLite Manager ,它可以讓你檢視與編輯本機上的 SQLite 檔案。實際檔案路徑可藉由將前面提過的
sqlitePath
印出來取得。
新增資料
這邊需要另一個型別為COpaquePointer
的變數statement
,用來取得操作資料庫後回傳的資訊:
var statement :COpaquePointer = nil
let sql = "insert into students "
+ "(name, height) "
+ "values ('小強', 178.2)" as NSString
if sqlite3_prepare_v2(
db, sql.UTF8String, -1, &statement, nil) == SQLITE_OK{
if sqlite3_step(statement) == SQLITE_DONE {
print("新增資料成功")
}
sqlite3_finalize(statement)
}
新增資料是使用sqlite3_prepare_v2()
函式,前面兩個參數與sqlite3_exec()
使用的相同,第三個參數則是設定資料庫可以讀取的最大資料量,單位是位元組( Byte ),設為-1
表示不限制讀取量,第四個參數是用來取得操作返回的資訊,記得參數前面要加上&
。
statement
要再當做sqlite3_step()
函式的參數傳入,如果返回SQLITE_DONE
,則是表示新增成功。
最後要使用sqlite3_finalize()
函式來釋放掉statement
,以免發生記憶體洩漏的問題。
讀取資料
讀取資料方式如下:
var statement :COpaquePointer = nil
var sql = "select * from students"
sqlite3_prepare_v2(
db, (sql as NSString).UTF8String, -1, &statement, nil)
while sqlite3_step(statement) == SQLITE_ROW{
let id = sqlite3_column_int(statement, 0)
let name = String.fromCString(
UnsafePointer<CChar>(sqlite3_column_text(statement, 1)))
let height = sqlite3_column_double(statement, 2)
print("\(id). \(name!) 身高: \(height)")
}
sqlite3_finalize(statement)
讀取資料以及後續的更新、刪除資料都與新增資料使用一樣的函式sqlite3_prepare_v2()
,參數都是一樣的意思,也就不再複述,請看前面新增資料的說明。
回傳的資料存在變數statement
中,以while
迴圈來一筆一筆取出,當等於SQLITE_ROW
時就是有資料,會一直取到不為SQLITE_ROW
,就會結束迴圈。(如果只有一筆資料的話,也可以使用if
條件句即可。)
迴圈中每筆資料的每個欄位則是使用sqlite3_column_資料類型()
函式來取出,像是 int 的欄位就是使用sqlite3_column_int()
, text 的欄位就是使用sqlite3_column_text()
, double 的欄位就是使用sqlite3_column_double()
,以此類推。
取出欄位的函式有兩個參數,第一個都固定是返回資訊statement
,第二個則是這個欄位的索引值,範例有三個欄位: id, name, height ,則索引值從 0 開始算起,依序為 0, 1, 2 。
更新資料
更新資料方式如下:
var statement :COpaquePointer = nil
var sql = "update students set name='小強' where id = 1"
if sqlite3_prepare_v2(
db, (sql as NSString).UTF8String, -1, &statement, nil)
== SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
print("更新資料成功")
}
sqlite3_finalize(statement)
}
刪除資料
刪除資料方式如下:
var statement :COpaquePointer = nil
var sql = "delete from students where id = 3"
if sqlite3_prepare_v2(
db, (sql as NSString).UTF8String, -1, &statement, nil)
== SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
print("刪除資料成功")
}
sqlite3_finalize(statement)
}
以上便為基本操作資料庫的方式。
將 SQLite 功能獨立出來
這一小節會將前面講過的資料庫基本功能獨立出來寫成一個類別,以便日後其他應用程式需要時可以重複使用。
首先以新增檔案的方式加入一個.swift
檔案,命名為SQLiteConnect.swift
,記得檔案類型要選擇Swift File
:
iOS > Source > Swift File
接著打開這隻檔案,建立一個類別,以及其內的方法:
class SQLiteConnect {
var db :COpaquePointer = nil
let sqlitePath :String
init?(path :String) {
sqlitePath = path
db = self.openDatabase(sqlitePath)
if db == nil {
return nil
}
}
// 連結資料庫 connect database
func openDatabase(path :String) -> COpaquePointer {
var connectdb: COpaquePointer = nil
if sqlite3_open(path, &connectdb) == SQLITE_OK {
print("Successfully opened database \(path)")
return connectdb
} else {
print("Unable to open database.")
return nil
}
}
// 建立資料表 create table
func createTable(
tableName :String, columnsInfo :[String]) -> Bool {
let sql = "create table if not exists \(tableName) "
+ "(\(columnsInfo.joinWithSeparator(",")))"
as NSString
if sqlite3_exec(
self.db, sql.UTF8String, nil, nil, nil) == SQLITE_OK{
return true
}
return false
}
// 新增資料
func insert(
tableName :String, rowInfo :[String:String]) -> Bool {
var statement :COpaquePointer = nil
let sql = "insert into \(tableName) "
+ "(\(rowInfo.keys.joinWithSeparator(","))) "
+ "values "
+"(\(rowInfo.values.joinWithSeparator(",")))"
as NSString
if sqlite3_prepare_v2(
self.db, sql.UTF8String, -1, &statement, nil)
== SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
return true
}
sqlite3_finalize(statement)
}
return false
}
// 讀取資料
func fetch(
tableName :String, cond :String?, order :String?)
-> COpaquePointer {
var statement :COpaquePointer = nil
var sql = "select * from \(tableName)"
if let condition = cond {
sql += " where \(condition)"
}
if let orderBy = order {
sql += " order by \(orderBy)"
}
sqlite3_prepare_v2(
self.db, (sql as NSString).UTF8String, -1,
&statement, nil)
return statement
}
// 更新資料
func update(
tableName :String,
cond :String?, rowInfo :[String:String]) -> Bool {
var statement :COpaquePointer = nil
var sql = "update \(tableName) set "
// row info
var info :[String] = []
for (k, v) in rowInfo {
info.append("\(k) = \(v)")
}
sql += info.joinWithSeparator(",")
// condition
if let condition = cond {
sql += " where \(condition)"
}
if sqlite3_prepare_v2(
self.db, (sql as NSString).UTF8String, -1,
&statement, nil) == SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
return true
}
sqlite3_finalize(statement)
}
return false
}
// 刪除資料
func delete(tableName :String, cond :String?) -> Bool {
var statement :COpaquePointer = nil
var sql = "delete from \(tableName)"
// condition
if let condition = cond {
sql += " where \(condition)"
}
if sqlite3_prepare_v2(
self.db, (sql as NSString).UTF8String, -1,
&statement, nil) == SQLITE_OK {
if sqlite3_step(statement) == SQLITE_DONE {
return true
}
sqlite3_finalize(statement)
}
return false
}
}
上述程式可以看到,這邊為類別建立一個新的建構器( initializer ),而且是可失敗建構器( failable initializer ),是為了確保有正確連結上資料庫。
接著就可以使用這個類別來操作資料庫,將前一小節的基本操作內容改成如下,在viewDidload()
裡:
// 資料庫檔案的路徑
let sqlitePath = NSHomeDirectory() + "/Documents/sqlite3.db"
// 印出儲存檔案的位置
print(sqlitePath)
// SQLite 資料庫
db = SQLiteConnect(path: sqlitePath)
if let mydb = db {
// create table
mydb.createTable("students", columnsInfo: [
"id integer primary key autoincrement",
"name text",
"height double"])
// insert
mydb.insert(
"students", rowInfo:
["name":"'大強'","height":"178.2"])
// select
let statement = mydb.fetch(
"students", cond: "1 == 1", order: nil)
while sqlite3_step(statement) == SQLITE_ROW{
let id = sqlite3_column_int(statement, 0)
let name =
String.fromCString(UnsafePointer<CChar>(
sqlite3_column_text(statement, 1)))
let height = sqlite3_column_double(statement, 2)
print("\(id). \(name!) 身高: \(height)")
}
sqlite3_finalize(statement)
// update
mydb.update(
"students",
cond: "id = 1",
rowInfo: ["name":"'小強'","height":"176.8"])
// delete
mydb.delete("students", cond: "id = 5")
}
以上便為本節範例的內容。
範例
本節範例程式碼放在 database/sqlite