數據持久化方案解析(一) —— 一個簡單的基于SQLite持久化方案示例(一)

版本記錄

版本號 時間
V1.0 2018.10.12 星期五

前言

數據的持久化存儲是移動端不可避免的一個問題,很多時候的業務邏輯都需要我們進行本地化存儲解決和完成,我們可以采用很多持久化存儲方案,比如說plist文件(屬性列表)、preference(偏好設置)、NSKeyedArchiver(歸檔)、SQLite 3CoreData,這里基本上我們都用過。這幾種方案各有優缺點,其中,CoreData是蘋果極力推薦我們使用的一種方式,我已經將它分離出去一個專題進行說明講解。這個專題主要就是針對另外幾種數據持久化存儲方案而設立。

開始

首先看一下寫作環境

Swift 4, iOS 11, Xcode 9

本文將學習如何在Swift項目中使用SQLite數據庫,包括插入,更新和刪除行。

這個帶有Swift的SQLite文章向您展示了如何在Swift中使用流行的數據庫平臺。 在軟件開發領域,您需要很長時間才能保留應用數據。 在許多情況下,這是以數據結構的形式出現的。 但是,如何有效地存儲它?

幸運的是,一些偉大的思想家已經開發出用于在數據庫中存儲結構化數據和編寫語言功能以訪問數據的解決方案。 SQLite默認在iOS上可用。 實際上,如果您以前使用過Core Data,那么您實際上已經使用過SQLite,因為Core Data只是SQLite上面的一個層,它提供了更方便的API。

在整個文章中,您將學習如何執行以下數據庫操作:

  • 創建并連接到數據庫
  • 創建一個表
  • 插入一行
  • 更新一行
  • 刪除一行
  • 查詢數據庫
  • 處理SQLite錯誤

在學習如何執行這些基本操作之后,您將看到如何以類似Swift的方式將它們包裝起來。 這將允許您為應用程序編寫抽象API,以便您(大多數)可以避免使用SQLite C API的痛苦!

最后,我將簡要介紹流行的開源Swift包裝器SQLite.swift,以便您基本了解底層框架如何在包裝器中工作。

注意:數據庫,甚至只是SQLite本身,都是要涵蓋的大量主題,因此它們大多超出了本教程的范圍。 假設您對關系數據庫意識形態有基本的了解,并且您主要在這里學習如何結合使用SQLite和Swift。

打開已經編制好的SQLite的入門項目并打開SQLiteTutorial.xcworkspace。 從Project Navigator中打開Tutorial playground

注意:項目打包在Xcode工作區中,因為它使用SQLite3依賴項作為嵌入式二進制文件。 此二進制文件包含您將在本教程中編寫的SQLite代碼的所有功能。

請注意,您的Playground配置為手動而不是自動運行:

這意味著它只會在您通過點擊Play按鈕顯式調用執行時執行。

您可能還會在頁面頂部看到destroyPart1Database()調用;您可以放心地忽略這一點,因為每次playground運行時都會銷毀數據庫文件。 這可確保在使用Swift教程瀏覽此SQLite時,所有語句都能成功執行。

你的playground需要在你的文件系統上編寫SQLite數據庫文件。 在終端中運行以下命令以創建playground的數據目錄:

mkdir -p ~/Documents/Shared\ Playground\ Data/SQLiteTutorial

Why Should I Choose SQLite? - 我為什么要選擇SQLite?

沒錯,SQLite不是在iOS上持久保存數據的唯一方法。 除了Core Data之外,還有許多其他的數據持久性替代方案,包括RealmCouchbase LiteFirebaseNSCoding

每個都有自己的優點和缺點 - 包括SQLite本身。 數據持久性沒有靈丹妙藥,作為開發人員,您可以根據應用程序的要求確定哪個選項超過其他選項。

