一、前言

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

二、@Database 注解详解

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

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

三、数据库表定义

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

Android Room 数据库详解相关推荐

  1. Android Sqlite数据库详解

    在整理ContentProvider知识点之前要先整理Sqlite数据库的知识,因为ContentProvider中要使用到数据库. 步入正题:Sqlite的起源是一艘军舰上,一个数据库程序员觉得潜艇 ...

  2. JMessage Android 端开发详解

    JMessage Android 端开发详解 目前越来越多的应用会需要集成即时通讯功能,这里就为大家详细讲一下如何通过集成 JMessage 来为你的 App 增加即时通讯功能. 首先,一个最基础的 ...

  3. 《Java和Android开发实战详解》——1.2节Java基础知识

    本节书摘来自异步社区<Java和Android开发实战详解>一书中的第1章,第1.2节Java基础知识,作者 陈会安,更多章节内容可以访问云栖社区"异步社区"公众号查看 ...

  4. Android系统目录结构详解

    Android系统基于linux内核.JAVA应用,算是一个小巧精致的系统.虽是开源,但不像Linux一般庞大,娇小可亲,于是国内厂商纷纷开发出自己基于Android的操作系统.在此呼吁各大厂商眼光放 ...

  5. [Android]多进程知识点详解

    作者:Android开发_Hua 链接:https://www.jianshu.com/p/e0f833151f66 多进程知识点汇总: 一:了解多进程 二:项目中多进程的实现 三:多进程的优缺点与使 ...

  6. pandas读写MySQL数据库详解及实战

    pandas读写MySQL数据库详解及实战 SQLAlchemy是Python中最有名的ORM工具. 关于ORM: 全称Object Relational Mapping(对象关系映射). 特点是操纵 ...

  7. 《Android游戏开发详解》——第1章,第1.6节函数(在Java中称为“方法”更好)...

    本节书摘来自异步社区<Android游戏开发详解>一书中的第1章,第1.6节函数(在Java中称为"方法"更好),作者 [美]Jonathan S. Harbour,更 ...

  8. 《Java和Android开发实战详解》——2.5节良好的Java程序代码编写风格

    本节书摘来自异步社区<Java和Android开发实战详解>一书中的第2章,第2.5节良好的Java程序代码编写风格,作者 陈会安,更多章节内容可以访问云栖社区"异步社区&quo ...

  9. Android事件流程详解

    Android事件流程详解 网络上有不少博客讲述了android的事件分发机制和处理流程机制,但是看过千遍,总还是觉得有些迷迷糊糊,因此特地抽出一天事件来亲测下,向像我一样的广大入门程序员详细讲述an ...

最新文章

  1. 如何创建自己的docker image并上传到DockerHub上
  2. 线性回归与梯度下降法
  3. Linux下实现脚本监测特定进程占用内存情况
  4. Spring Cloud Feign 熔断器支持
  5. 新年彩蛋:Spring Boot自定义Banner
  6. 郁闷的.net程序员与坑爹的.net 4 client profile
  7. nekohtml资料
  8. Sublime 编译汇编程序
  9. java swing 颜色_Java Swing按钮颜色
  10. Java List去重 Lis集合去重 List去重效率对比 List去重复元素效率对比 List去重效率
  11. 如何在苹果电脑Mac浏览器Safari中安装使用浏览器插件
  12. 写给自己以及各位程序员,无论你在什么位置,我想你都应该看一下
  13. 飞机大战Python全代码 + 图片
  14. 机器学习(二)——基本术语
  15. Linux内核编译很简单,6步编译一个自己的内核
  16. php排版word文档试卷,你可知Word还能这么用!原来试卷是这样做出来的
  17. java基于微信小程序的大学生心理测试 uniapp小程序
  18. PDF格式转换Word怎么转
  19. 如何通过WPS将Word(doc、docx)转换为PDF格式的教程方法
  20. SQL Server函数之空值处理

热门文章

  1. 共阴极数码管,学号显示实验
  2. c语言共阴极数码管编码,数码管之共阴极与共阳极编码
  3. 【大数据】Hadoop 体系(五)
  4. 详细设计的工具——PAD图
  5. 数美滑块,js逆向:★★★★
  6. 对于Android11无法访问Android/data的解决方案 还在为你的大姐姐找不到而担心吗?还在为你的学习资料找不到而发愁吗?2021-03-11
  7. Unity_播放音乐
  8. [转|会计学习]资产盘盈、盘亏的会计处理
  9. 常用数据库的种类与特点
  10. 电脑共享文件打不开要如何解决