Room Database入門指南
說到Android端有哪些可以使用的資料庫,大家首先想到的自然是SQLite這種帶有官方屬性加持的輕型的資料庫。
不過對於像我這種基本上沒有接觸過SQL資料庫語言編寫的人來說,要通過去寫難以查錯且又毫不熟悉的資料庫程式碼才能操作資料庫的話,那就太令人頭大了。

image
於是乎,便於Android開發者操作資料庫的框架也就多了起來,其中人氣較高的就有GreenDao、Realm,ObjectBox等,而Room則是谷歌官方十分推薦的,可以代替SQlite的不二之選。
本篇的主要介紹物件也是Room資料庫,不過在此之前,還得簡單介紹一下上面提到過的其他幾位,同時做個小小的對比。
一、介紹與比較
由於我用過的資料庫框架並不多,所以對於用過的可以說一下感受,沒用過的就簡單帶過了。
介紹
GreenDao 和 ObjectBox
在這些資料庫中, GreenDao 算是早聞其名,不過一直沒有用過,後來它的作者又出了個 ObjectBox ,而且你可以在 greenDAO" target="_blank" rel="nofollow,noindex"> GreenDao 的GitHub頁面 找到推薦使用 ObjectBox 的 ObjectBox地址 .
Realm
我真正使用過的還只有 Realm 資料庫,這裡要提一下, Realm 資料庫對於中國的開發者非常的友好,就像大名鼎鼎的Glide一樣, Realm 也有中文的介紹文件,文件地址在此:
開始使用Realm 雖然這份文件對應的版本不是最新的. 不過對於初次接觸 Realm 人來說,看這份文件就可以上手了
最開始使用Realm的時候也是碰過不少坑,不過最主要的是所有資料庫物件需要繼承 RealmObject 這個類(也可以通過介面實現),這樣對專案已有的資料結構不太友好,同時我還發現繼承了 RealmObject 的物件並不能與 Gson 完美結合,如果需要轉換的話,還是得費一番周折的。
種種原因,導致我最後從專案中抽去了Realm這個資料庫.
Room
與 Realm 分手後的日子裡,我並沒有放棄對新的資料庫的尋找,後來在瀏覽 Google官方文件的時候才發現了 Room 這個新的資料庫,經過我一番使用後,就決定是它了!

image
比較
因為懶惰的原因,我並沒有做過深入的測試,下面會給出從網上找到的關於這些資料庫的對比,原文地址如下:
Realm, ObjectBox or Room. Which one is for you?
然後是資料量達到 100k/10k 的時候,進行增刪改查等操作消耗的時間對比:

image

image

image

image

image

image
可以看到,在各個方面,統統都是 ObjectBox 傲視群雄。
那這篇文章為什麼還是要寫介紹關於 Room Database 呢?
首先是官方Buff加持,和介紹文件裡的一句話:

image
大致意思就是: 我們強烈建議你用Roon去代替SQLite,不過如果你是個鐵頭娃非得用SQLite,那我們也沒有辦法。
除了上面這段話,還有一點也可以作為選擇Room的原因,就是對於Apk的“增量”是多少。據別人的測試
ObjectBox和Realm分別佔用1-1.5MB和3-4MB(大小取決於手機架構),而作為SQL封裝的Room只佔用大約50KB。在方法的增量上,Room只有300多個,ObjectBox和Realm則分別是1300和2000個
當然,如果你的資料量很大的話,我覺得還是 ObjectBox 更加適合你,因為就從上面的操作資料對比來看, ObjectBox 太牛逼了!我以後肯定也會花時間去對 ObjectBox 做一番研究,不過目前還是先來介紹介紹 Room 吧。
二、Room的結構
之前有說過, Room 是可以代替 SQLite 的,不過我覺得Google推出它更多的是為了搭配 DataBinding 使用,如果你對於 DataBinding 不太熟悉,可以看一看我前面的關於 DataBinding 的文章,這裡就不再贅述了。下面就開始說說 Room 的結構。
Room主要分為三個部分,分別是 Database (資料庫) 、 Entity (實體) 、 DAO (資料訪問物件)
Database(資料庫)
資料庫指的就是一個數據庫物件,它繼承於 RoomDataBase 這個類,並且需要用 @DataBase 註解,獲取這個資料庫物件的方法是通過呼叫 Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder() ,後者表示在記憶體中儲存資料,如果程式結束了資料也就消失了,所以一般還是使用前者。
Entity(實體)
實體的概念就比較簡單了,就類似於MySQL資料庫裡面的表,一個實體類相當於一個表,而一個實體類有多個屬性,就相當於表的多個欄位,這個看一看接下來關於 Entity 的程式碼便一目瞭然。
DAO
關於 DAO ,抽象的概念就表示 資料訪問物件 ,在這裡簡單的解釋一下就是資料操作介面,可以通過編寫 DAO介面 對資料庫進行增刪改查等一系列操作。
PS:這些介面可以支援RxJava的哦!
下面是圖片說明:

image
三、開始使用
在 Room 的使用過程中,也是遇到一些坑的,不過都已經解決掉了。如果你也遇到過某些問題,不妨對照一下我的接入流程,說不定就找到了問題所在。

image
接入Gradle
為了避免之後的單元測試出現 <font color="#DC143C">java.lang.RuntimeException: Method e in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.</font> 的錯誤,除了 Room 相關的依賴需要新增外,這裡還需要再引用一下 robolectric單元測試庫 解決問題!
//room資料庫 def room_version = "1.1.1" implementation "android.arch.persistence.room:runtime:$room_version" annotationProcessor "android.arch.persistence.room:compiler:$room_version" kapt "android.arch.persistence.room:compiler:$room_version"// 由於要使用Kotlin,這裡使用了kapt implementation "android.arch.persistence.room:rxjava2:$room_version"//之後會用到rxjava,所以這裡也可以有 //implementation "android.arch.persistence.room:guava:$room_version"//由於我們不用guava,這行註釋掉 testImplementation "android.arch.persistence.room:testing:$room_version" //robolectric測試 testImplementation 'org.robolectric:shadows-multidex:3.8' testImplementation "org.robolectric:robolectric:3.8" //這樣就資瓷單元測試咯!
和我一樣使用Kotlin的童鞋別忘了下面這行:
apply plugin: 'kotlin-kapt'
還有,需要做如下更改:
androidTestImplementation 'com.android.support.test:runner:1.0.2' //更改為 implementation 'com.android.support.test:runner:1.0.2'
這點一定要改哦!不然會出現一些莫名其妙的問題
相關庫的依賴成功新增後就可以開始動手了!
建立 Entity、Dao 與 DataBase
建立Entity
首先,建立一個 Entity 物件,就把它命名為 Book 吧
@Entity class Book(@field:ColumnInfo(name = "book_name") var bookName: String?, var author: String?, var type: String?) { @PrimaryKey(autoGenerate = true) var id: Int = 0 }
Book有三個屬性,分別表示書名、作者、型別。其中有三點需要注意:
- 每個 Entity物件 都需要使用 @Entity 註釋宣告
- @PrimaryKey 註釋用於宣告主鍵,這裡還添加了 autoGenerate = true,表示它是自增的
- @ColumnInfo 註釋用來給屬性設定別名,如果 bookName 屬性不設定別名的話,查詢的時候可以通過 “ bookName ”進行查詢,設定別名後就可以通過設定的“ book_name ” 進行查詢了,看 DAO介面 便知
建立 DAO
這裡,通過 DAO介面 來對 Book 這個物件進行增刪改查:
@Dao interface BookDao { @get:Query("SELECT * FROM book") val all: List<Book> @Query("SELECT * FROM book WHERE author LIKE :author") fun getBookByAuthor(author: String): List<Book> @Query("SELECT * FROM book WHERE book_name LIKE :name") fun getBookByNamer(name: String): List<Book> @Insert fun insert(book: Book): Long? @Insert fun insert(vararg books: Book): List<Long> @Insert fun insert(books: List<Book>): List<Long> @Update fun update(book: Book): Int @Update fun update(vararg books: Book): Int @Update fun update(books: List<Book>): Int @Delete fun delete(book: Book): Int @Delete fun delete(vararg books: Book): Int @Delete fun delete(books: List<Book>): Int }
上面的 DAO介面 ,同樣需要進行幾點說明:
- DAO介面 需要使用 @Dao 註釋進行宣告
- Insert 操作可以使用 Long 作為返回值的型別,表示插入操作前的物件數量
- Update 和 Delete 操作可以使用 Int 作為返回值,表示更新或者刪除的行數
- 返回型別還可以是 void ,如果結合 Rxjava 使用的話還可以是 Completable、Single、 Maybe、Flowable 等,具體可以參見這篇文章: Room :link: RxJava (需要備好梯子,不過後續有時間的話我也會介紹一下Room搭配Rxjava的使用)
Dao介面編寫完成後,還剩下最重要的 DataBase
建立 DataBase
由於例項化一個 RoomDatabase 物件的開銷是比較大的,所以 DataBase 的使用需要遵循單例模式,只在全域性建立一個例項即可。
這裡為了方便理解,還是使用java程式碼去建立一個 BookDataBase類 ,當然,轉換成Kotlin只需要Shift + Alt + Ctrl + K 即可
如果你使用的是餓漢式的單例模式,在Kotlin中通過object修飾可達到同樣效果
@Database(entities = {Book.class}, version = 1) public abstract class BookDataBase extends RoomDatabase { public abstract BookDao bookDao(); private static BookDataBase instance; public static BookDataBase getInstance(Context context){ if (instance == null){ synchronized (BookDataBase.class){ if (instance == null){ instance = create(context); } } } return instance; } private static BookDataBase create(Context context) { return Room.databaseBuilder( context,BookDataBase.class,"book-db").allowMainThreadQueries().build(); } }
上面的例子中有一些需要特別注意:
- @Database 註釋用於進行宣告,同時還需要有相關的 entity物件 ,其中 version 是當前資料庫的版本號,如果你對資料相關的 實體類結構 進行了更改,這裡的 version 就需要 加一
- BookDataBase 除了繼承於 RoomDatabase ,還需要例項出相關的 DAO介面
- create()方法中的" book-db "是資料庫的名字,這裡隨意,不過需要注意的是 allowMainThreadQueries() 方法,這裡由於我們會用到單元測試,所以加上這行程式碼是為了防止 【 Cannot access database on the main thread since it may potentially lock the UI for a long period of time. 】 的報錯。正式使用時,請務必去掉這行程式碼,因為它會讓所有耗時操作執行在主執行緒!
到這裡,我們就可以先愉快的進行測試了.
測試
初級測試
找到 src 下的 test 目錄,然後可以像我這樣建立一個 RoomTest 類進行測試