SQLite確實有一些優點:

  • 隨iOS一起提供,因此它不會為您的應用程序包增加任何開銷
  • 試過并經過測試;1.0版于2000年8月發布
  • 開源
  • 適用于數據庫開發人員和管理員熟悉的查詢語言
  • 跨平臺

SQLite的缺點可能是非常主觀的,就把研究留給你了!


The C API - C API

SQLite with Swift教程的這部分將引導您完成最常見和最基本的SQLite API。 你很快就會意識到在Swift方法中包裝C API是理想的,但要緊緊抓住并首先完成C代碼;你將在本教程的第二部分做一些包裝。

1. Opening a Connection - 打開連接

在做任何事情之前,您首先需要創建一個數據庫連接。

playground開始部分下添加以下方法:

func openDatabase() -> OpaquePointer? {
  var db: OpaquePointer? = nil
  if sqlite3_open(part1DbPath, &db) == SQLITE_OK {
    print("Successfully opened connection to database at \(part1DbPath)")
    return db
  } else {
    print("Unable to open database. Verify that you created the directory described " +
      "in the Getting Started section.")
    PlaygroundPage.current.finishExecution()
  }
  
}

上面的方法調用sqlite3_open(),它打開或創建一個新的數據庫文件。 如果成功,則返回OpaquePointer;這是一個用于C指針的Swift類型,無法直接在Swift中表示。 調用此方法時,您必須捕獲返回的指針才能與數據庫進行交互。

許多SQLite函數返回Int32結果代碼。 這些代碼中的大多數都被定義為SQLite庫中的常量。 例如,SQLITE_OK表示結果代碼0。可以在on the main SQLite site上找到不同結果代碼的列表。

要打開數據庫,請將以下行添加到您的playground

let db = openDatabase()

Play按鈕運行playground并觀看控制臺輸出。 如果控制臺未打開,請按play按鈕左側的按鈕:

如果openDatabase()成功,您將看到如下輸出:

Successfully opened connection to database at /Users/username/Documents/Shared Playground Data/SQLiteTutorial/Part1.sqlite

其中username是您的Home目錄。

2. Creating a Table - 創建表

現在您已連接到數據庫文件,您可以創建一個表。 您將使用一個非常簡單的表來存儲聯系人。

該表將包含兩列; Id,是INTPRIMARY KEY;和Name,這是一個CHAR(255)

添加以下字符串,其中包含創建表所需的SQL語句:

let createTableString = """
CREATE TABLE Contact(
Id INT PRIMARY KEY NOT NULL,
Name CHAR(255));
"""

請注意,您正在使用Swift 4的便捷多語法來編寫此語句!

接下來,添加執行CREATE TABLE SQL語句的此方法:

func createTable() {
  // 1
  var createTableStatement: OpaquePointer? = nil
  // 2
  if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
    // 3
    if sqlite3_step(createTableStatement) == SQLITE_DONE {
      print("Contact table created.")
    } else {
      print("Contact table could not be created.")
    }
  } else {
    print("CREATE TABLE statement could not be prepared.")
  }
  // 4
  sqlite3_finalize(createTableStatement)
}

逐步完成這一步:

  • 1) 首先,在下一步中創建一個指向引用的指針。
  • 2) sqlite3_prepare_v2()將SQL語句編譯為字節代碼并返回狀態代碼 - 在對數據庫執行任意語句之前的重要步驟。 如果您有興趣,可以在這里找到更多信息。 檢查返回的狀態代碼以確保語句編譯成功。 如果是,則該過程轉到步驟3;否則,您打印一條消息,指出該語句無法編譯。
  • 3) sqlite3_step()運行已編譯的語句。 在這種情況下,您只需“步進”一次,因為此語句只有一個結果。 稍后在這個帶有Swift教程的SQLite中,您將看到何時需要多次執行單個語句。
  • 4) 您必須始終在編譯語句上調用sqlite3_finalize()以刪除它并避免資源泄漏。 一旦聲明完成,您就不應該再次使用它。

