FMDB與SQLite 數據庫應用示範:打做一隻簡單的電影資料庫 App
通常在 App 中使用數據庫并處理數據都會是一個重要和嚴肅的話題。在幾個月前我寫了一篇關於如何利用 SwiftyDB 來管理 SQLite 數據庫的文章。今天,我又提起數據庫這個話題,只不過這次我會介紹另一個庫。你也許聽說過了,它就是FMDB。 這兩個庫的功能都是一樣的,都是用來與 SQLite 數據庫打交道并允許你高效地管理你的 App 數據。但是,它們在使用上是截然不同的。SwfityDB 提供了一個高級 API 來隱藏所有 SQL 細節和其它底層操作,而 FMDB 提供了一個更精細的粒度來處理數據,它是一個位於更底層的 API。它仍然“隱藏”了與 SQLite數據連接和通訊的細節這些非常繁瑣的工作,大部份開發者想要的功能無非是想自定義查詢和操作數據。總的來說,兩個庫在不同的情況下各有所長,這取決於 App的特性和目的。因此,它們都是非常棒的工具,你可以根據自己的需要選擇最合適的一個。 現在,讓我們來看看 FMDB,它實際是一個對 SQLite 的高級別的 封裝,這樣我們可以不必關心如何連接數據庫,以及如何去實際讀取和寫入數據到數據庫中。對於那些想充分利用自己的 SQL 技能并自己編寫 SQL 語句的開發者來說,FMDB 是最好的選擇,他們完全可以不用自己編寫 SQLite 管理器。它可以在 O-C 和 Swift 下使用,將它整合到你的專案中非常容易,一點也不費事。 我們將通過一個小的 Demo App 中的幾個例子來學習 FMDB。一開始我們會用程式碼創建一個新的數據庫,然後進行常見的數據庫操作:插入、修改、刪除和查詢。更多的內容,我建議你看一下原來的 Github 頁。當然,由於這是一個數據庫相關的主題,我會假設你懂得基本的 SQL語句,否則你可能需要先了解一下這些知識才能繼續學習。 如果你與我一樣,是一個數據庫愛好者,那麼請跟我來吧,我們會學習一些非常有趣的事情! Demo App 介紹在本文中,我們的 Demo App 將顯示一個影片列表,并通過一個新的 View Controller 展現(我經常使用電影來作為演示數據,而且在 IMDB上有一個非常好的數據源)。當顯示電影詳情時,我們能夠將電影標記為 已觀看 并 打分 (從 0-3)。 電影數據保存在 SQLite 數據庫中,這個數據庫我們會使用 FMDB 進行管理。初始數據將從一個已有的 tab 符分隔檔案 (.tsv) 導入。 我們將主要精力放在數據庫相關的內容,因此這裡提供了一個 開始專案,你可以用它來開始學習。在開始專案中,已經完成了一些工作,包括原始的 .tsv 檔案,我們將通過這個檔案來獲取最初始的電影數據。 再多透露一點關於 Demo App 的事情。首先,它是一個 navigation-based的App,有兩個 View Controller:一個叫做 MoviesViewController,它包含了一個 Tableview,我們用來顯示電影的標題和封面 (總共有 20 部電影)。 在表記錄中,電影封面圖片沒有保存在本地;而是當列表被顯示時從網絡上異步抓取的。點擊每一行電影的 cell,會顯示第二個 View Controller,叫做 MovieDetailsViewController。每部電影的數據將包括:
此外,我們還有一個 switch 控件,用於表示這部電影你是否已經看過,以及一個 stepper 控件用於你給這部電影打分,通過加號和減號按鈕來調整你對這部電影的喜好程度。影片詳情修改后應當保存到數據庫。 除此之外,在 MoviesViewController.swift 檔案中,你會發現一個結構體,叫做 MovieInfo。它的屬性和數據庫中的欄位段相匹配,一個 MovieInfo 結構將在代碼中用於代表一個影片的信息。在這裡我就不討論關於這個數據庫以及我們要如何使用它了,我們會在具體過程中去了解它。我再說一次接下來我們將要干的事情:創建數據庫(通過編寫程式碼)、插入數據、修改數據、刪除數據和查找數據。我們會力求簡化這些過程,但我們也可以在大數據量的應用中套用同樣的技術。 下載完開始專案并瀏覽了專案內容之後,我們繼續下一步的內容。我們一開始要將 FMDB這個庫添加到開始專案中,然後才能學習如何對數據庫進行操作。同時,我們將學習到一些讓你的程序員生涯更加舒適的良好實踐。 整合 FMDB 到 Swift 專案中整合 FMDB 到專案中的最常見的方式是使用 CocoaPods ,你可以參考 這裡。但是,對於 Swift專案,最好的辦法是下載源代碼的 zip 包,然後將某些檔案拖到你的專案中。Xcode 可能會問你是否要添加 bridging header 檔案,因為 FMDB 庫是用 Objective-C 些的,爲了讓 Swift 和 Objective-C 這兩種語言能夠共存,你必須使用 bridging 檔案。 讓我們具體演示一下。在瀏覽器中打開我前面提到的超鏈接。在頁面右上角有一個綠色的 “Clone or download” 按鈕。點擊它,你會看到另一個 “Download ZIP” 的按鈕,點擊這個按鈕,源代碼就會以 zip 壓縮檔案的形式下載到你的電腦。 打開下載的 zip 檔案,解壓縮,找到 fmdb-master/src/fmdb 目錄 (使用 Finder)。這個目錄下的檔案就是你需要添加到開始專案中的檔案。最好先在專案導航窗口中新創建一個 Group 用於存放這些檔案,這樣這些檔案會單獨組織在一起以和專案中的其它檔案分開。選中這個目錄下的所有檔案(其中 .plist 檔案除外,你不需要它),然後拖到 Xcode 的專案導航窗口。 添加完這些檔案后,Xcode 會問你是否需要創建一個 bridging header 檔案。 如果你不想手動創建這個檔案的話,最好點擊確認。這會添加另一個檔案到專案中,叫做 FMDBTut-Bridging-Header.h。打開這個檔案并編寫下面的代碼: #import "FMDB.h" 現在,FMDB 的類就可以在 Swift 中使用了,接下來我們就準備使用它們。 創建數據庫和數據庫打交道總是這幾個步驟:和數據庫建立連接、加載或修改數據庫中的數據、關閉連接。我們可以在專案中的每一個類中重複這些步驟,因為 FMDB 的類可以在你想用時候就用。但是,我覺得這並不是一個好主意,將來你修改或調試代碼時這會帶來一些不方便,因為這些和數據庫有關的代碼在整個專案中擴散了。我更願意創建一個類來干這些事情:
你應該明白了,我們會基於 FMDB 創建一個更高級的數據庫 API,只不過它們只會用在這個 App 中。爲了讓這個類獲得更大的靈活性,我們將這個類創建成一個單實例 singleton,這樣使用它時不用創建類的實例。關於單實例 singletons,可以參考 這裡 ,或者去 web 上搜索。 現在,讓我們理論聯繫實際。打開開始專案,創建一個類用於數據庫管理(使用 Xcode 的 File 菜單 > New > File… -> Cocoa Touch Class)。當 Xcode 詢問類名時,使用 DBManager 作為類名,并伸延 NSObject 類。創建好新的類檔案之後,請繼續。 打開 DBManager 類,添加以下代碼以實現 singleton: static let shared: DBManager = DBManager() 我強烈建議你讀一下關於 Swift 的 singleton,理解爲什麽這行代碼會實現 singleton。無論如何,以後我們只需要用這樣的代碼 然後,我們需要宣告 3 個重要屬性:
添加代碼: let databaseFileName = "database.sqlite" var pathToDatabase: String! var database: FMDatabase! 嘿,別急!我們忘記了 override init() { super.init() let documentsDirectory = (NSSearchPathForDirectoriesInDomains(.documentDirectory,.userDomainMask,true)[0] as NSString) as String pathToDatabase = documentsDirectory.appending("/(databaseFileName)") } 當然,這個 我們創建一個方法,叫做 現在,來看一看數據庫檔案要如何創建: func createDatabase() -> Bool { var created = false if !FileManager.default.fileExists(atPath: pathToDatabase) { database = FMDatabase(path: pathToDatabase!) } return created } 我們做了兩件事情:
先不要管 回到這個方法,繼續檢查數據庫已創建成功,然後打開它: func createDatabase() -> Bool { var created = false if !FileManager.default.fileExists(atPath: pathToDatabase) { database = FMDatabase(path: pathToDatabase!) if database != nil { // 打開數據庫 if database.open() { } else { print("Could not open the database.") } } } return created }
現在,我們來創建一張數據庫表。爲了簡單起見,我們只創建這一張表。欄位(表名我們會使用 movies) 和 MovieInfo 結構中的屬性完全相同,如果你打開 MoviesViewController.swift 檔案,你會看到這些屬性。為了簡便,我給出了正確的 SQL 語句(從中你也可以看到每個欄位的名字和型態): let createMoviesTableQuery = "create table movies (movieID integer primary key autoincrement not null,title text not null,category text not null,year integer not null,movieURL text,coverURL text not null,watched bool not null default 0,likes integer not null)" 接下來真正執行上面的 SQL 語句,這會在我們的數據庫中創建一張數據表: database.executeUpdate(createMoviesTableQuery,values: nil)
上面的語句會導致 Xcode 報錯。因為這個方法會 拋出 一個異常。因此我們需要將上面的語句修改為: do { try database.executeUpdate(createMoviesTableQuery,values: nil) created = true } catch { print("Could not create table.") print(error.localizedDescription) } 注意在 現在我將完整的 func createDatabase() -> Bool { var created = false if !FileManager.default.fileExists(atPath: pathToDatabase) { database = FMDatabase(path: pathToDatabase!) if database != nil { // Open the database. if database.open() { let createMoviesTableQuery = "create table movies (movieID integer primary key autoincrement not null,likes integer not null)" do { try database.executeUpdate(createMoviesTableQuery,values: nil) created = true } catch { print("Could not create table.") print(error.localizedDescription) } // At the end close the database. database.close() } else { print("Could not open the database.") } } } return created } 一些建議在繼續後面的內容之前,我來演示一些最佳實踐,也許會讓我們的工作更加輕鬆,避免潛在的問題。之所以要這樣,是因為我們的 App 太簡單了,而且操作的數據非常少。如果你在做一個大專案,那你真的需要注意這些,因為它們會節省你的時間,防止你出現相同的代碼和相同的錯誤。 因此,從現在開始,就讓我們的日子更好過而且在大專案中節省我們的時間吧。當我們建立數據庫連接查找數據或者進行修改操作時(插入、修改和刪除),我們必須重複這幾步:判斷數據庫物件(database)是否宣告,如果未宣告,就呼叫 在 DBManager 類中,我們來創建這個方法: func openDatabase() -> Bool { if database == nil { if FileManager.default.fileExists(atPath: pathToDatabase) { database = FMDatabase(path: pathToDatabase) } } if database != nil { if database.open() { return true } } return false } 這個方法首先檢查 database物件是否宣告,如果未宣告它的值應該是 nil。然後嘗試打開數據庫,這個方法的返回值是 在前面,我們構造了一條 SQL 語句用於創建 movies 表: let createMoviesTableQuery = "create table movies (movieID integer primary key autoincrement not null,likes integer not null)" 這個 SQL 語句并沒有問題,但有一點,在我們下次書寫新的 SQL 語句時會導致某種風險。這個問題出在欄位名上,每當我們創建查詢時都不得不拼寫這些欄位名。這樣做的次數一多,我們就會拼錯一個甚至是多個的欄位,這就會導致錯誤。例如,如果我們粗心大意,很容易將 movieID 拼寫成 movieId,或者將 movieURL 拼成 movieurl。我們總會和許多 SQL 語句打交道的,這種情況基本上都會發生,這跟表的多寡無關。好了,這不是什麽大問題,因為要發現這種問題其實也不花多少時間。但爲什麽要把時間浪費在這種問題上?消滅風險是一種好的做法,將欄位名(每個表中,這裡我們只是一張表)定義成constant 屬性吧!在本例中,我們可以這樣做: 在 DBManager 類的一開始,加入下列語句: let field_MovieID = "movieID" let field_MovieTitle = "title" let field_MovieCategory = "category" let field_MovieYear = "year" let field_MovieURL = "movieURL" let field_MovieCoverURL = "coverURL" let field_MovieWatched = "watched" let field_MovieLikes = "likes" 我使用了 field 前綴,這樣在 Xcode 中輸入時很容易找到這些欄位名。當你輸入 field 時 Xcode 會自動建議你名字中包含有 field 的屬性,這樣你就容易找到你想要的欄位名了。名字的第二部份實際上是每個欄位的簡短描述。你甚至可以更進一步,將每個屬性中帶上表名: let field_Movies_MovieID = "movieID" 當然這裡並不需要,因為我們只有一張表。如果你有多張表就不一樣了,你可以使用上面的命名規約。 通過用常量來保存欄位名,我們不再需要手動拼寫欄位名,我們可以在所有需要的地方用常量來代替欄位名,以避免輸入錯誤。如果我們修改我們的 SQL 語句,則會是這個樣子: let createMoviesTableQuery = "create table movies ((field_MovieID) integer primary key autoincrement not null,(field_MovieTitle) text not null,(field_MovieCategory) text not null,(field_MovieYear) integer not null,(field_MovieURL) text,(field_MovieCoverURL) text not null,(field_MovieWatched) bool not null default 0,(field_MovieLikes) integer not null)" 上面兩個建議在你的專案中并不是必須的,我這裡只是建議和推薦,但你用不用它們則隨你的便。你可以繼續使用傳統的做法,也可以找出另一種更好的解決方案。但在我們的 Demo App中,我仍然會採用這兩個建議。言歸正傳,讓我們繼續後面的內容。 插入記錄在這一節,我們會將原始數據導入到數據庫中,數據來自於 movies.tsv 檔案,這個檔案已經包含在開始專案中了 (可以在專案導航窗口中找到它)。這個檔案包含了 20 部影片的數據,每條影片的記錄是以 “rn” (不包含雙引號)分割的。每條記錄中的欄位用 tab 字符 (“t”) 分割。這種格式的轉換都不是什麽大問題。數據格式如下所示:
對於表中還沒有數據的其它欄位,我們暫時用默認值替代。 在 DBManager 類中,我們創建一個新方法,為我們處理所有事情。我們會使用前面創建的方法來打開數據庫,因此方法的第一行是這個: func insertMovieData() { // Open the database. if openDatabase() { } } 接下來的流程是這個樣子:
首先是獲得 “movies.tsv” 檔案的位置并加載它到一個 String 物件中: if let pathToMoviesFile = Bundle.main.path(forResource: "movies",ofType: "tsv") { do { let moviesFileContents = try String(contentsOfFile: pathToMoviesFile) } catch { print(error.localizedDescription) } } 從檔案中加載 String 會拋出異常,所以要用 let moviesData = moviesFileContents.components(separatedBy: "rn") 然後是第三步。使用 var query = "" for movie in moviesData { let movieParts = movie.components(separatedBy: "t") if movieParts.count == 5 { let movieTitle = movieParts[0] let movieCategory = movieParts[1] let movieYear = movieParts[2] let movieURL = movieParts[3] let movieCoverURL = movieParts[4] } } 在上面的 query += "insert into movies ((field_MovieID),(field_MovieTitle),(field_MovieCategory),(field_MovieYear),(field_MovieURL),(field_MovieCoverURL),(field_MovieWatched),(field_MovieLikes)) values (null,'(movieTitle)','(movieCategory)',(movieYear),'(movieURL)','(movieCoverURL)',0);" 最後兩個欄位,我們使用默認值。後面我們會用 SQL 語句中的 update 語句來修改它們。 在 if !database.executeStatements(query) { print("Failed to insert initial data into the database.") print(database.lastError(),database.lastErrorMessage()) } 如果插入失敗, 這聽起來可能無所謂,但別忘了(我再次提醒,別忘了)關閉數據庫連接,因此最終加上一句 func insertMovieData() { if openDatabase() { if let pathToMoviesFile = Bundle.main.path(forResource: "movies",ofType: "tsv") { do { let moviesFileContents = try String(contentsOfFile: pathToMoviesFile) let moviesData = moviesFileContents.components(separatedBy: "rn") var query = "" for movie in moviesData { let movieParts = movie.components(separatedBy: "t") if movieParts.count == 5 { let movieTitle = movieParts[0] let movieCategory = movieParts[1] let movieYear = movieParts[2] let movieURL = movieParts[3] let movieCoverURL = movieParts[4] query += "insert into movies ((field_MovieID),0);" } } if !database.executeStatements(query) { print("Failed to insert initial data into the database.") print(database.lastError(),database.lastErrorMessage()) } } catch { print(error.localizedDescription) } } database.close() } } 雖然我們的重點是如何處理 “movies.tsv” 檔案的數據,并以一種易於復用的方式轉換這些數據,但仍然有一個和我們的主題不太相關的地方也值得關注: 如何創建多個 SQL 語句 (記住,語句之間用 ; 分隔),以及如何批量執行這些語句。這是 FMDB 的特性之一,我們在這一節中學會了它。 在我們結束本節之前,我們還需要做一件事情;我們必須呼叫我們的新方法去創建數據庫和插入初始數據到數據庫中。打開 AppDelegate.swift 檔案,找到 func applicationDidBecomeActive(_ application: UIApplication) { if DBManager.shared.createDatabase() { DBManager.shared.insertMovieData() } } 加載數據在 MoviesViewController 類中,有一個 TableView,基本功能已經完成,只差一步就可以顯示我們從數據庫中加載的數據了。TableView 的數據源是一個陣列,叫做 回到 DBManager ,最關鍵的任務是如何用 FMDB 來執行 SELECT SQL 語句,我們會在一個新方法中加載影片數據: func loadMovies() -> [MovieInfo]! { } 方法的返回值是一個 MovieInfo 物件集合,這個方法會在 MoviesViewController 類中用到。我們會在方法一開始宣告一個本地陣列變數,用於存儲我們從數據庫中加載的結果,接著當然是打開數據庫: func loadMovies() -> [MovieInfo]! { var movies: [MovieInfo]! if openDatabase() { } return movies } 下一步是創建 SQL 語句,告訴數據庫我們需要什麼樣的數據: let query = "select * from movies order by (field_MovieYear) asc" 接下來執行 SQL 語句: do { let results = try database.executeQuery(query,values: nil) } catch { print(error.localizedDescription) } FMDatabase 的 在上面的 SQL 語句中,我們讓 FMDB 返回所有的影片,并以發佈年份升序排序。這僅僅是一個簡單的例子,但你可以根據自己的需要創建更複雜的 SQL 語句。讓我們來看一個更複雜的例子,我們想根據指定的種類加載影片數據,同樣以年份排序,但是以降序排序: let query = "select * from movies where (field_MovieCategory)=? order by (field_MovieYear) desc" 注意在 SQL 語句的 where 字句中并沒有指定 category 的具體值。對應地,我們在 SQL 中用一個符號替代,我們會在下面提供一個真實的值(我們告訴 FMDB 我們只需要類型為 犯罪 影片): let results = try database.executeQuery(query,values: ["Crime"]) 再來一個例子,我們需要所有指定類型的影片,但放映年份必須大於我們指定的年份,同時按照 ID 降序排序: let query = "select * from movies where (field_MovieCategory)=? and (field_MovieYear)>? order by (field_MovieID) desc" 執行上面的 SQL 語句需要提供兩個值: let results = try database.executeQuery(query,values: ["Crime",1990]) 明白了吧,創建和執行查詢語句沒有什麽特別的地方,再次說明,你可以按照自己的需求創建你自己的 SQL 語句。 接下來我們繼續下一步,即使用返回的數據。下面的程式碼中,我們用一個 while results.next() { let movie = MovieInfo(movieID: Int(results.int(forColumn: field_MovieID)),title: results.string(forColumn: field_MovieTitle),category: results.string(forColumn: field_MovieCategory),year: Int(results.int(forColumn: field_MovieYear)),movieURL: results.string(forColumn: field_MovieURL),coverURL: results.string(forColumn: field_MovieCoverURL),watched: results.bool(forColumn: field_MovieWatched),likes: Int(results.int(forColumn: field_MovieLikes)) ) if movies == nil { movies = [MovieInfo]() } movies.append(movie) } 有一個關鍵的地方,上述代碼中有一個強制性的規定,無論你獲取到的數據是一條還是多條,都要宣告 if results.next() { } 另外有一個地方需要注意:每個 現在,讓我們看一眼我們所創建的方法的完整程式碼: func loadMovies() -> [MovieInfo]! { var movies: [MovieInfo]! if openDatabase() { let query = "select * from movies order by (field_MovieYear) asc" do { print(database) let results = try database.executeQuery(query,values: nil) while results.next() { let movie = MovieInfo(movieID: Int(results.int(forColumn: field_MovieID)),likes: Int(results.int(forColumn: field_MovieLikes)) ) if movies == nil { movies = [MovieInfo]() } movies.append(movie) } } catch { print(error.localizedDescription) } database.close() } return movies } 接下來我們使用這個方法,在 TableView 上顯示影片數據。打開MoviesViewController.swift 檔案,編輯 override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) movies = DBManager.shared.loadMovies() tblMovies.reloadData() } 然後,我們還需要在 func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell",for: indexPath) let currentMovie = movies[indexPath.row] cell.textLabel?.text = currentMovie.title cell.imageView?.contentMode = UIViewContentMode.scaleAspectFit (URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: URL(string: currentMovie.coverURL)!,completionHandler: { (imageData,response,error) in if let data = imageData { DispatchQueue.main.async { cell.imageView?.image = UIImage(data: data) cell.layoutSubviews() } } }).resume() return cell } 每部影片的圖片都是異步下載的,只有當數據下載完后才會在 cell 上顯示。但願 URLSession 語句塊的寫法不會讓你困惑,改成用多個語句來寫,它就是這樣: let sessionConfiguration = URLSessionConfiguration.default let session = URLSession(configuration: URLSessionConfiguration.default) let task = session.dataTask(with: URL(string: currentMovie.coverURL)!) { (imageData,error) in if let data = imageData { DispatchQueue.main.async { cell.imageView?.image = UIImage(data: data) cell.layoutSubviews() } } } task.resume() 現在,你可以第一次咝 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |