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,並按下加號+,如下圖:

sqlite01

▼ 這邊會列出來所有可以加入的函式庫,所以填入sqlite來過濾,最後會出現兩個類似的函式庫,實際上兩個指的是一樣的東西,所以選一個加入就好,這邊選取libsqlite3.tbd,按下Add加入:

sqlite02

▼ 如果有順利加入的話, sqlite 函式庫就會出現在列表中,如下圖:

sqlite03

新增 header 檔案

▼ 前面提到需要利用 Objective-C 來與 SQLite (也就是前一步驟加入的 sqlite 函式庫)連結,所以接著必須為應用程式加上一個 header 檔案以連結。先以新增檔案的方式加入,但請注意選擇檔案類型時,要選擇iOS > Source > Header File,如下圖:

sqlite04

▼ 接著將檔案命名為 BridgeHeader.h ( .h 就是一個 header 檔案),請記得要勾選 Targets ,如下圖:

sqlite05

建立好 header 檔案後,請點開這隻檔案,並填入一行程式,如下:

#include "sqlite3.h"

這隻 header 檔案就是用來引入 sqlite 函式庫,所以填寫這一行程式即可。

與 Objective-C 連結

▼ 最後要將 Swift 與前一步驟設置好的 header 檔案連結起來,首先找到TARGETS > ExSQLite > Build Settings > Objective-C Bridging Header,這邊有很多東西可以設定,所以你可以在右上角的搜尋框輸入bridg來過濾,如下圖:

sqlite06

▼ 接著對下圖標示1的地方點兩下,會彈出一個輸入框,填入 header 檔案的路徑與名稱(請記得路徑也要填):

sqlite07

▼ 如果順利加入的話,會顯示如下圖:

sqlite08

這樣便完成了加入 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

results matching ""

    No results matching ""