現在,將以下方法調用添加到playground

createTable()

Run你的playground,您應該看到控制臺輸出中出現以下內容:

Contact table created.

現在您有了一個表,是時候向它添加一些數據了。 您將添加Id1且名稱為Ray的單行。

3. Inserting Some Data - 插入一些數據

將以下SQL語句添加到playground的底部:

let insertStatementString = "INSERT INTO Contact (Id, Name) VALUES (?, ?);"

如果您沒有太多的SQL經驗,這可能看起來有點奇怪。 為什么值由問號代表?

在使用sqlite3_prepare_v2()編譯語句時,請記住上面的內容。語法告訴編譯器在實際執行語句時將提供實際值。

這有性能方面的考慮,并且允許您提前編譯語句,這可以提高性能,因為編譯是一項代價高昂的操作。 然后可以使用不同的值反復重復使用已編譯的語句。

接下來,在您的playground中創建以下方法:

func insert() {
  var insertStatement: OpaquePointer? = nil

  // 1
  if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
    let id: Int32 = 1
    let name: NSString = "Ray"

    // 2
    sqlite3_bind_int(insertStatement, 1, id)
    // 3
    sqlite3_bind_text(insertStatement, 2, name.utf8String, -1, nil)

    // 4
    if sqlite3_step(insertStatement) == SQLITE_DONE {
      print("Successfully inserted row.")
    } else {
      print("Could not insert row.")
    }
  } else {
    print("INSERT statement could not be prepared.")
  }
  // 5
  sqlite3_finalize(insertStatement)
}

以下是上述方法的工作原理:

  • 1) 首先,編譯語句并驗證一切正常;
  • 2) 在這里,您為占位符定義一個值。函數的名稱--sqlite3_bind_int() - 意味著您將Int值綁定到語句。函數的第一個參數是要綁定的語句,而第二個參數是你要綁定的?非零的索引的位置。第三個也是最后一個參數是值本身。此綁定調用返回狀態代碼,但現在您認為它成功。
  • 3) 執行相同的綁定過程,但這次是文本值。此次調用還有兩個附加參數;出于本教程的目的,您可以簡單地為它們傳遞-1nil。如果您愿意,可以在這里閱讀有關綁定參數的更多信息。
  • 4) 使用sqlite3_step()函數執行語句并驗證它是否已完成。
  • 5) 一如既往,最終確定聲明。如果您要插入多個聯系人,則可能會保留該語句并使用不同的值重新使用它。

接下來,通過將以下內容添加到playground中來調用您的新方法:

insert()

運行您的playground并驗證您在控制臺輸出中看到以下內容:

Successfully inserted row.

4. Challenge: Multiple Inserts - 挑戰:多個插入

挑戰時間! 您的任務是更新insert()以插入聯系人數組。

作為提示,您需要在再次執行之前調用sqlite3_reset()將已編譯的語句重置回其初始狀態。

func insert() {

  var insertStatement: OpaquePointer? = nil
  // 1
  let names: [NSString] = ["Ray", "Chris", "Martha", "Danielle"]

  if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {

    // 2
    for (index, name) in names.enumerated() {
      // 3
      let id = Int32(index + 1)
      sqlite3_bind_int(insertStatement, 1, id)
      sqlite3_bind_text(insertStatement, 2, name.utf8String, -1, nil)

      if sqlite3_step(insertStatement) == SQLITE_DONE {
        print("Successfully inserted row.")
      } else {
        print("Could not insert row.")
      }
      // 4
      sqlite3_reset(insertStatement)
    }

    sqlite3_finalize(insertStatement)
  } else {
    print("INSERT statement could not be prepared.")
  }
}

正如您所看到的,代碼與您已有的代碼非常相似,但具有以下顯著差異:

  • 1) 現在有一系列聯系人,而不是一個常數;
  • 2) 對每個聯系人遍歷一次數組;
  • 3) 現在,索引是從枚舉的索引生成的,該索引對應于數組中聯系人姓名的位置;
  • 4) SQL語句在每個遍歷結束時重置,以便下一個可以使用它。

