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 */@Daointerface 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 会将整个操作过程放在后盾线程中实现。因而,该挂起函数是主线程平安的,也就是在主线程能够释怀调用而不用放心阻塞主线程。

@Insertsuspend fun insert(word: Word)

在底层 Room 生成了 Dao 形象函数的实现代码。上面代码片段就是咱们的数据插入方法的具体实现:

/* Copyright 2020 Google LLC.     SPDX-License-Identifier: Apache-2.0 */@Overridepublic 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 */@Overridepublic 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。