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。