Room Kotlin API的使用入門教程

Room 是 SQLite 的封裝,它使 Android 對數據庫的操作變得非常簡單,也是迄今為止我最喜歡的 Jetpack 庫。在本文中我會告訴大傢如何使用並且測試 Room Kotlin API,同時在介紹過程中,我也會為大傢分享其工作原理。

我們將基於 Room with a view codelab 為大傢講解。這裡我們會創建一個存儲在數據庫的詞匯表,然後將它們顯示到屏幕上,同時用戶還可以向列表中添加單詞。

定義數據庫表

在我們的數據庫中僅有一個表,就是保存詞匯的表。Word 類代表表中的一條記錄,並且它需要使用註解 @Entity。我們使用 @PrimaryKey 註解為表定義主鍵。然後,Room 會生成一個 SQLite 表,表名和類名相同。每個類的成員對應表中的列。列名和類型與類中每個字段的名稱和類型一致。如果您希望改變列名而不使用類中的變量名稱作為列名,可以通過 @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 註解,因為它可以使您更靈活地對成員進行重命名而無需同時修改數據庫的列名。因為修改列名會涉及到修改數據庫模式,因而您需要實現數據遷移。

訪問表中的數據

如需訪問表中的數據,需要創建一個數據訪問對象 (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 相關的內容。

插入數據

要實現插入數據的操作,首先創建一個抽象的掛起函數,需要插入的單詞作為它的參數,並且添加 @Insert 註解。Room 會生成將數據插入數據庫的全部操作,並且由於我們將函數定義為可掛起,所以 Room 會將整個操作過程放在後臺線程中完成。因此,該掛起函數是主線程安全的,也就是在主線程可以放心調用而不必擔心阻塞主線程。

@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

為瞭測試 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。

以上就是Room Kotlin API使用入門教程的詳細內容,更多關於Room Kotlin API使用的資料請關註WalkonNet其它相關文章!

推薦閱讀:

    None Found