一、前言

    数据库是 Room 的三大组件之一,数据库是用来存储数据的,是 Room 中必不可少的一部分。本篇幅将详细讲解 Room 数据库相关的内容(对于数据库的基本使用请参阅:Android Room 库基础入门)。

二、@Database 注解详解

    @Database 注解是 Room 定义数据库类的注解,通过该注解可以设定数据库的部分配置,注解的属性如下:

字段名称数据类型默认值说明
entitiesClass<?>[]-数据库关联的数据实体类,每一个数据实体类对应数据库中的一张表
viewsClass<?>[]-数据库视图,每一个类对应数据库中的一个视图
versionint-数据库版本,数据库架构发生改变时需要升级版本号
exportSchemaBooleantrue将数据库结构到处到目录
autoMigrationsClass<?>[]-数据库自动迁移处理类列表,每一个类定义数据库迁移升级时需要执行测操作。

三、数据库表定义

    Room 在创建数据库时,不需要手动编写 SQL 语句创建表,它会根据关联的数据实体类创建对应的表。在定义数据库时,通过 @Database 注解的 entities 参数指定数据库中关联的数据实体类(即所包含的数据表)。如下示例代码所示:

@Entity(tableName = "users")
data class User(@PrimaryKey(autoGenerate = true) val uid: Int?, @ColumnInfo() val name: String, @ColumnInfo val age: Int, @ColumnInfo val addr: String?)

@Entity(tableName = "schools")
data class School(@PrimaryKey val sid: Int, val name: String, val addr: String)

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

示例讲解:以上的示例中,AppDatabase 数据库中关联了 User 和 School 两个实体类,它内部有 users 和 schools 两个表(数据实体类指定了表名)

注意事项:
1. 数据库关联的数据实体类必须是 @Entity 注解标注的;
2. 数据库新增数据表时,需要新建数据实体类,并将实体类添加到数据库定义的关联实体类列表中;
3. 数据库只在首次创建数据库时,根据定义的关联数据库实体类创建表,无需编写 SQL 语句;
4. 应用在新版本添加表(或更改表结构),需要进行数据库迁移(也就是常说的数据库升级),否则数据库不会创建新表(或更改表结构)。

四、数据库迁移

    随着应用的版本迭代和功能变更,应用的数据库结构也可能跟随这变动,比如新增数据表、更改原有的表结构等等。在数据结构变动的过程中,保护数据库已有的用户数据是非常重要的。在 Room 中进行数据库迁移,大致可分为两个步骤,第一是修改数据实体类(新增或者修改原有的),第二是将数据实体类的变更同步到底层数据库对应的表中。

4.1 修改数据实体类

    数据库迁移的过程中,修改数据库实体类包括新增数据实体类(新增表)、修改原有的数据实体类(变更现有表结构)和删除数据实体类(删除表)。这个过程比较简单,无非就是新建数据实体类或者对现有的数据实体类进行修改。

注意事项:数据迁移过程中,新增的数据实体类一定要同步到数据库 @Database 注解声明的关联实体类列表中

4.2 将数据实体类变更同步到底层数据库对应的表中

     Room 只会在首次创建数据库时,根据定义的关联数据实体类创建表,因为首次创建并不涉及用户数据问题,但是在后续数据库迁移过程中,必须保证不影响旧的用户数据。Room 提供了两种增量式数据库迁移,自动迁移和手动迁移。自动迁移能够满足多数的基本构造变更,但是如果比较复杂的迁移,就需要手动定义迁移路径。

4.2.1 自动迁移

注意事项:Room 仅在 2.4.0-alpha01 及更高版本库中才支持自动迁移,如果使用低与此版本的 Room 库,需要使用手动迁移。

    声明数据库在两个版本之间自动迁移,在 @Database 注解中的 autoMigrations 参数中添加一个 @AutoMigration 注解即可。如下代码示例所示:

// 迁移前数据库定义
@Database(entities = [User::class, School::class, Student::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

// 迁移时数据库定义
@Database(entities = [User::class, School::class, Student::class], version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)])
abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

注意事项:Room 自动迁移是基于新版本和旧版本数据库生成的数据库架构的,如果 @DatabaseexportSchema 设置为 false,或者没有提升数据库的版本号就进行编译,自动迁移操作将会失败。

4.2.1.1 添加自迁移规范

    如果 Room 发现迁移过程中有歧义的架构,并且在未提供更多信息的时候无法制定确切的自迁移方案,在编译期间就会发生错误,并要求开发者提供一个 AutoMigrationSpec 的实现。多数情况下,自迁移发生错误都是由下列的原因造成的。

  • 删除或者重命名数据表名
  • 删除或者重命名数据表的列名

    对于这种编译错误,开发者可以定义一个 AutoMigrationSpec 对象,给 Room 准确地生成迁移路径提供必要的额外信息。在 RoomDatabase 内部定义一个 AutoMigrationSpec 实现类,并且用以下注解进行标注。

  • @DeleteTable:删除数据表
  • @RenameTable: 重命名数据表
  • @DeleteColumn:删除数据表中的列
  • @RenameColumn:重命名数据表中的列

    在自动迁移数据库时使用 AutoMigrationSpec 实现类,只需要在 @AutoMigration 注解的 spec 参数中指定即可。如果应用需要在自动迁移完成之后做更多的操作,可以重写 onPostMigrate() 方法并在内部实现需要进行的操作,Room 会在自动迁移完成之后调用这个方法。如下示例所示:

