http://tommwq.tech/blog/room%e4%bd%bf%e7%94%a8%e7%ae%80%e4%bb%8b/
- 1. Entity
- 2. Dao
- 3. Database
- 4. Room插件
- 5. 常见问题
- 5.1. DatabaseBuilder的callback未被调用
- 5.2. Room检查表结构的方法
Room是Jetpack中的ORM组件。Room可以简化SQLite数据库操作。Room包含3个主要的组件:
- Entity。Entity是实体类,代表数据库里的一张表。
- DAO。DAO提供了访问数据库的接口,返回Entity或Entity集合。
- Database。Database是Entity和DAO的集合,代表一个SQLite数据库。Database是我们访问DAO和Entity的入口。
1 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。
2 Dao
还是从例子入手。
@Dao interface UserDao { @Query("SELECT * FROM user") fun getAll(): List<User> @Query("SELECT * FROM user WHERE uid IN (:userIds)") fun loadAllByIds(userIds: IntArray): List<User> @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<List<Plant>> @Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name") fun getPlantsWithGrowZoneNumber(growZoneNumber: Int): LiveData<List<Plant>> @Query("SELECT * FROM plants WHERE id = :plantId") fun getPlant(plantId: String): LiveData<Plant> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(plants: List<Plant>) }
这里简单介绍一下LiveData。LiveData是一个为更新界面而定制的Observable,它将后台线程的数据投递到主线程。为了避免过度渲染,LiveData只在Activity或Fragment活跃的时候才投递数据。
如果使用kotlin进行开发,可以将DAO方法声明为suspend,配合viewModelScope使用。
3 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。
4 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' }
5 常见问题
5.1 DatabaseBuilder的callback未被调用
Room底层使用了SQLiteOpenHelper,只有当数据库被实际使用时,数据库才会被建立,回调函数才被调用。如果要手动调用callback,可以执行
// and then db.beginTransaction() db.endTransaction() // or query a dummy select statement db.query("select 1", null) return db
5.2 Room检查表结构的方法
Room的createFromAsset使用PRAGMA tableinfo('tbl')来得到表的结构,并生成TableInfo实例。将这个实例和由Entity类生成的TableInfo进行比对,如果不一致,抛出IllegalStateException异常。
"Migration didn't properly handle XXX
tableinfo为每个列生成一行,记录了列的编号、名字、数据类型、是否可为NULL、默认值、列在主键中的顺序。