Room 是 SQLite 的封裝,它使 Android 對數據庫的操做變得很是簡單,也是迄今爲止我最喜歡的 Jetpack 庫。在本文中我會告訴你們如何使用而且測試 Room Kotlin API,同時在介紹過程當中,我也會爲你們分享其工做原理。java
咱們將基於 Room with a view codelab 爲你們講解。這裏咱們會建立一個存儲在數據庫的詞彙表,而後將它們顯示到屏幕上,同時用戶還能夠向列表中添加單詞。android
在咱們的數據庫中僅有一個表,就是保存詞彙的表。Word 類表明表中的一條記錄,而且它須要使用註解 @Entity。咱們使用 @PrimaryKey 註解爲表定義主鍵。而後,Room 會生成一個 SQLite 表,表名和類名相同。每一個類的成員對應表中的列。列名和類型與類中每一個字段的名稱和類型一致。若是您但願改變列名而不使用類中的變量名稱做爲列名,能夠經過 @ColumnInfo 註解來修改。sql
/* 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
註解,由於它可使您更靈活地對成員進行重命名而無需同時修改數據庫的列名。由於修改列名會涉及到修改數據庫模式,於是您須要實現數據遷移。數據庫
如需訪問表中的數據,須要建立一個數據訪問對象 (DAO)。也就是一個叫作 WorkDao 的接口,它會帶有 @Dao 註解。咱們但願經過它實現表級別的數據插入、刪除和獲取,因此數據訪問對象中會定義相應的抽象方法。操做數據庫屬於比較耗時的 I/O 操做,因此須要在後臺線程中完成。咱們將把 Room 與 Kotlin 協程和 Flow 相結合來實現上述功能。安全
/* 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 中介紹了 協程的相關基本概念,
在 Kotlin Vocabulary 另外一個視頻中則介紹了 Flow 相關的內容。app
要實現插入數據的操做,首先建立一個抽象的掛起函數,須要插入的單詞做爲它的參數,而且添加 @Insert 註解。Room 會生成將數據插入數據庫的所有操做,而且因爲咱們將函數定義爲可掛起,因此 Room 會將整個操做過程放在後臺線程中完成。所以,該掛起函數是主線程安全的,也就是在主線程能夠放心調用而沒必要擔憂阻塞主線程。ide
@Insert suspend fun insert(word: Word)
在底層 Room 生成了 Dao 抽象函數的實現代碼。下面代碼片斷就是咱們的數據插入方法的具體實現:函數
/* 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()
函數被調用,裏面包含三個參數: 數據庫、一個用於表示是否正處於事務中的標識、一個 Callable
對象。Callable.call()
包含處理數據庫插入數據操做的代碼。學習
若是咱們看一下 CoroutinesRoom.execute()
的 實現,咱們會看到 Room 將 callable.call() 移動到另一個 CoroutineContext。該對象來自構建數據庫時您所提供的執行器,或者默認使用 Architecture Components IO Executor。測試
爲了可以查詢表數據,咱們這裏建立一個抽象函數,而且爲其添加 @Query 註解,註解後緊跟 SQL 請求語句: 該語句從單詞數據表中請求所有單詞,而且以字母順序排序。
咱們但願當數據庫中的數據發生改變的時候,可以獲得相應的通知,因此咱們返回一個 Flow<List<Word>>
。因爲返回類型是 Flow,Room 會在後臺線程中執行數據請求。
@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(); } }); }
咱們能夠看到代碼裏調用了 CoroutinesRoom.createFlow()
,它包含四個參數: 數據庫、一個用於標識咱們是否正處於事務中的變量、一個須要監聽的數據庫表的列表 (在本例中列表裏只有 word_table) 以及一個 Callable 對象。Callable.call() 包含須要被觸發的查詢的實現代碼。
若是咱們看一下 CoroutinesRoom.createFlow() 的 實現代碼,會發現這裏同數據請求調用同樣使用了不一樣的 CoroutineContext
。同數據插入調用同樣,這裏的分發器來自構建數據庫時您所提供的執行器,或者來自默認使用的 Architecture Components IO
執行器。
咱們已經定義了存儲在數據庫中的數據以及如何訪問他們,如今咱們來定義數據庫。要建立數據庫,咱們須要建立一個抽象類,它繼承自 RoomDatabase
,而且添加 @Database
註解。將 Word 做爲須要存儲的實體元素傳入,數值 1 做爲數據庫版本。
咱們還會定義一個抽象方法,該方法返回一個 WordDao
對象。全部這些都是抽象類型的,由於 Room 會幫咱們生成全部的實現代碼。就像這裏,有不少邏輯代碼無需咱們親自實現。
最後一步就是構建數據庫。咱們但願可以確保不會有多個同時打開的數據庫實例,並且還須要應用的上下文來初始化數據庫。一種實現方法是在類中添加伴生對象,而且在其中定義一個 RoomDatabase 實例,而後在類中添加 getDatabase 函數來構建數據庫。若是咱們但願 Room 查詢不是在 Room 自身建立的 IO Executor 中執行,而是在另外的 Executor 中執行,咱們須要經過調用 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,咱們須要實現 AndroidJUnit 測試來讓 Room 在設備上建立 SQLite 數據庫。
當實現 Dao 測試的時候,在每一個測試運行以前,咱們建立數據庫。當每一個測試運行後,咱們關閉數據庫。因爲咱們並不須要在設備上存儲數據,當建立數據庫的時候,咱們可使用內存數據庫。也由於這僅僅是個測試,咱們能夠在主線程中運行請求。
/* 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() // 因爲當進程結束的時候會清除這裏的數據,因此使用內存數據庫 db = Room.inMemoryDatabaseBuilder(context, WordRoomDatabase::class.java) // 能夠在主線程中發起請求,僅用於測試。 .allowMainThreadQueries() .build() wordDao = db.wordDao() } @After @Throws(IOException::class) fun closeDb() { db.close() } ... }
要測試單詞是否可以被正確添加到數據庫,咱們會建立一個 Word 實例,而後插入數據庫,而後按照字母順序找到單詞列表中的第一個,而後確保它和咱們建立的單詞是一致的。因爲咱們調用的是掛起函數,因此咱們會在 runBlocking 代碼塊中運行測試。由於這裏僅僅是測試,因此咱們無需關心測試過程是否會阻塞測試線程。
/* 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 如何處理數據庫衝突、能夠經過建立 TypeConverters 存儲原生 SQLite 沒法存儲的數據類型 (好比 Date 類型)、可使用 JOIN 以及其它 SQL 功能實現複雜的查詢、建立數據庫視圖、預填充數據庫以及當數據庫被建立或打開的時候觸發特定動做。
更多相關信息請查閱咱們的 Room 官方文檔,若是想經過實踐學習,能夠訪問 Room with a view codelab。