image
說到這裡,可能會有童鞋尚未了解過單元測試,這時候你可以先去看看相關部落格,比如這篇
不過這裡使用的單元測試是 Android Studio 自帶的,也沒有用到太複雜的東西,同時我會做一些說明,不夠了解的童鞋也可以繼續往下看,看完你也就瞭解了
@RunWith(AndroidJUnit4::class) class RoomTest { private var bookDao: BookDao? = null private var bookDataBase: BookDataBase? = null @Before @Throws(Exception::class) fun setUp() { ShadowLog.stream = System.out//這樣方便列印日誌 val context = InstrumentationRegistry.getTargetContext() bookDataBase = BookDataBase.getInstance(context) bookDao = bookDataBase?.bookDao() } @Test fun insert() { val book1 = Book("時間簡史", "斯蒂芬·威廉·霍金", "科學") val book2 = Book("百年孤獨", "西亞·馬爾克斯", "文學") val list = bookDao?.insert(book1, book2) assert(list?.size == 2) } @Test fun query(){ val books = bookDao?.all for (book in books?: emptyList()) { Log.e(javaClass.name, "獲取的書籍資料: ${Gson().toJson(book)}") } } @After @Throws(Exception::class) fun cloaseDb() { bookDataBase?.close() } }
可以看到,這裡的單元測試使用的是 AndroidJUnit4 ,通過 @Before 註釋的方法,表示用於 相關資源的初始化 ,類似於Activity的onCreate()方法;而通過 @After 註釋的方法,則是用於 相關資源的銷燬 ,類似於Activity的onDestroy()方法。
剩下的,通過 @Test 註釋的方法就表示用於測試的單元,每個測試類裡面可以有多個測試單元,這裡目前只寫了插入和查詢兩個單元,在 RoomTest 類上通過右鍵執行,然後看一下結果:

image
在測試程式碼中的 **insert()單元 ** 裡,有這樣一行程式碼:
assert(list.size == 2)
而測試的結果是一片綠色,就表示這個斷言是正確的,list列表長度剛好為2,這裡為了驗證返回的list是整個資料庫長度還是僅僅表示此次進行插入操作的長度,我們修改一下 insert()測試單元:
@Test fun insert() { val book1 = Book("時間簡史", "斯蒂芬·威廉·霍金", "科學") val book2 = Book("百年孤獨", "西亞·馬爾克斯", "文學") val list = bookDao?.insert(book1, book2) assert(list?.size == 2) val list2 = bookDao?.insert(book1, book2) assert(list2?.size == 4) }
這時候在 insert()單元測試區域 右鍵執行,就只測試這一個單元,然後結果如下:

image
我們在 insert()單元 中進行了兩次插入操作,所以資料庫的總長度應該為 4 ,而這裡第39行的程式碼:
assert(list2?.size == 4)
返回的cede 是 -1,就表示實際上每次插入操作返回的列表長度應該為插入的數量,而非資料庫總量。其他操作亦是如此。
在單元測試中,我們的測試並不能直接用於正式的專案中,因為資料庫操作屬於耗時操作,所以一定不能把這些操作放在主執行緒裡,而最方便的執行緒切換,莫過於 Rxjava 啦!
現在開始使用 Rxjava 進行測試吧
結合Rxjava的測試
首先,要在專案中新增 Rxjava 的依賴:
//rxJava2+rxAndroid implementation "io.reactivex.rxjava2:rxjava:2.x.y" implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
在單元測試中,RxJava 如果做 IO執行緒 到 UI執行緒 的切換操作,結果是無法獲取的,所以需要將這些執行緒進行合併,方法如下:
@Before @Throws(Exception::class) fun setUp() { val context = InstrumentationRegistry.getTargetContext() bookDataBase = BookDataBase.getInstance(context) bookDao = bookDataBase?.bookDao() ShadowLog.stream = System.out initRxJava2() } private fun initRxJava2() { RxJavaPlugins.reset() RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } RxAndroidPlugins.reset() RxAndroidPlugins.setMainThreadSchedulerHandler { Schedulers.trampoline() } }
在 @Before註解 下的 setUp() 方法中進行RxJava的配置,然後我們可以把RxJava常用的執行緒切換寫在一個方法裡,方便複用:
private fun<T> doWithRxJava(t: T): Observable<T>{ return Observable.create<T>{it.onNext(t)} .subscribeOn(Schedulers.io()) .unsubscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) }
接著,對 insert單元 和 query單元 進行修改:
@Test fun insert() { val book1 = Book("時間簡史", "斯蒂芬·威廉·霍金", "科學") val book2 = Book("百年孤獨", "西亞·馬爾克斯", "文學") doWithRxJava(bookDao?.insert(book1, book2)) .subscribe ({ Log.e("insert長度:" , "${it?.size}") assert(it?.size == 2) },{ Log.e("insert出錯:" , "${it.stackTrace}-${it.message}") }) } @Test fun query(){ doWithRxJava(bookDao?.all) .subscribe({ for(book in it?: emptyList()){ Log.e(javaClass.name, "獲取的書籍資料: ${Gson().toJson(book)}") assert(it?.size == 2) } },{ Log.e("query出錯:" , "${it.stackTrace}-${it.message}") }) }
然後看一下測試的結果:

image
那麼, Room DataBase 的入門指南,就寫到這裡啦!
後續我可能會再寫一篇進階版的文章,涵蓋了真實使用的場景,然後看能不能寫一個簡單的Demo出來,這樣更方便學習吧!

image