@Database(entities = [User::class, School::class, Student::class], version = 2, 
    autoMigrations = [AutoMigration(from = 1, to = 2, spec = AppDatabase.RenameTableAutoMigrationSpec::class)])
abstract class AppDatabase: RoomDatabase() {
    class RenameTableAutoMigrationSpec : AutoMigrationSpec {
        override fun onPostMigrate(db: SupportSQLiteDatabase) {
            // ....
        }
    }


    abstract fun userDao(): UserDao
}

笔者注:由于笔者写这篇文章时,Room 库 2.4.0还在alpha阶段,并且因 JDK 版本问题,alpha版本出现编译失败的情况,无法验证示例代码,如果你们的环境可以使用2.4.0-alpha02版本编译通过,或者后续发布正式版之后可以使用,可以是尝试下自动迁移带来的便利。

4.2.2 手动迁移数据库

    如果数据库迁移包含了复杂的架构变更,Room可能无法自动生成准确的迁移路径,此时,开发者就需要手动定义迁移路径(在Room 2.3.0就及以下版本只能使用手动迁移数据库)。定义数据库迁移路径,需要定义一个类,该类实现 Migration 抽象类,通过重写 migrate() 方法来精确定义旧版本和新版本之间的迁移路径(换句话说就是开发者重写 Migration 类的 migrate() 方法,在里面添加数据库迁移需要进行的操作)。然后在数据库构建器中使用 addMigration() 接口,将定义的 Migration 实例化对象传入。如下示例所示:

// 1. 数据库迁移前
@Entity(tableName = "users")
data class User(@PrimaryKey(autoGenerate = true) val uid: Int?, @ColumnInfo() val name: String, @ColumnInfo val age: Int, @ColumnInfo val addr: String?)

@Entity(tableName = "schools")
data class School(@PrimaryKey val sid: Int, val name: String, val addr: String)

// 其他代码

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

// 构建数据库
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db")
    .build()

// ============================================================================================
// 2. 数据库迁移(新增一张student表,这里演示就用简单的例子)
@Entity(tableName = "users")
data class User(@PrimaryKey(autoGenerate = true) val uid: Int?, @ColumnInfo() val name: String, @ColumnInfo val age: Int, @ColumnInfo val addr: String?)

@Entity(tableName = "schools")
data class School(@PrimaryKey val sid: Int, val name: String, val addr: String)

@Entity(tableName = "students")
data class Student(@PrimaryKey val sid: Int, val name: String, val age: Int)

// 其他代码

@Database(entities = [User::class, School::class, Student::class], version = 2)
abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}


val migration_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 这里新建一张students表,注意表结构一定要跟定义的数据实体类一致,包括数据类型、主键、字段是否为空等
        database.execSQL("CREATE TABLE students(sid TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL)")
    }
}

// 构建数据库
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db")
    .addMigrations(migration_1_2)
    .build()
  • 数据迁移前数据库结构
    数据迁移前数据库结构
  • 数据迁移后数据库结构
    数据迁移后数据库结构

讲解:笔者使用了可以访问应用数据目录的模拟器,用于展现数据库结构的变化。用真机在没有获取root权限的情况下是无法访问应用内部存储目录的,大家需要注意哦。另外,笔者只是使用简单的示例进行讲解,喜欢挑战的可以实现一些复杂的数据库迁移。

注意事项:
1. 迁移数据库过程中,必须保证迁移路径定义的变更跟数据实体类定义的结构完全一致(如:数据类型、主键、字段是否为空等),否则运行会报异常,关于数据实体类跟表结构关系参考:Android Room 数据实体类详解
2. 为每次数据库迁移定义一个 Migration,如果应用升级导致跨多个版本迁移,Room 会自动实现从低版本的往高版本的逐一进行迁移,但必须保证数据库构建器里包含所有的 Migration

  • 示例:数据库跨多个版本迁移

跨多个版本数据库迁移,这种场景一般是设备安装了某个版本的应用,应用开发商进行了多次更新,且其中包含了多次的数据库迁移,但是设备应用并没有更新,当设备将应用从旧版直接更新到最新版本时,跨越了多个版本(包括数据版本)。

// 1. 迁移前,数据库版本为1
@Entity(tableName = "users")
data class User(@PrimaryKey(autoGenerate = true) val uid: Int?, @ColumnInfo() val name: String, @ColumnInfo val age: Int, @ColumnInfo val addr: String?)

@Entity(tableName = "schools")
data class School(@PrimaryKey val sid: Int, val name: String, val addr: String)

// 其他代码

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

// 构建数据库
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db")
    .build()

