文章目录
- 一、Room 预填充数据简介
- 二、安装 DB Browser for SQLite 数据库查看工具
- 三、使用 DB Browser for SQLite 新建数据库
- 四、应用中设预填充数据对应的数据库文件
- 1、数据准备
- 2、原执行结果
- 3、预填充数据后的执行结果
- 五、预填充数据报错信息 – 数据库字段属性必须完全相同
- 六、完整代码示例
- 1、Entity 实体类代码
- 2、RoomDatabase 类代码
一、Room 预填充数据简介
在 Android 中使用 Room 框架 , 创建 SQLite 数据库时 , 有时需要预填充一些数据 , 这些数据一般都是来自 assets 资源目录 ;
如果用户首次打开应用 , 就会从 assets 资源目录中获取 SQLite 数据库文件 , 将该文件中的数据读取出来 , 并存储到 Room 数据库中 ;
二、安装 DB Browser for SQLite 数据库查看工具
想要预填充数据 , 需要创建 SQLite 数据库文件 , 这里使用 DB Browser for SQLite 创建并查看 SQLite 数据库文件 ;
首先 , 下载 DB Browser for SQLite 数据库工具 , 下载地址是 , 官方地址已经挂了 , 这里是 CSDN 下载地址 https://download.csdn.net/download/han1202012/87904496 , 0 积分即可下载 ;
然后 , 安装 DB Browser for SQLite 数据库 ; 下载后的文件是 DB.Browser.for.SQLite-3.12.2-win64.msi 文件 ;
双击上述安装文件 , 运行安装程序 ,
同意许可协议 ,
创建快捷方式 ,
设置安装地址 , 默认在 C 盘 ,
这里 点击 Browse 按钮 , 改成 D 盘 ,
开始安装
等待安装完成 ,
DB Browser for SQLite 数据库工具 安装完毕 ;
打开 DB Browser for SQLite 数据库工具 , 界面如下图所示 ;
三、使用 DB Browser for SQLite 新建数据库
参考 【Jetpack】Room 中的销毁重建策略 ( 创建临时数据库表 | 拷贝数据库表数据 | 删除旧表 | 临时数据库表重命名 ) 博客 中的 版本 1 数据库表结构对应的 Entity 实体类代码 ,
@Entity(tableName = "student")class Student {/** * @PrimaryKey 设置主键 autoGenerate 为自增 * @ColumnInfo name 设置列名称 / typeAffinity 设置列类型 */@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)var id: Int = 0/** * 姓名字段 * 数据库表中的列名为 name * 数据库表中的类型为 TEXT 文本类型 */@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)lateinit var name: String/** * 年龄字段 * 数据库表中的列名为 age * 数据库表中的类型为 INTEGER 文本类型 */@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER)var age: Int = 0}
Room 实体类代码 , 在 DB Browser for SQLite 工具中 , 创建 student 表字段 ;
打开 DB Browser for SQLite 工具 , 选择 ” 文件 / 新建数据库 ” ,
设置数据库存储目录 , 并设置数据库名称 ” init.db ” ;
点击 ” 保存 ” 按钮后 , 会弹出为 刚创建的数据库 编辑表定义 对话框 ;
点击 ” 增加 ” 按钮 , 插入了一个默认 Field1 字段 , 类型是 INTEGER ,
将创建的第一个字段 , 名称设置为 id , 类型仍为 INTEGER 不变 , 将该字段设置为 非空 / 自增 / 主键 ;
生成的 SQL 语句如下 :
CREATE TABLE "" ("id"INTEGER NOT NULL,PRIMARY KEY("id" AUTOINCREMENT));
继续添加 name 和 age 两个字段 ; 生成的 SQL 语句如下 :
CREATE TABLE "" ("id"INTEGER NOT NULL,"name"TEXT,"age"INTEGER,PRIMARY KEY("id" AUTOINCREMENT));
为数据库表设置名称 student ; 生成的 sql 语句如下所示 :
CREATE TABLE "student" ("id"INTEGER NOT NULL,"name"TEXT,"age"INTEGER,PRIMARY KEY("id" AUTOINCREMENT));
点击 ” 编辑表定义 ” 对话框中的 OK 按钮 , 即可创建数据库表成功 ; 创建后的数据库表如下 :
创建好数据库表之后 , 在 执行 SQL 面板界面 , 插入两条数据 ;
点击 三角形 的 执行按钮 , 即可执行下面的 SQL 语句 , 向 数据库 student 表中插入两条数据 ;
INSERT INTO student (name, age) VALUES ('Tom', 18);INSERT INTO student (name, age) VALUES ('Jerry', 16);
在 浏览数据 面板中, 查看刚才插入的数据 ;
设置完毕后 , 保存数据 ;
最终 , 得到一个 db 类型的数据库文件 ;
四、应用中设预填充数据对应的数据库文件
1、数据准备
将上个章节生成的 init.db 数据库文件拷贝到 assets 目录下 ,
然后在 RoomDatabase.Builder 构建器创建时 , 调用 RoomDatabase.Builder 构建器的 createFromAsset 函数 , 就可以自动从 assets 目录下自动读取 db 数据库文件中的数据 , 并将数据初始化本应用的数据库表中 ;
/** * 配置Room以使用位于的预打包数据库创建和打开数据库 * 应用程序“assets/”文件夹。 * * Room不打开预打包的数据库,而是将其复制到内部 * App数据库文件夹,然后打开它。预打包的数据库文件必须位于 * 应用程序的“assets/”文件夹。例如,位于的文件的路径 * “assets/databases/products.db”将变成“databases/products.db”。 * * 将验证预打包的数据库模式。最好是创建你的 * 预打包数据库模式时利用导出的模式文件生成 * (数据库。exportSchema]已启用。 * * 此方法不支持内存数据库[Builder]。 * * @param databaseFilePath 数据库文件所在的“assets/”目录中的文件路径。 * * @return This [Builder] instance. */fun createFromAsset(databaseFilePath: String): RoomDatabase.Builder<T" />> {mCopyFromAssetPath = databaseFilePathreturn this}
2、原执行结果
如果不设置 数据库 初始化数据 , 则输出的日志如下 :
2023-06-14 13:16:39.615 I/Room_MainActivity: Observer#onChanged 回调, List<Student>: []2023-06-14 13:16:40.019 I/Room_MainActivity: 插入数据 S1 : Student(id=0, name='Tom', age=18)2023-06-14 13:16:40.024 I/Room_MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18)]2023-06-14 13:16:40.522 I/Room_MainActivity: 插入数据 S2 : Student(id=0, name='Jerry', age=16)2023-06-14 13:16:40.526 I/Room_MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18), Student(id=2, name='Jerry', age=16)]2023-06-14 13:16:41.024 I/Room_MainActivity: 更新数据 S2 : Student(id=2, name='Jack', age=60)2023-06-14 13:16:41.031 I/Room_MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=1, name='Tom', age=18), Student(id=2, name='Jack', age=60)]2023-06-14 13:16:41.530 I/Room_MainActivity: 删除数据 id = 12023-06-14 13:16:41.538 I/Room_MainActivity: Observer#onChanged 回调, List<Student>: [Student(id=2, name='Jack', age=60)]2023-06-14 13:16:42.032 I/Room_MainActivity: 主动查询 : LiveData : androidx.room.RoomTrackingLiveData@8896405 , 实际数据 : null2023-06-14 13:16:42.037 I/Room_MainActivity: 主动查询2 : [Student(id=2, name='Jack', age=60)]
3、预填充数据后的执行结果
设置了 预填充数据 后 , 执行效果如下 :
2023-06-14 14:15:08.268 I/Room_MainActivity: 插入数据 S1 : Student(id=0, name='Tom', age=18)2023-06-14 14:15:08.797 I/Room_MainActivity: 插入数据 S2 : Student(id=0, name='Jerry', age=16)2023-06-14 14:15:09.329 I/Room_MainActivity: 更新数据 S2 : Student(id=2, name='Jack', age=60)2023-06-14 14:15:09.865 I/Room_MainActivity: 删除数据 id = 12023-06-14 14:15:10.413 I/Room_MainActivity: 主动查询 : LiveData : androidx.room.RoomTrackingLiveData@9a0df02 , 实际数据 : null2023-06-14 14:15:10.429 I/Room_MainActivity: 主动查询2 : [Student(id=6, name='Tom', age=18), Student(id=7, name='Jerry', age=16), Student(id=8, name='Tom', age=18), Student(id=9, name='Jerry', age=16)]
五、预填充数据报错信息 – 数据库字段属性必须完全相同
期间遇到该错误 , 报错信息如下 ;
2023-06-14 13:21:12.068 E/AndroidRuntime: FATAL EXCEPTION: arch_disk_io_0Process: kim.hsl.rvl, PID: 18915java.lang.RuntimeException: Exception while computing database live data.at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:92)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)at java.lang.Thread.run(Thread.java:764) Caused by: java.lang.IllegalStateException: Pre-packaged database has an invalid schema: student(kim.hsl.rvl.Student). Expected:TableInfo{name='student', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]} Found:TableInfo{name='student', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}at androidx.room.RoomOpenHelper.onCreate(RoomOpenHelper.java:82)at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onCreate(FrameworkSQLiteOpenHelper.java:118)at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:393)at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:298)at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:92)at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:53)at androidx.room.SQLiteCopyOpenHelper.getWritableDatabase(SQLiteCopyOpenHelper.java:90)at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:476)at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:281)at androidx.room.RoomDatabase.query(RoomDatabase.java:324)at androidx.room.util.DBUtil.query(DBUtil.java:83)at kim.hsl.rvl.StudentDao_Impl$4.call(StudentDao_Impl.java:123)at kim.hsl.rvl.StudentDao_Impl$4.call(StudentDao_Impl.java:120)at androidx.room.RoomTrackingLiveData$1.run(RoomTrackingLiveData.java:90)... 3 more
分析下面的错误 :
期待获取的数据库表信息 :
TableInfo{name='student', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}}, foreignKeys=[], indices=[]}
实际获取的数据库表信息 :
TableInfo{name='student', columns={name=Column{name='name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
唯一的区别就是 age 字段的 非空属性不同 , 这里 在 DB Browser for SQLite 工具中设置 age 字段为非空字段 ;
右键点击数据库表 , 在弹出的右键菜单中 , 选择 ” 修改表 ” 选项 ,
将 age 属性设置为非空 ;
六、完整代码示例
本博客中的代码是在上一篇博客 【Jetpack】Room 中的销毁重建策略 ( 创建临时数据库表 | 拷贝数据库表数据 | 删除旧表 | 临时数据库表重命名 ) 的基础上 , 添加了 由 DB Browser for SQLite 工具制作的 预填充数据 文件 ;
1、Entity 实体类代码
该实体类中 , 暂时只保留 id , name , age 三个字段 ;
package kim.hsl.rvlimport androidx.room.ColumnInfoimport androidx.room.Entityimport androidx.room.Ignoreimport androidx.room.PrimaryKey/** * 定义数据库表 Entity 实体 / 同时定义数据库表 和 对鹰的实体类 * 设置该数据类对应数据库中的一张数据表, 表名为 student * 该数据库表中的数据对应一个 Student 类实例对象 */@Entity(tableName = "student")class Student {/** * @PrimaryKey 设置主键 autoGenerate 为自增 * @ColumnInfo name 设置列名称 / typeAffinity 设置列类型 */@PrimaryKey(autoGenerate = true)@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)var id: Int = 0/** * 姓名字段 * 数据库表中的列名为 name * 数据库表中的类型为 TEXT 文本类型 */@ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)lateinit var name: String/** * 年龄字段 * 数据库表中的列名为 age * 数据库表中的类型为 INTEGER 文本类型 */@ColumnInfo(name = "age", typeAffinity = ColumnInfo.INTEGER)var age: Int = 0/** * 性别字段 * 数据库表中的列名为 sex * 数据库表中的类型为 TEXT 文本类型 *//*@ColumnInfo(name = "sex", typeAffinity = ColumnInfo.TEXT)var sex: String = "M"*//** * 性别字段 * 数据库表中的列名为 sex * 数据库表中的类型为 INTEGER 文本类型 *//*@ColumnInfo(name = "sex", typeAffinity = ColumnInfo.INTEGER)var sex: Int = 0*//** * degree字段 * 数据库表中的列名为 sex * 数据库表中的类型为 INTEGER 文本类型 *//*@ColumnInfo(name = "degree", typeAffinity = ColumnInfo.INTEGER)var degree: Int = 0*//** * 有些属性用于做业务逻辑 * 不需要插入到数据库中 * 使用 @Ignore 注解修饰该属性字段 */@Ignorelateinit var studentInfo: String/** * 默认的构造方法给 Room 框架使用 */constructor(id: Int, name: String, age: Int) {this.id = idthis.name = namethis.age = age}/** * 使用 @Ignore 标签标注后 * Room 就不会使用该构造方法了 * 这个构造方法是给开发者使用的 */@Ignoreconstructor(name: String, age: Int) {this.name = namethis.age = age}/** * 使用 @Ignore 标签标注后 * Room 就不会使用该构造方法了 * 这个构造方法是给开发者使用的 */@Ignoreconstructor(id: Int) {this.id = id}override fun toString(): String {return "Student(id=$id, name='$name', age=$age)"}}
与之完全对应的数据库表如下 :
对应的 SQLite 数据库表创建语句如下 :
CREATE TABLE "student" ("id"INTEGER NOT NULL,"name"TEXT,"age"INTEGER NOT NULL,PRIMARY KEY("id" AUTOINCREMENT));
2、RoomDatabase 类代码
在 RoomDatabase.Builder 构建器创建时 , 调用 RoomDatabase.Builder 构建器的 createFromAsset 函数 , 就可以自动从 assets 目录下自动读取 db 数据库文件中的数据 , 并将数据初始化本应用的数据库表中 ;
package kim.hsl.rvlimport android.content.Contextimport android.util.Logimport androidx.room.Databaseimport androidx.room.Roomimport androidx.room.RoomDatabaseimport androidx.room.migration.Migrationimport androidx.sqlite.db.SupportSQLiteDatabase@Database(entities = [Student::class], version = 1, exportSchema = true)abstract class StudentDatabase: RoomDatabase() {/** * 获取 数据库访问 对象 * 这是必须要实现的函数 */abstract fun studentDao(): StudentDaocompanion object {lateinit var instance: StudentDatabase/** * 数据库版本 1 升级到 版本 2 的迁移类实例对象 */val MIGRATION_1_2: Migration = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {Log.i("Room_StudentDatabase", "数据库版本 1 升级到 版本 2")database.execSQL("alter table student add column sex integer not null default 1")}}/** * 数据库版本 2 升级到 版本 3 的迁移类实例对象 */val MIGRATION_2_3: Migration = object : Migration(2, 3) {override fun migrate(database: SupportSQLiteDatabase) {Log.i("Room_StudentDatabase", "数据库版本 2 升级到 版本 3")database.execSQL("alter table student add column degree integer not null default 1")}}/** * 数据库版本 3 升级到 版本 4 的迁移类实例对象 * 销毁重建策略 */val MIGRATION_3_4: Migration = object : Migration(3, 4) {override fun migrate(database: SupportSQLiteDatabase) {Log.i("Room_StudentDatabase", "数据库版本 3 升级到 版本 4")// 创新临时数据库database.execSQL("CREATE TABLE temp_student (" +"id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +"name TEXT," +"age INTEGER NOT NULL," +"sex TEXT NOT NULL DEFAULT 'M'," +"degree INTEGER NOT NULL DEFAULT 1)")// 拷贝数据database.execSQL("INSERT INTO temp_student (name, age, degree)" +"SELECT name, age, degree FROM student")// 删除原始表database.execSQL("DROP TABLE student")// 将临时表命令为原表表明database.execSQL("ALTER TABLE temp_student RENAME TO student")}}fun inst(context: Context): StudentDatabase {if (!::instance.isInitialized) {synchronized(StudentDatabase::class) {// 创建数据库instance = Room.databaseBuilder(context.applicationContext,StudentDatabase::class.java,"student_database.db").createFromAsset("init.db")/*.addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4)*/.fallbackToDestructiveMigration().allowMainThreadQueries() // Room 原则上不允许在主线程操作数据库// 如果要在主线程操作数据库需要调用该函数.build()}}return instance;}}}