Room of Jetpack's Architecture Components

Room也是一个安卓rom框架,但作为Android的亲儿子,并且原生支持LiveDataRxjava嵌套使用。配合上其他谷歌组件应该会更香吧

Room 主要包含三个组件:

  • Database: 包含数据库持有者,作为与应用持久化相关数据的底层连接的主要接入点。这个类需要用 @Database

    注解,并满足下面条件:

    • 必须是继承 RoomDatabase 的抽象类
    • 注解中包含该数据库相关的实体类列表
    • 包含的抽象方法不能有参数,且返回值必须是被 @Dao 注解的类
  • Entity: 表示了数据库中的一张表

  • DAO: 包含了访问数据库的一系列方法

基本使用

User

@Entity
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

UserDao

@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
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
}

AppDatabase

@Database(entities = arrayOf(User::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

然后将使用下面代码获得创建的数据库的实例:

val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

定义 Entity

为了让 Room 可以访问 entity,entity 中的字段必须是 public 的,或者提供了getter/setter方法。默认情况下,Room 会将 entity 中的每个字段作为数据库表中一列,如果你不想持久化某个字段,可以使用 @Ignore 注解。默认数据库表名为 entity 类名,你可以通过 @Entity 注解的 tableName 属性 更改,默认列名是字段名,你可以通过 @ColumnInfo 注解更改。

主键

每个 entity 必须至少有一个字段作为主键(primary key),即使该 entity 只有一个字段。使用 @PrimaryKey 注解来指定主键,如果你希望 SQLite 帮你自动生成这个唯一主键,需要将 @PrimaryKeyautoGenerate 属性设置成 true,不过需要改列是 INTEGER 类型的。如果字段类型是 longintInsert 方法会将 0 作为缺省值,如果字段类型是 IntegerLong 类型,Insert 方法会将 null 作为缺省值。
如果 entity 的主键是复合主键(composite primary key),你就需要使用 @Entity 注解的 primaryKeys 属性定义这个约束,如:

@Entity(tableName = "users")
data class User (
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

索引

有些时候,我们需要添加索引以加快查询速度,可以使用 @Entity 注解的 indices 属性创建索引,如果某个字段或字段组是唯一的,可以将 @Index 注解的 unique 属性设置为 true 来强制这个唯一性,如:

@Entity(indices = arrayOf(Index(value = ["first_name", "last_name"],
        unique = true)))
data class User(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?,
    @Ignore var picture: Bitmap?
)

关系

从数据库到各个对象模型的映射关系是一种常见的做法,并且在服务器端效果很好。 即使程序在访问字段时加载它们,服务器仍然可以正常运行。但是,在客户端,这种类型的延迟加载是不可行的,因为它通常发生在UI线程上,并且在UI线程中的磁盘上查询信息会造成严重的性能问题。 UI线程通常需要大约16毫秒来计算和绘制活动的更新版式,因此,即使查询仅花费5毫秒,您的应用仍有可能会用完时间来绘制框架,从而引起明显的视觉故障。如果有并行的单独事务在运行,或者设备正在运行其他磁盘密集型任务,则查询可能需要花费更多时间才能完成。但是,如果您不使用延迟加载,则您的应用程序会获取比所需更多的数据,从而造成内存消耗问题。。
例如,UI 加载了 Book 对象列表,每个 book 都有一个 Author 对象,你可能最开始想采用懒加载的方式获取 Book实例(使用getAuthor() 方法获取 author),第一次调用 getAuthor() 会调用数据库查询。过一会,你意识到你需要在 UI 上显示作者名,你写了下面这样的代码:

    authorNameTextView.setText(user.getAuthor().getName());

这看似正常的变更会导致 Author 表在主线程中被查询。那提前查询好作者信息是不是就行了呢?明显不行,如果你不再需要这些数据,就很难改变数据的加载方式了。例如,如果你的 UI 不再需要显示作者信息了,你的应用仍然会加载这些不需要的数据,从而浪费昂贵的内存空间,如果 Author 又引用了其他表,那么应用的效率将会进一步降低。
所以为了让 Room 能同时引用多个 entity,你需要创建一个包含每个 entity 的 POJO,然后编写一个连接相应表的查询。这个结构良好的 model,结合 Room 健壮的查询校验功能,就能够让你的应用花费更少的资源加载数据,提升应用的性能和用户体验。
虽然不能直接指定对象间关系,但可以指定外键(Foreign Key)约束。例如对于 Book entity 有一个作者的外键引用 User,可以通过 @ForeignKey 注解指定这个外键约束:

@Entity(foreignKeys = arrayOf(ForeignKey(
            entity = User::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("user_id"))
       )
)
data class Book(
    @PrimaryKey val bookId: Int,
    val title: String?,
    @ColumnInfo(name = "user_id") val userId: Int
)

可以通过 @ForeignKey注解的 onDeleteonUpdate 属性指定级联操作,如级联更新和级联删除:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id",
                                  onUpdate = ForeignKey.CASCADE,
                                  onDelete = ForeignKey.CASCADE))

有时,一个包含嵌套对象的 entity 或 POJO 表示一个完整的数据库逻辑,可以使用 @Embedded 注解将该嵌套对象的字段分解到该表中,如 User 表需要包含 Address相关字段,可以使用 @Embedded 注解表示这是个组合列:

data class Address(
    val street: String?,
    val state: String?,
    val city: String?,
    @ColumnInfo(name = "post_code") val postCode: Int
)

@Entity
data class User(
    @PrimaryKey val id: Int,
    val firstName: String?,
    @Embedded val address: Address?
)

也就是说, User 表包含 idfirstNamestreetstatecity,和 post_code 列。
Embedded 字段也能包含其他 Embedded 字段。
如果有另一个组合列也是 Address 类型的,可以使用 @Embedded 注解的 prefix 属性添加列名前缀以保证列的唯一性。

使用 DAO

DAO(data access objects)是应用中操作数据库的最直接的接口,应用中对数据库的操作都表现在这个对象上,也就是说,应用不需要知道具体的数据库操作方法,只需要利用 DAO 完成数据库操作就行了,所以这一系列 Dao 对象也构成了 Room 的核心组件。DAO 可以是个接口,也可以是个抽象类,如果是个抽象类,那么它可以有个构造器,以 RoomDatabase 作为唯一参数,Room 会在编译时自动生成每个 DAO 的实现类。

Insert

定义一个用 @Insert 注解的 DAO 方法,Room 会自动生成一个在单个事务中将所有参数插入数据库的实现,如果方法只有一个参数,那么它可以返回 long 类型的 rowId,如果方法参数是数组或集合,那么它可以返回 long[]List:

@Dao
interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1: User, user2: User)

    @Insert
    fun insertUsersAndFriends(user: User, friends: List<User>)
}

