Room 是 SQLite 的封裝赴涵,它使 Android 對數(shù)據(jù)庫的操作變得非常簡單慌盯,也是迄今為止我最喜歡的 Jetpack 庫。在本文中我會告訴大家如何使用并且測試 Room Kotlin API躏结,同時在介紹過程中,我也會為大家分享其工作原理。
我們將基于 Room with a view codelab 為大家講解蛉艾。這里我們會創(chuàng)建一個存儲在數(shù)據(jù)庫的詞匯表停做,然后將它們顯示到屏幕上晤愧,同時用戶還可以向列表中添加單詞。
定義數(shù)據(jù)庫表
在我們的數(shù)據(jù)庫中僅有一個表蛉腌,就是保存詞匯的表官份。Word 類代表表中的一條記錄只厘,并且它需要使用注解 @Entity。我們使用 @PrimaryKey 注解為表定義主鍵舅巷。然后羔味,Room 會生成一個 SQLite 表,表名和類名相同钠右。每個類的成員對應(yīng)表中的列赋元。列名和類型與類中每個字段的名稱和類型一致。如果您希望改變列名而不使用類中的變量名稱作為列名飒房,可以通過 @ColumnInfo 注解來修改搁凸。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
我們推薦大家使用 @ColumnInfo
注解,因為它可以使您更靈活地對成員進行重命名而無需同時修改數(shù)據(jù)庫的列名狠毯。因為修改列名會涉及到修改數(shù)據(jù)庫模式护糖,因而您需要實現(xiàn)數(shù)據(jù)遷移。
訪問表中的數(shù)據(jù)
如需訪問表中的數(shù)據(jù)垃你,需要創(chuàng)建一個數(shù)據(jù)訪問對象 (DAO)椅文。也就是一個叫做 WorkDao 的接口,它會帶有 @Dao 注解惜颇。我們希望通過它實現(xiàn)表級別的數(shù)據(jù)插入皆刺、刪除和獲取,所以數(shù)據(jù)訪問對象中會定義相應(yīng)的抽象方法凌摄。操作數(shù)據(jù)庫屬于比較耗時的 I/O 操作羡蛾,所以需要在后臺線程中完成。我們將把 Room 與 Kotlin 協(xié)程和 Flow 相結(jié)合來實現(xiàn)上述功能锨亏。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Dao
interface WordDao {
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
}
我們在視頻 Kotlin Vocabulary 中介紹了 協(xié)程的相關(guān)基本概念痴怨,
在 Kotlin Vocabulary 另一個視頻中則介紹了 Flow 相關(guān)的內(nèi)容。
插入數(shù)據(jù)
要實現(xiàn)插入數(shù)據(jù)的操作器予,首先創(chuàng)建一個抽象的掛起函數(shù)浪藻,需要插入的單詞作為它的參數(shù),并且添加 @Insert 注解乾翔。Room 會生成將數(shù)據(jù)插入數(shù)據(jù)庫的全部操作爱葵,并且由于我們將函數(shù)定義為可掛起,所以 Room 會將整個操作過程放在后臺線程中完成反浓。因此萌丈,該掛起函數(shù)是主線程安全的,也就是在主線程可以放心調(diào)用而不必擔(dān)心阻塞主線程雷则。
@Insert
suspend fun insert(word: Word)
在底層 Room 生成了 Dao 抽象函數(shù)的實現(xiàn)代碼辆雾。下面代碼片段就是我們的數(shù)據(jù)插入方法的具體實現(xiàn):
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Override
public Object insert(final Word word, final Continuation<? super Unit> p1) {
return CoroutinesRoom.execute(__db, true, new Callable<Unit>() {
@Override
public Unit call() throws Exception {
__db.beginTransaction();
try {
__insertionAdapterOfWord.insert(word);
__db.setTransactionSuccessful();
return Unit.INSTANCE;
} finally {
__db.endTransaction();
}
}
}, p1);
}
CoroutinesRoom.execute()
函數(shù)被調(diào)用,里面包含三個參數(shù): 數(shù)據(jù)庫月劈、一個用于表示是否正處于事務(wù)中的標識度迂、一個 Callable
對象藤乙。Callable.call()
包含處理數(shù)據(jù)庫插入數(shù)據(jù)操作的代碼。
如果我們看一下 CoroutinesRoom.execute()
的 實現(xiàn)英岭,我們會看到 Room 將 callable.call() 移動到另外一個 CoroutineContext湾盒。該對象來自構(gòu)建數(shù)據(jù)庫時您所提供的執(zhí)行器,或者默認使用 Architecture Components IO Executor诅妹。
查詢數(shù)據(jù)
為了能夠查詢表數(shù)據(jù)罚勾,我們這里創(chuàng)建一個抽象函數(shù),并且為其添加 @Query 注解吭狡,注解后緊跟 SQL 請求語句: 該語句從單詞數(shù)據(jù)表中請求全部單詞尖殃,并且以字母順序排序。
我們希望當數(shù)據(jù)庫中的數(shù)據(jù)發(fā)生改變的時候划煮,能夠得到相應(yīng)的通知送丰,所以我們返回一個 Flow<List<Word>>
。由于返回類型是 Flow弛秋,Room 會在后臺線程中執(zhí)行數(shù)據(jù)請求器躏。
@Query(“SELECT * FROM word_table ORDER BY word ASC”)
fun getAlphabetizedWords(): Flow<List<Word>>
在底層,Room 生成了 getAlphabetizedWords():
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Override
public Flow<List<Word>> getAlphabetizedWords() {
final String _sql = "SELECT * FROM word_table ORDER BY word ASC";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
return CoroutinesRoom.createFlow(__db, false, new String[]{"word_table"}, new Callable<List<Word>>() {
@Override
public List<Word> call() throws Exception {
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfWord = CursorUtil.getColumnIndexOrThrow(_cursor, "word");
final List<Word> _result = new ArrayList<Word>(_cursor.getCount());
while(_cursor.moveToNext()) {
final Word _item;
final String _tmpWord;
_tmpWord = _cursor.getString(_cursorIndexOfWord);
_item = new Word(_tmpWord);
_result.add(_item);
}
return _result;
} finally {
_cursor.close();
}
}
@Override
protected void finalize() {
_statement.release();
}
});
}
我們可以看到代碼里調(diào)用了 CoroutinesRoom.createFlow()
蟹略,它包含四個參數(shù): 數(shù)據(jù)庫登失、一個用于標識我們是否正處于事務(wù)中的變量、一個需要監(jiān)聽的數(shù)據(jù)庫表的列表 (在本例中列表里只有 word_table) 以及一個 Callable 對象挖炬。Callable.call() 包含需要被觸發(fā)的查詢的實現(xiàn)代碼揽浙。
如果我們看一下 CoroutinesRoom.createFlow() 的 實現(xiàn)代碼,會發(fā)現(xiàn)這里同數(shù)據(jù)請求調(diào)用一樣使用了不同的 CoroutineContext
意敛。同數(shù)據(jù)插入調(diào)用一樣馅巷,這里的分發(fā)器來自構(gòu)建數(shù)據(jù)庫時您所提供的執(zhí)行器,或者來自默認使用的 Architecture Components IO
執(zhí)行器草姻。
創(chuàng)建數(shù)據(jù)庫
我們已經(jīng)定義了存儲在數(shù)據(jù)庫中的數(shù)據(jù)以及如何訪問他們钓猬,現(xiàn)在我們來定義數(shù)據(jù)庫。要創(chuàng)建數(shù)據(jù)庫撩独,我們需要創(chuàng)建一個抽象類敞曹,它繼承自 RoomDatabase
,并且添加 @Database
注解跌榔。將 Word 作為需要存儲的實體元素傳入,數(shù)值 1 作為數(shù)據(jù)庫版本捶障。
我們還會定義一個抽象方法僧须,該方法返回一個 WordDao
對象。所有這些都是抽象類型的项炼,因為 Room 會幫我們生成所有的實現(xiàn)代碼担平。就像這里示绊,有很多邏輯代碼無需我們親自實現(xiàn)。
最后一步就是構(gòu)建數(shù)據(jù)庫暂论。我們希望能夠確保不會有多個同時打開的數(shù)據(jù)庫實例面褐,而且還需要應(yīng)用的上下文來初始化數(shù)據(jù)庫。一種實現(xiàn)方法是在類中添加伴生對象取胎,并且在其中定義一個 RoomDatabase 實例展哭,然后在類中添加 getDatabase 函數(shù)來構(gòu)建數(shù)據(jù)庫。如果我們希望 Room 查詢不是在 Room 自身創(chuàng)建的 IO Executor 中執(zhí)行闻蛀,而是在另外的 Executor 中執(zhí)行匪傍,我們需要通過調(diào)用 setQueryExecutor() 將新的 Executor 傳入 builder。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
// 返回實例
instance
}
}
}
測試 Dao
為了測試 Dao觉痛,我們需要實現(xiàn) AndroidJUnit 測試來讓 Room 在設(shè)備上創(chuàng)建 SQLite 數(shù)據(jù)庫役衡。
當實現(xiàn) Dao 測試的時候,在每個測試運行之前薪棒,我們創(chuàng)建數(shù)據(jù)庫手蝎。當每個測試運行后,我們關(guān)閉數(shù)據(jù)庫俐芯。由于我們并不需要在設(shè)備上存儲數(shù)據(jù)棵介,當創(chuàng)建數(shù)據(jù)庫的時候,我們可以使用內(nèi)存數(shù)據(jù)庫泼各。也因為這僅僅是個測試鞍时,我們可以在主線程中運行請求。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@RunWith(AndroidJUnit4::class)
class WordDaoTest {
private lateinit var wordDao: WordDao
private lateinit var db: WordRoomDatabase
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
// 由于當進程結(jié)束的時候會清除這里的數(shù)據(jù)扣蜻,所以使用內(nèi)存數(shù)據(jù)庫
db = Room.inMemoryDatabaseBuilder(context, WordRoomDatabase::class.java)
// 可以在主線程中發(fā)起請求逆巍,僅用于測試。
.allowMainThreadQueries()
.build()
wordDao = db.wordDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
...
}
要測試單詞是否能夠被正確添加到數(shù)據(jù)庫莽使,我們會創(chuàng)建一個 Word 實例锐极,然后插入數(shù)據(jù)庫,然后按照字母順序找到單詞列表中的第一個芳肌,然后確保它和我們創(chuàng)建的單詞是一致的灵再。由于我們調(diào)用的是掛起函數(shù),所以我們會在 runBlocking 代碼塊中運行測試亿笤。因為這里僅僅是測試翎迁,所以我們無需關(guān)心測試過程是否會阻塞測試線程。
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Test
@Throws(Exception::class)
fun insertAndGetWord() = runBlocking {
val word = Word("word")
wordDao.insert(word)
val allWords = wordDao.getAlphabetizedWords().first()
assertEquals(allWords[0].word, word.word)
}
除了本文所介紹的功能净薛,Room 提供了非常多的功能性和靈活性汪榔,遠遠超出本文所涵蓋的范圍。比如您可以指定 Room 如何處理數(shù)據(jù)庫沖突肃拜、可以通過創(chuàng)建 TypeConverters 存儲原生 SQLite 無法存儲的數(shù)據(jù)類型 (比如 Date 類型)痴腌、可以使用 JOIN 以及其它 SQL 功能實現(xiàn)復(fù)雜的查詢雌团、創(chuàng)建數(shù)據(jù)庫視圖、預(yù)填充數(shù)據(jù)庫以及當數(shù)據(jù)庫被創(chuàng)建或打開的時候觸發(fā)特定動作士聪。
更多相關(guān)信息請查閱我們的 Room 官方文檔锦援,如果想通過實踐學(xué)習(xí),可以訪問 Room with a view codelab剥悟。