上手使用 Room Kotlin API

image

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剥悟。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灵寺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子懦胞,更是在濱河造成了極大的恐慌替久,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躏尉,死亡現(xiàn)場離奇詭異蚯根,居然都是意外死亡,警方通過查閱死者的電腦和手機胀糜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門颅拦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人教藻,你說我怎么就攤上這事距帅。” “怎么了括堤?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵碌秸,是天一觀的道長。 經(jīng)常有香客問我悄窃,道長讥电,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任轧抗,我火速辦了婚禮恩敌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘横媚。我一直安慰自己纠炮,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布灯蝴。 她就那樣靜靜地躺著恢口,像睡著了一般。 火紅的嫁衣襯著肌膚如雪穷躁。 梳的紋絲不亂的頭發(fā)上耕肩,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音,去河邊找鬼看疗。 笑死,一個胖子當著我的面吹牛睦授,可吹牛的內(nèi)容都是我干的两芳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼去枷,長吁一口氣:“原來是場噩夢啊……” “哼怖辆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起删顶,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竖螃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逗余,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體特咆,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年录粱,在試婚紗的時候發(fā)現(xiàn)自己被綠了腻格。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡啥繁,死狀恐怖菜职,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旗闽,我是刑警寧澤酬核,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站适室,受9級特大地震影響嫡意,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜亭病,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一鹅很、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罪帖,春花似錦促煮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坐昙,卻和暖如春绳匀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工疾棵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戈钢,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓是尔,卻偏偏與公主長得像殉了,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拟枚,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容