Update

@Update 注解的方法可以更改一系列给定的 entity, 使用匹配的主键去查询更改这些 entity,可以返回 int 型的数据库更新行数:

@Dao
interface MyDao {
    @Update
    fun updateUsers(vararg users: User)
}

Delete

@Delete 注解的方法可以删除一系列给定的 entity, 使用匹配的主键去查询更改这些 entity,可以返回 int 型的数据库删除行数:

@Dao
interface MyDao {
    @Delete
    fun deleteUsers(vararg users: User)
}

Query

@Query 注解的方法可以让你方便地读写数据库,Room 会在编译时验证这个方法,所以如果查询有问题编译时就会报错。Room 还会验证查询的返回值,如果查询响应的字段名和返回对象的字段名不匹配,如果有些字段不匹配,你会看到警告,如果所有字段都不匹配,你会看到 error。下面是一个简单的查询,查询所有的用户:

@Dao
interface MyDao {
    @Query("SELECT * FROM user")
    fun loadAllUsers(): Array<User>
}

如果你想要添加查询条件,可以使用 :参数名 的方式获取参数值:

@Dao
interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    fun findUserWithName(search: String): List<User>
}

当然,查询条件集合也是支持的:

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegions(regions: List<String>): List<NameTuple>
}
复制代码

很多时候,我们不需要查询表中的所有字段,我们只用到了 UI 用到的那几列,为了节省资源,也为了加快查询速度,我们就可以定义一个包含用到的字段的 POJO(这个 POJO 可以使用 @Embedded 注解) ,查询方法可以使用这个 POJO:

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

Room 也允许你方便地进行多表查询,如查询某个用户所借的所有书籍信息:

@Dao
interface MyDao {
    @Query(
        "SELECT * FROM book " +
        "INNER JOIN loan ON loan.book_id = book.id " +
        "INNER JOIN user ON user.id = loan.user_id " +
        "WHERE user.name LIKE :userName"
    )
    fun findBooksBorrowedByNameSync(userName: String): List<Book>
}

多表查询也能使用 POJO,如查询用户名和他的宠物名:

@Dao
interface MyDao {
    @Query(
        "SELECT user.name AS userName, pet.name AS petName " +
        "FROM user, pet " +
        "WHERE user.id = pet.user_id"
    )
    fun loadUserAndPetNames(): LiveData<List<UserPet>>

    // You can also define this class in a separate file.
    data class UserPet(val userName: String?, val petName: String?)
}

执行查询时,您通常会希望您的应用程序的UI在数据更改时自动更新。 为此,请在查询方法说明中使用LiveData类型的返回值。 在数据库更新时,Room会生成所有必需的代码来更新LiveData

Observable queries

@Dao
interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    fun loadUsersFromRegionsSync(regions: List<String>): LiveData<List<User>>
}

Reactive queries with RxJava

Room对RxJava2类型的返回值提供以下支持:

  1. @Query方法:Room支持类型为Publisher,Flowable和Observable的返回值。
  2. @ Insert,@ Update和@Delete方法:Room 2.1.0及更高版本支持Completable,Single和Maybe类型的返回值。

数据库的更新与迁移

随着应用功能的改变,你需要去更改 entity 和数据库,但很多时候,你不希望因此丢失数据库中已存在的的数据,尤其是无法从远程服务器恢复这些数据时。也就是说,如果你不提供必要的迁移操作,Room 将会重建数据库,数据库中所有的数据都将丢失。
为此, Room 允许你写一些 Migration 类去保护用户数据,每个 Migration 类指定一个 startVersionendVersion,在运行时,Room 会运行每个 Migration 类的 migrate() 方法,以正确的顺序将数据库迁移到最新版本:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
                "PRIMARY KEY(`id`))")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
    }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()

注意,为了保证迁移逻辑按预期运行,应该使用完整的查询而不是引用表示查询的常量。

高级用法与技巧

TypeConverter

有些时候,我们需要把一些自定义数据类型存入数据库,或者在存入数据库前做一些类型转换,如我们需要把 Date 类型的字段作为 Unix 时间戳存入数据库:

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time?.toLong()
    }
}

然后使用 @TypeConverters 注解那些需要使用转换器的元素。如果注解了 Database,那么数据库中所有的 DaoEntity 都能使用它。如果注解了 Dao,那么 Dao 中所有的方法都能使用它。如果注解了 Entity,那么 Entity 中所有的字段都能使用它。如果注解了 POJO,那么 POJO 中所有的字段都能使用它。如果注解了 Entity 字段,那么只有这个 Entity 字段能使用它。如果注解了 Dao 方法,那么该 Dao 方法中所有的参数都能使用它。如果注解了 Dao 方法参数,那么只有这个参数能使用它:

@Database(entities = arrayOf(User::class), version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

查询的时候,你仍然可以用你的自定义类型,就像使用原语类型一样:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
    fun findUsersBornBetweenDates(from: Date, to: Date): List<User>
}

Database 对象的创建

实例化 RoomDatabase 是相当昂贵的,最好使用 Dagger2 等依赖注入工具注入唯一的 Database 实例,如:

@Module(includes = ViewModelModule.class)
class AppModule {
    ...
    @Singleton @Provides
    GithubDb provideDb(Application app) {
        return Room.databaseBuilder(app, GithubDb.class,"github.db").build();
    }

    @Singleton @Provides
    UserDao provideUserDao(GithubDb db) {
        return db.userDao();
    }

    @Singleton @Provides
    RepoDao provideRepoDao(GithubDb db) {
        return db.repoDao();
    }
}


使用Kodein容器依赖注入

private const val DB_MODULE_TAG = "DBModule"

val dbModule = Kodein.Module(DB_MODULE_TAG) {

    bind<UserDatabase>() with singleton {
        Room.databaseBuilder(BaseApplication.INSTANCE, UserDatabase::class.java, "user")
                .fallbackToDestructiveMigration()
                .build()
    }
}

即使不使用依赖注入,也应该采用单例的方式创建 Database:

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {

    private static volatile AppDatabase INSTANCE;

    public abstract UserDao userDao();

    public static AppDatabase getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (AppDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            AppDatabase.class, "sample.db")
                            .build();
                }
            }
        }
        return INSTANCE;
    }

}

线程切换

操作数据库是个非常耗时操作,所以不能在主线程(UI线程)中查询或更改数据库,Room 也为此做了线程检查,如果你在主线程中操作了数据库会直接抛出异常。

更多详细内容访问这里,毕竟持续更新中。。。

------ 本文结束 ------
坚持原创技术分享,您的支持将鼓励我继续创作!