SQLite

iOS 支援很常見的資料庫 SQLite ,這是一個輕量的關聯式資料庫管理系統( Relational Database Management System ,縮寫為 RDBMS ),所有的資料內容其實就是一個檔案,而且絕大部分的 SQL 指令都可以使用。

以下會先介紹在應用程式中如何使用 SQLite 來新增、讀取、更新與刪除資料,最後會將 SQLite 功能獨立寫在一個類別中,以便日後其他應用程式需要時可以重複使用。

本節內容已假設你已經會使用基礎的 SQL 指令,所以不會詳細介紹如何使用 SQL 語法,如果尚未熟悉,請先了解相關內容再繼續本節的範例。

使用 SQLite

首先在 Xcode 裡,新建一個 Single View Application 類型的專案,取名為 ExSQLite 。

一開始必須先在 ViewController 中引入 SQLite 函式庫:

import SQLite3

接著宣告一個變數來儲存 SQLite 的連線資訊,型別為OpaquePointer?,後續的資料庫操作都會需要這個變數:

var db :OpaquePointer? = nil

資料庫檔案路徑

前面有提過 SQLite 其實操作的是一個檔案,所以要先取得這個資料庫檔案的本機儲存位置:

let sqliteURL: URL = {
    do {
            return try FileManager.default.url(
                for: .documentDirectory, 
                in: .userDomainMask, 
                appropriateFor: nil, 
                create: true).appendingPathComponent("db.sqlite")
    } catch {
        fatalError("Error getting file URL from document directory.")
    }
}()

db.sqlite是這個資料庫檔案名稱,沒有特別的規定,你也可以命名成其他可供辨識的檔案名稱。

如果沒有這個檔案,系統會自動嘗試建立起來。

開啟資料庫連線

接著要與資料庫連線,會用到前面兩個步驟宣告的變數:

if sqlite3_open(sqliteURL.path, &db) == SQLITE_OK {
    print("資料庫連線成功")
} else {
    print("資料庫連線失敗")
}

使用sqlite3_open()函式來連線,請注意第二個參數前面必須加上&,這是一個指標的概念(與前面章節提過的輸入輸出參數 In-Out Parameters 類似),函式內使用的就是傳入參數db本身,所以稍後操作資料庫時可以直接使用這個db變數。

建立資料表

建立一個名為 students 的資料表,欄位分別為 id, name, height ,欄位類型依序為 integer, text, double :

var sql = """
create table if not exists students
( id integer primary key autoincrement,
name text, height double)
"""

if sqlite3_exec(
  db, sql.cString(using: String.Encoding.utf8), nil, nil, nil) 
  == SQLITE_OK{
    print("建立資料表成功")
}

使用sqlite3_exec()函式來建立資料表,第一個參數就是前面建立資料庫連線後的db,第二個參數就是 SQL 指令,這邊會將指令字串sql轉成需要的編碼格式。

如果返回為SQLITE_OK,則表示建立成功。

新增資料

這邊需要另一個型別為OpaquePointer?的變數statement,用來取得操作資料庫後回傳的資訊:

var statement :OpaquePointer? = nil
var sql = """
insert into students
(name, height)
values ('小強', 178.2)
"""

