Room是Jetpack中的ORM组件。Room可以简化SQLite数据库操作。Room包含3个主要的组件:
Entity。Entity是实体类,代表数据库里的一张表。
DAO。DAO提供了访问数据库的接口,返回Entity或Entity集合。
Database。Database是Entity和DAO的集合,代表一个SQLite数据库。Database是我们访问DAO和Entity的入口。
Entity
Entity是实体类,代表一个数据表。我们首先看一个简单的例子:
@Entity(tableName="users")
data class User (
@PrimaryKey
var uid: Int,
@ColumnInfo(name = "first_name")
var firstName: String?,
@ColumnInfo(name = "last_name")
var lastName: String?
@Ignore
var picture: Bitmap?
)
注解
说明
@Entity
声明实体类。
@PrimaryKey
声明主键。
@ColumnInfo
声明字段在数据表中的属性。
@Ignore
禁止将字段映射到数据表。
Room要求实体类必须拥有主键,且主键必须是Int或Long型。@ColumnInfo声明了列名和域名的对照关系。如果列名和域名相同,可以省略这个注解。上面这个Entity对应的SQL模式就是:
CREATE TABLE users (
INT uid PRIMARY KEY,
TEXT first_name,
TEXT last_name
);
可以看到,从Entity到SQL的映射是非常直观的。
实体类的域可以拥有默认值。实体类除了作为数据容器之外,也可以具有行为。参考下面的例子:
@Entity(tableName = "plants")
data class Plant(
@PrimaryKey @ColumnInfo(name = "id")
val plantId: String,
val name: String,
val description: String,
val growZoneNumber: Int,
val wateringInterval: Int = 7,
val imageUrl: String = ""
) {
fun shouldBeWatered(since: Calendar, lastWateringDate: Calendar) =
since > lastWateringDate.apply { add(DAY_OF_YEAR, wateringInterval) }
override fun toString() = name
}
Entity告诉Room如何在Java对象和SQL记录之间进行转换。然而要从SQLite数据库中得到Java对象,我们还需要Dao。
Dao
还是从例子入手。
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List
@Query("SELECT * FROM user WHERE first_name LIKE :first AND last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
suspend fun insertAll(vararg users: User)
@Delete
suspend fun delete(user: User)
}
注解
说明
@Dao
声明接口是DAO。
@Query
将SQL查询语句映射为Java方法。
@Insert
将SQL插入语句映射为Java方法。
@Delete
将SQL删除语句映射为Java方法。
从例子里可以看出,DAO和Entity有两个区别,首先Entity是类,而DAO是接口。其次,Entity将Java对象映射为SQL记录,将域映射为数据表中的列;DAO将SQL语句映射为Java方法。我们不需要手动编写这些方法,Room会自动生成它们。
数据库查询会引发磁盘IO,这是一个耗时操作。为了避免ANR,需要将数据库查询放到后台线程里执行。很多时候我们需要根据查询结果来更新界面,而界面必须在主线程中修改。那么如何将后台线程查询出的数据传递给主线程呢?你可以自己编写Handler,更简单的办法是让查询方法返回LiveData。
@Dao
interface PlantDao {
@Query("SELECT * FROM plants ORDER BY name")
fun getPlants(): LiveData>
@Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): LiveData>
@Query("SELECT * FROM plants WHERE id = :plantId")
fun getPlant(plantId: String): LiveData
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(plants: List)
}
这里简单介绍一下LiveData。LiveData是一个为更新界面而定制的Observable,它将后台线程的数据投递到主线程。为了避免过度渲染,LiveData只在Activity或Fragment活跃的时候才投递数据。
如果使用kotlin进行开发,可以将DAO方法声明为suspend,配合viewModelScope使用。
Database
Database是Entity和DAO的集合,也是访问Entity和DAO的入口。Database是一个抽象类,每个DAO由一个抽闲方法返回。
@Database(entities = [User::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}
此外Entity必须在@Database中进行注册。
实际的Database类也是由Room生成的。通过Room.databaseBuilder可以构造Database类。
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"database-name"
).build()
综合起来,Room的用法可以总结为:
用Entity封装数据记录,用DAO映射查询语句。
通过databaseBuilder得到Database,通过Database得到DAO,通过DAO管理Entity。
Room插件
Room会自动生成类,这个动作是在编译期完成的,因为我们需要引入编译插件。
apply plugin: 'kotlin-kapt'
dependencies {
def room_version = "2.1.0-alpha04"
kapt "android.arch.persistence.room:compiler:$room_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0-alpha'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0-alpha'
implementation 'androidx.room:room-runtime:2.1.0-alpha06'
kapt 'androidx.room:room-compiler:2.1.0-alpha06'
implementation 'androidx.room:room-ktx:2.1.0-alpha06'
}
常见问题
DatabaseBuilder的callback未被调用
Room底层使用了SQLiteOpenHelper,只有当数据库被实际使用时,数据库才会被建立,回调函数才被调用。如果要手动调用callback,可以执行
// and then
db.beginTransaction()
db.endTransaction()
// or query a dummy select statement
db.query("select 1", null)
return db
Room检查表结构的方法
Room的createFromAsset使用PRAGMA tableinfo('tbl')来得到表的结构,并生成TableInfo实例。将这个实例和由Entity类生成的TableInfo进行比对,如果不一致,抛出IllegalStateException异常。
"Migration didn't properly handle XXX
tableinfo为每个列生成一行,记录了列的编号、名字、数据类型、是否可为NULL、默认值、列在主键中的顺序。