5. Querying Contacts - 查詢聯系人

既然你已經插入了一兩行,那么確定它們真的很好用!

將以下內容添加到playground

let queryStatementString = "SELECT * FROM Contact;"

此查詢只是從聯系人表中檢索所有記錄。 使用*表示將返回所有列。

添加以下方法以執行查詢:

func query() {
  var queryStatement: OpaquePointer? = nil
  // 1
  if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
    // 2
    if sqlite3_step(queryStatement) == SQLITE_ROW {
      // 3
      let id = sqlite3_column_int(queryStatement, 0)

      // 4
      let queryResultCol1 = sqlite3_column_text(queryStatement, 1)
      let name = String(cString: queryResultCol1!)

      // 5
      print("Query Result:")
      print("\(id) | \(name)")

    } else {
      print("Query returned no results")
    }
  } else {
    print("SELECT statement could not be prepared")
  }

  // 6
  sqlite3_finalize(queryStatement)
}

下面分步詳細說明:

  • 1) Prepare語句;
  • 2) 執行該語句。 請注意,您現在正在檢查狀態代碼SQLITE_ROW,這意味著您在逐步執行結果時檢索了一行;
  • 3) 是時候從返回的行中讀取值了。 根據您對表的結構和查詢的了解,您可以逐列訪問行的值。 第一列是Int,因此您使用sqlite3_column_int()并傳入語句和從零開始的列索引。 您將返回的值分配給本地范圍的id常量;
  • 4) 接下來,從Name列中獲取文本值。 由于C API,這有點亂。 首先,將值捕獲為queryResultCol1,以便在下一行將其轉換為正確的Swift字符串;
  • 5) 打印出結果;
  • 6) 最后Finalize語句。

現在,通過將以下內容添加到playground的底部來調用您的新方法:

query()

Run你的playground,您將在控制臺中看到以下輸出:

Query Result:
1 | Ray

W00t! 看起來你的數據是進入數據庫的!

6. Challenge: Printing Every Row - 挑戰:打印每一行

您的任務是更新query()以打印出表中的每個聯系人。

func query() {
  var queryStatement: OpaquePointer? = nil
  if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {

    while (sqlite3_step(queryStatement) == SQLITE_ROW) {
      let id = sqlite3_column_int(queryStatement, 0)
      let queryResultCol1 = sqlite3_column_text(queryStatement, 1)
      let name = String(cString: queryResultCol1!)
      print("Query Result:")
      print("\(id) | \(name)")
    }

  } else {
    print("SELECT statement could not be prepared")
  }
  sqlite3_finalize(queryStatement)
}

請注意,不是像前面那樣使用單個步驟來檢索第一行,而是這次使用while循環來執行步驟,只要返回代碼是SQLITE_ROW就會發生。 當您到達最后一行時,返回代碼將通過SQLITE_DONE,循環將中斷。

7. Updating Contacts - 更新聯系人

下一個自然進展是更新現有行。 你應該開始看到一種模式出現了。

首先,創建UPDATE語句:

let updateStatementString = "UPDATE Contact SET Name = 'Chris' WHERE Id = 1;"

在這里你使用真正的值而不是占位符。 通常你會使用占位符并執行適當的語句綁定,但為了簡潔起見,你可以在這里跳過它。

接下來,將以下方法添加到playground

func update() {
  var updateStatement: OpaquePointer? = nil
  if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
    if sqlite3_step(updateStatement) == SQLITE_DONE {
      print("Successfully updated row.")
    } else {
      print("Could not update row.")
    }
  } else {
    print("UPDATE statement could not be prepared")
  }
  sqlite3_finalize(updateStatement)
}