// =========================================================================================================
// 2. 从版本1直接迁移到版本3,中间跨度了一个版本2(从版本1迁移到版本2的参考上面的例子)
@Entity(tableName = "users")
data class User(@PrimaryKey(autoGenerate = true) val uid: Int?, @ColumnInfo() val name: String, @ColumnInfo val age: Int, @ColumnInfo val addr: String?)

@Entity(tableName = "schools")
data class School(@PrimaryKey val sid: Int, val name: String, val addr: String)

@Entity(tableName = "students")
data class Student(@PrimaryKey val sid: Int, val name: String, val age: Int, val addr: String?) // students表增加一个addr列

// 其他代码

// 此时数据版本为3,直接有版本1迁移到版本3
@Database(entities = [User::class, School::class, Student::class], version = 3)
abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
}

// 构建数据库类
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db")
    .addMigrations(migration_1_2, migration_2_3) // 这里必须包含所有版本的 Migration
    .build()

// 版本1升级到版本2的Migration(新增students表)
val migration_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 这里新建一张students表,注意表结构一定要跟定义的数据实体类一致,包括数据类型、主键、字段是否为空等
        database.execSQL("CREATE TABLE students(sid INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL)")
    }
}

// 版本2升级到版本3的Migration(students表新增addr列
val migration_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 注意,列属性一定要跟数据实体类定义的一致
        database.execSQL("ALTER TABLE students ADD COLUMN addr TEXT")
    }
}

  • 迁移前数据库结构
    迁移前数据库结构

  • 迁移后数据库结构
    迁移后数据库结构

讲解:从上面的例子可以看到,直接从版本1迁移到版本3,Room 会自动完成从版本1-2-3的顺序进行迁移。

五、预填充 Room 数据库

    有时候,你可能想要首次使用应用时数据库就已经加载了一系列特定的数据,称之为预填充数据库。在 Room 2.2.0 以及更高版本,你可以在初始化 Room 数据库时通过 API 方法,使用存储在设备文件系统中预封装的数据库文件对 Room 数据库进行预填充。

注意事项:在内存中的 Room 数据库不支持使用 createFromAsset() 或者 createFromFile() 进行预填充

5.1 从应用的 assets 资源中预填充

    从应用的 assets 目录下的任何子目录中加载预封装的数据库文件来预填充 Room 数据库,可以在构建 Room 数据的构建器(RoomDatabase.Builder)中调用 createFromAsset() 方法(在调用 build() 之前 调用)。createFromAsset() 方法传入预封装数据库文件在 assets/ 目录下的相对路径字符串,如下示例所示:

val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app.db")
        // 在数据库Builder中加载,createFromAsset方法参数是预封装数据库文件在assts目录下的相对路径
    .createFromAsset("init_data.db")
    .build()

注意事项:从 assets 中预填充数据库时,Room会校验数据库的结构是否跟预封装的数据库结构一致,如果结构不一致不会填充数据。但并不是数据库所有表结构都要一致,只需要保证需要预填充数据的表的结构一致即可,对于不需要预填充数据的表可以不一致甚至不存在(例如:预封装数据库中包含users表,但是应用数据库中可以用users、teachers、schools等表,但是对于users表,必须保证预封装数据库中的表结构跟应用数据库中的表结构完全一致,才能完成预填充)。建创建预封装数据库时导出数据库结构文件,在创建应用数据库时根据导出的预封装数据库结构来创建应用数据库。

5.2 从文件系统中预填充

    从设备文件系统中任何可以访问的地方(除了应用的 assets 目录下)加载预封装数据库来预填充 Room 数据库,可以在构建 Room 数据的构建器(RoomDatabase.Builder)中调用 createFromFile() 方法(在调用 build() 之前 调用)。 createFromFile() 方法传入预封装数据库文件 File 对象,如下示例所示:

val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app.db")
     // 在数据库Builder中加载,createFromFile方法参数是预封装数据库文件在文件系统中的File对象(绝对路径)
     // 若设备开启分区存储,可能会出现部分文件系统无法访问,这个需要特别注意。
    .createFromFile(File(Environment.getExternalStorageDirectory(), "init_data.db"))
    .build()

注意事项:
1. 从文件系统中预填充数据库时,Room会校验数据库的结构是否跟预封装的数据库结构一致,如果结构不一致不会填充数据。但并不是数据库所有表结构都要一致,只需要保证需要预填充数据的表的结构一致即可,对于不需要预填充数据的表可以不一致甚至不存在(例如:预封装数据库中包含users表,但是应用数据库中可以用users、teachers、schools等表,但是对于users表,必须保证预封装数据库中的表结构跟应用数据库中的表结构完全一致,才能完成预填充)。建创建预封装数据库时导出数据库结构文件,在创建应用数据库时根据导出的预封装数据库结构来创建应用数据库。
2. 若设备开启分区存储,可能会出现部分文件系统无法访问,请确保预封装的数据库放在可访问的文件系统中。

六、写在最后面

    Room 数据库的迁移不可避免,因为一个应用不可能完全从一开始就规划好所有,后续版本的自迁移功能为开发者省去很多工作,但是如果自迁移无法迁移,开发者就需要进行手动迁移。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