if sqlite3_prepare_v2(
  db, sql.cString(using: String.Encoding.utf8), -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 :OpaquePointer? = nil
var sql = "select * from students"
sqlite3_prepare_v2(
  db, sql.cString(using: String.Encoding.utf8), -1, &statement, nil)

while sqlite3_step(statement) == SQLITE_ROW{
    let id = sqlite3_column_int(statement, 0)
    let name = String(cString: 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 :OpaquePointer? = nil
var sql = "update students set name='小強' where id = 1"

if sqlite3_prepare_v2(
  db, sql.cString(using: String.Encoding.utf8), -1, &statement, nil)
  == SQLITE_OK {
    if sqlite3_step(statement) == SQLITE_DONE {
        print("更新資料成功")
    }
    sqlite3_finalize(statement)
}

刪除資料

刪除資料方式如下:

var statement :OpaquePointer? = nil
var sql = "delete from students where id = 3"

if sqlite3_prepare_v2(
  db, sql.cString(using: String.Encoding.utf8), -1, &statement, nil) 
  == SQLITE_OK {
    if sqlite3_step(statement) == SQLITE_DONE {
        print("刪除資料成功")
    }
    sqlite3_finalize(statement)
}

建立資料庫時使用的sqlite3_exec()函式,其實就是後續新增、讀取、更新及刪除使用的sqlite3_prepare_v2()sqlite3_step()sqlite3_finalize()這三個函式封裝起來的功能,所以前後這兩種方式是差不多的,都可以執行 SQL 語法。

以上便為基本操作資料庫的方式。

將 SQLite 功能獨立出來

這一小節會將前面講過的資料庫基本功能獨立出來寫成一個類別,以便日後其他應用程式需要時可以重複使用。

首先以新增檔案的方式加入一個.swift檔案,命名為SQLiteConnect.swift,記得檔案類型要選擇Swift File

iOS > Source > Swift File

接著打開這隻檔案,建立一個類別,以及其內的方法:

import SQLite3

class SQLiteConnect {

    var db :OpaquePointer? = nil
    let sqlitePath :String

    init?(path :String) {
        sqlitePath = path
        db = self.openDatabase(sqlitePath)

        if db == nil {
            return nil
        }
    }

    // 連結資料庫 connect database
    func openDatabase(_ path :String) -> OpaquePointer? {
        var connectdb: OpaquePointer? = 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.joined(separator: ",")))"

        if sqlite3_exec(
            self.db, sql.cString(using: String.Encoding.utf8), nil, nil, nil) 
            == SQLITE_OK{
            return true
        }

        return false
    }

    // 新增資料
    func insert(_ tableName :String, rowInfo :[String:String]) -> Bool {
        var statement :OpaquePointer? = nil
        let sql = "insert into \(tableName) "
                + "(\(rowInfo.keys.joined(separator: ","))) "
                + "values (\(rowInfo.values.joined(separator: ",")))"

        if sqlite3_prepare_v2(
            self.db, sql.cString(using: String.Encoding.utf8), -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?) 
        -> OpaquePointer {
        var statement :OpaquePointer? = 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.cString(using: String.Encoding.utf8), -1, &statement, nil)

        return statement!
    }

    // 更新資料
    func update(
        _ tableName :String, cond :String?, rowInfo :[String:String]) -> Bool {
        var statement :OpaquePointer? = nil
        var sql = "update \(tableName) set "

        // row info
        var info :[String] = []
        for (k, v) in rowInfo {
            info.append("\(k) = \(v)")
        }
        sql += info.joined(separator: ",")

        // condition
        if let condition = cond {
            sql += " where \(condition)"
        }

        if sqlite3_prepare_v2(
            self.db, sql.cString(using: String.Encoding.utf8), -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 :OpaquePointer? = nil
        var sql = "delete from \(tableName)"

        // condition
        if let condition = cond {
            sql += " where \(condition)"
        }

        if sqlite3_prepare_v2(
            self.db, sql.cString(using: String.Encoding.utf8), -1, &statement, nil) 
            == SQLITE_OK {
            if sqlite3_step(statement) == SQLITE_DONE {
                return true
            }
            sqlite3_finalize(statement)
        }

        return false
    }

}

上述程式可以看到,這邊為類別建立一個新的建構器( initializer ),而且是可失敗建構器( failable initializer ),是為了確保有正確連結上資料庫。

接著就可以使用這個類別來操作資料庫,將前一小節的基本操作內容改成如下,在ViewControllerviewDidload()裡:

// 資料庫檔案的路徑
let sqlitePath = sqliteURL.path

// 印出儲存檔案的位置
print(sqlitePath)

// SQLite 資料庫
db = SQLiteConnect(path: sqlitePath)

if let mydb = db {

    // create table
    let _ = mydb.createTable("students", columnsInfo: [
        "id integer primary key autoincrement",
        "name text",
        "height double"])

    // insert
    let _ = 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(cString: sqlite3_column_text(statement, 1))
        let height = sqlite3_column_double(statement, 2)
        print("\(id). \(name) 身高: \(height)")
    }
    sqlite3_finalize(statement)

    // update
    let _ = mydb.update(
        "students", 
        cond: "id = 1", 
        rowInfo: ["name":"'小強'","height":"176.8"])

    // delete
    let _ = mydb.delete("students", cond: "id = 5")

}

以上便為本節範例的內容。

除了這節介紹的操作內容,你也可以使用第三方套件像是 FMDB 或是 SQLite.swift 來處理 SQLite 相關的程式,這些套件會將複雜的資料庫操作封裝起來,讓你可以更為快速與簡單的上手 SQLite 。

範例

本節範例程式碼放在 database/sqlite

results matching ""

    No results matching ""