這與您之前看到的類似:prepare, step, finalize!將以下內容添加到您的playground

update()
query()

這將執行您的新方法,然后調用您先前定義的query()方法,以便您可以看到結果:

Successfully updated row.
Query Result:
1 | Chris

恭喜您更新第一行! 這多么容易。

8. Deleting Contacts - 刪除聯系人

下面看一下刪除您創建的行。 再次,您將使用熟悉的prepare, step, and finalize

將以下內容添加到playground

let deleteStatementStirng = "DELETE FROM Contact WHERE Id = 1;"

現在添加以下方法來執行語句:

func delete() {
  var deleteStatement: OpaquePointer? = nil
  if sqlite3_prepare_v2(db, deleteStatementStirng, -1, &deleteStatement, nil) == SQLITE_OK {
    if sqlite3_step(deleteStatement) == SQLITE_DONE {
      print("Successfully deleted row.")
    } else {
      print("Could not delete row.")
    }
  } else {
    print("DELETE statement could not be prepared")
  }
  
  sqlite3_finalize(deleteStatement)
}

你現在掌握了嗎?Prepare, step, and finalize

執行這個新方法,然后調用query(),如下所示:

delete()
query()

現在運行你的playground,你應該在你的控制臺中看到以下輸出:

Successfully deleted row.
Query returned no results

注意:如果您完成了上面的“多個插入”挑戰,由于表中仍存在行,因此輸出可能與上面的內容略有不同。

9. Handling Errors - 處理錯誤

到目前為止,希望你已經設法避免SQLite錯誤。 但是,當你進行沒有意義的調用,或者根本無法編譯時,錯誤將會到來。

在發生這些事情時處理錯誤消息可以節省大量的開發時間,它還使您有機會向用戶顯示有意義的錯誤消息。

將以下語句 - 固定格式錯誤 - 添加到您的playground

let malformedQueryString = "SELECT Stuff from Things WHERE Whatever;"

現在添加一個方法來執行這個格式錯誤的語句:

func prepareMalformedQuery() {
  var malformedStatement: OpaquePointer? = nil
  // 1
  if sqlite3_prepare_v2(db, malformedQueryString, -1, &malformedStatement, nil) == SQLITE_OK {
    print("This should not have happened.")
  } else {
    // 2
    let errorMessage = String.init(cString: sqlite3_errmsg(db))
    print("Query could not be prepared! \(errorMessage)")
  }
  
  // 3
  sqlite3_finalize(malformedStatement)
}

以下是您將如何強制執行錯誤:

  • 1) 準備語句,該語句將失敗并且不應返回SQLITE_OK
  • 2) 使用sqlite3_errmsg()從數據庫中獲取錯誤消息。 此函數返回最近錯誤的文本描述。 然后,您將錯誤打印到控制臺;
  • 3) 一如既往,最終finalize

調用該方法以查看錯誤消息:

prepareMalformedQuery()

Run你的playground,您應該在控制臺中看到以下輸出:

Query could not be prepared! no such table: Things

嗯,這實際上很有幫助 - 你顯然無法在不存在的表上運行SELECT語句!

10. Closing the Database Connection - 關閉數據庫連接

完成數據庫連接后,您將負責關閉它。 但請注意 - 在成功關閉數據庫之前,必須執行許多操作,如SQLite documentation中所述。

調用close函數,如下所示:

sqlite3_close(db)

Run你的playground;您應該在playground的右側結果視圖中看到狀態代碼0;這表示SQLITE_OK,這意味著您的關閉調用成功。

您已經成功創建了一個數據庫,添加了一個表,向表中添加了行,查詢并更新了這些行,甚至刪除了一行 - 所有這些都使用了Swift的SQLite C API。 很好!

在下一節中,您將利用所學內容,并了解如何在Swift中包含其中一些調用。

后記

本篇主要講述了一個簡單的基于SQLite持久化方案示例,感興趣的給個贊或者關注~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容