我們大多數(shù)人都至少熟悉某些 Core Data 提供給我們的直接可用的持久化特性。然而不幸的是,它們中的很多在 Android 平臺上并不是自動化的。 例如,Core Data 抽象出大部分?jǐn)?shù)據(jù)庫的 SQL 語法和數(shù)據(jù)庫標(biāo)準(zhǔn),這些語法和標(biāo)準(zhǔn)都是數(shù)據(jù)庫工程師們每天面對的問題。 因為 Android 僅提供一個簡單的 SQLite 客戶端,所以你扔需要寫 SQL 并且確保你的數(shù)據(jù)庫表被適當(dāng)?shù)臉?biāo)準(zhǔn)化了。
Core Data 允許我們在對象方面考慮。 實(shí)際上,它能自動處理列集和散集對象。 得益于其提供了記錄層的緩存,所以它在移動設(shè)備上的性能很好。每次從存儲中請求同樣的數(shù)據(jù)段時,它不用再創(chuàng)建另外一個的對象實(shí)例。當(dāng)觀察一個對象的變化時,我們甚至不需要刷新那個被觀察對象就能做到。
Android 上可不是這種情況。你要完全負(fù)責(zé)將對象寫入數(shù)據(jù)庫或者從中將它們讀取。這意味著你要自己實(shí)現(xiàn)對象緩存(如果需要的話),管理對象實(shí)例化,以及手動對任何已存在對象進(jìn)行是否需要變更的檢測。
在 Android 中,你需要注意版本特定的功能。不同版本的 Android 使用不同的 SQLite 實(shí)現(xiàn)。這意味著同一個數(shù)據(jù)庫指令可能在其他平臺版本上產(chǎn)生完全不同的結(jié)果。根據(jù)你執(zhí)行的 SQLite 版本不同,一個查詢語句也會執(zhí)行得各不相同。
許多Android開發(fā)者來自企業(yè)。許多年來,對象關(guān)系型映射庫一直在服務(wù)器平臺上用于減輕與數(shù)據(jù)庫的交互的難度。然而,這些庫因為性能問題而導(dǎo)致無法在移動端直接使用。意識到這一點(diǎn),一些開發(fā)者組織起來制作了面向移動端的 ORM 庫來解決這一問題。
在 Android 上給 SQLite 添加 ORM 支持的其中一個使用廣泛的的方法是 OrmLite。OrmLite 提供對持久化對象的自動列集和散集化。它不用寫大量的 SQL,并且提供程序接口來查詢,更新,刪除對象。在 ORM 中另一個競爭者是 greenDAO。它提供許多與 OrmLite 類似的功能,但是承諾具有更好的性能(根據(jù)它的網(wǎng)站上說),例如基于注解的設(shè)置。
對第三方庫通常都是抱怨都是加到項目中以后造成了額外的復(fù)雜度并且使得性能臃腫。有開發(fā)者覺得這實(shí)在很蛋疼,于是他寫了一個叫 Cupboard 的輕量級 Android SQLite 框架的封裝。它聲稱的目標(biāo)是在不使用 ContentValues 和不解析 Cursors 的情況下為 Java 對象的提供持久化存儲,它將會簡單輕量,并整合時不會對核心 Android 類造成任何影響。你仍將需要管理數(shù)據(jù)庫的創(chuàng)建,但是查詢對象會變得簡單很多。
還有開發(fā)者決定完全廢棄 SQLite 并且創(chuàng)建了 Perst。它從開始到接口都是用面向?qū)ο笳Z言設(shè)計的。它善于列集和散集對象,并且在性能跑分中表現(xiàn)很好。這種解決方法確實(shí)是完全替換了部分 Android 框架,但是也存在一定風(fēng)險,因為你可能很難再在以后將其替換成不同方案了。
有這些甚至是更多可用的選擇,為什么大家還都選擇在原始的 Android 數(shù)據(jù)庫框架下開發(fā)呢?這么說吧,框架和封裝相較于它們所解決的問題,有時候能帶來更多麻煩。例如,在一個項目中,我們同時寫入數(shù)據(jù)庫且實(shí)例化很多對象,這就會導(dǎo)致我們的 ORM 庫慢得像爬一樣。因為這個庫不是設(shè)計用來讓我們以這樣的方式使用的。
在評估框架和庫的時候,檢查看看有沒有使用 Java 的反射機(jī)制。反射機(jī)制在 Java 中代價相對較大,因此要謹(jǐn)慎使用。另外,如果你的項目是 Ice Cream Sandwich 前的版本,還要看看你的庫是否在使用 Java 最近解決的一個 bug,它會導(dǎo)致在運(yùn)行時因為注解而導(dǎo)致的性能下降。
最后,還要評估添加框架會不會顯著的增加項目的復(fù)雜度。如果你與其他開發(fā)者共同開發(fā),記住,他們也必須花時間來學(xué)習(xí)該庫。在你決定要不要使用一個第三方解決方案之前,弄明白 Android 到底是如何處理數(shù)據(jù)存儲的這一問題,是非常重要的。
Android 上創(chuàng)建和打開數(shù)據(jù)庫相對簡單。你必須通過子類化 SQLiteOpenHelper 來進(jìn)行實(shí)現(xiàn)。默認(rèn)的構(gòu)造方法中,你要制定數(shù)據(jù)庫名字。如果該數(shù)據(jù)庫已經(jīng)存在,它會被打開。如果不存在,則會被創(chuàng)建。應(yīng)用能有許多單獨(dú)的數(shù)據(jù)庫文件。每個數(shù)據(jù)庫都必須表示為單獨(dú)的 SQLiteOpenHelper
子類。
數(shù)據(jù)庫文件對你的應(yīng)用來說是私有的,它們存在文件系統(tǒng)中你的應(yīng)用的子文件夾下,并且受Linux文件訪問權(quán)限保護(hù)。可惜的是,數(shù)據(jù)庫文件沒有加密。
但是創(chuàng)建數(shù)據(jù)庫文件是不夠的,在你的 SQLiteOpenHelper
子類中,你要重寫 onCreate()
方法執(zhí)行SQL語句創(chuàng)建表,視圖以及任何數(shù)據(jù)庫模式(schema)中的東西。你可以重寫例如 onConfigure()
之類的其他方法來啟用或禁用數(shù)據(jù)庫功能,比如預(yù)寫日志或外鍵支持等。
除了在 SQLiteOpenHelper
子類的構(gòu)造方法中指定數(shù)據(jù)庫名字,你還要指定數(shù)據(jù)庫版本號。版本號對于任意一個給定的 release 版本必須是不變的,而且根據(jù)框架的要求,這個數(shù)需要是只增不減的。
SQLiteOpenHelper
使用數(shù)據(jù)庫的版本號來決定是否需要升級或降級。在升級或降級的回調(diào)方法中,你將使用提供給你的 oldVersion
和 newVersion
參數(shù)來決定哪個 ALTER 語句需要執(zhí)行來更新 schema。為每個新數(shù)據(jù)庫版本提供單獨(dú)的語句是一種很好的方法,這樣就可以處理數(shù)據(jù)庫的跨版本升級了。
數(shù)據(jù)庫查詢是由 SQLiteDatabase 類來管理的。 在你的 SQLiteOpenHelper
子類調(diào)用 getReadableDatabase()
或 getWritableDatabase()
時,會返回一個 SQLiteDatabase
實(shí)例。要注意這些方法通常會返回同一個對象。唯一一個例外是 getReadableDatabase()
,在遇到諸如磁盤空間已滿之類的問題時,它會返回一個只讀的數(shù)據(jù)庫,這時會阻止寫入數(shù)據(jù)庫。由于磁盤問題其實(shí)很少發(fā)生,許多開發(fā)者開發(fā)過程中只調(diào)用 getWritableDatabase()
。
數(shù)據(jù)庫創(chuàng)建和模式改變是在你第一次獲得 SQLiteDatabase
實(shí)例后才會進(jìn)行的。因此,你不能在主線程請求 SQLiteDatabase
實(shí)例。你的 SQLiteOpenHelper
子類幾乎都會返回同樣的 SQLiteDatabase
實(shí)例。這意味著在任何線程調(diào)用 SQLiteDatabase.close()
都會關(guān)閉你應(yīng)用中所有的 SQLiteDatabase
實(shí)例。這就導(dǎo)致一大堆難于查找的 bug。實(shí)際上,有些開發(fā)者選擇只在程序啟動時打開 SQLiteDatabase
,只在程序關(guān)閉調(diào)用 close()
。
SQLiteDatabase
提供了對數(shù)據(jù)庫進(jìn)行查詢,插入,更新及刪除的方法。對于簡單的查詢,你不用寫任何 SQL。但對于更高級的查詢,你還要得自己寫 SQL。SQLiteDatabase 有一個 rawQuery()
和 execSQL()
方法, 這能把整個 SQL 當(dāng)做參數(shù)來執(zhí)行高級查詢,例如使用 unions 和 joins 命令。你可以使用 SQLiteQueryBuilder 來協(xié)助你完成適當(dāng)?shù)牟樵儭?/p>
query()
和 rawQuery()
都返回 Cursor 對象。保持 Cursor 對象的引用并且在應(yīng)用中傳來傳去聽起來十分誘人,但是 Cursor 對象比 單純的 Java 對象 (Plain Old Java Object, POJO) 耗費(fèi)更多的系統(tǒng)資源。因此,Cursor 對象需要盡快散集化到 POJO。在散集化之后你需要調(diào)用 close()
方法釋放資源。
SQLite 中支持事務(wù) (transactions)。你可以通過調(diào)用 SQLiteDatabase.beginTransaction()
開啟一個事務(wù)。事務(wù)能通過調(diào)用 beginTransaction()
嵌套。當(dāng)外層事務(wù)結(jié)束后所有在這個事務(wù)中完成的工作,以及所有嵌套的事務(wù)都需要提交或回滾。所有那些沒有用 setTransactionSuccessful()
方法標(biāo)記為完成的事務(wù)中的變更都會被回滾。
如前面提到的,Android 不提供任何列集和散集對象的方法。這意味著我們要負(fù)責(zé)管理從 Cursor 中取數(shù)據(jù)到 POJO 中的邏輯。這套邏輯應(yīng)該用 Data Access Object (DAO) 封裝好。
Java 從業(yè)者,順帶上 Android 開發(fā)者,應(yīng)該都對 DAO 模式很熟悉了。它的主要目的是將應(yīng)用的交互從持久化層中抽象出來,而不暴露持久化的實(shí)現(xiàn)細(xì)節(jié)。這可以把應(yīng)用從數(shù)據(jù)模式中隔離出來。這也使得遷移到第三方的數(shù)據(jù)庫實(shí)現(xiàn)時,對應(yīng)用的核心邏輯造成的風(fēng)險能更小。你的應(yīng)用中的所有與數(shù)據(jù)庫的交互都應(yīng)該通過 DAO 實(shí)現(xiàn)。
獲取 SQLiteDatabase
的參照是一個昂貴的操作,因此永遠(yuǎn)不要在主線程執(zhí)行它。擴(kuò)展來說,數(shù)據(jù)庫查詢也不要在主線程執(zhí)行。Android 提供 Loaders 來幫助實(shí)現(xiàn)這一點(diǎn)。它們允許 activity 或 fragment 異步加載數(shù)據(jù)。Loaders 可以解決配置改變時數(shù)據(jù)持久的問題,也能檢測數(shù)據(jù)并在內(nèi)容改變的時候發(fā)送新的結(jié)果。Android 提供 CursorLoader 來從數(shù)據(jù)庫中加載數(shù)據(jù)。
雖然數(shù)據(jù)庫對于創(chuàng)建它們的應(yīng)用來說是私有的,但是 Android 也提供與其他應(yīng)用程序共享數(shù)據(jù)的方法。Content Providers 提供一個結(jié)構(gòu)化的接口,能使其他應(yīng)用讀取甚至可能修改你的數(shù)據(jù)。和 SQLiteDatabase
類似, Content Providers 開放出 query()
,insert()
,update()
和 delete()
這些方法來操作數(shù)據(jù)。數(shù)據(jù)以 Cursor
的形式返回,而且對 Content Provider 的訪問默認(rèn)是同步的,這樣可以使訪問是線程安全的。
Android 數(shù)據(jù)庫與 iOS 上類似的功能相比,實(shí)現(xiàn)更加復(fù)雜。但是切記,不要只是為了減少模板代碼而去使用第三方庫。對于 Android 數(shù)據(jù)庫框架的徹底理解會讓你知道該不該選擇使用第三方庫,以及使用什么樣的第三方庫。Android Developer 網(wǎng)站 提供了兩個操作 SQLite 數(shù)據(jù)庫的樣例工程。你可以詳細(xì)看看 NotePad 和 SearchableDictionary 這兩個項目可以獲得更多信息。