【Jetpack】Room 预填充数据 ( 安装 DB Browser for SQLite 工具 | 创建数据库文件 | 应用中设预填充数据对应的数据库文件 | 预填充数据库表字段属性必须一致 )





一、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 = databaseFilePath
    return 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 = 1
2023-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 , 实际数据 : null
2023-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 = 1
2023-06-14 14:15:10.413 I/Room_MainActivity: 主动查询 : LiveData : androidx.room.RoomTrackingLiveData@9a0df02 , 实际数据 : null
2023-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_0
    Process: kim.hsl.rvl, PID: 18915
    java.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.rvl

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import 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 注解修饰该属性字段
     */
    @Ignore
    lateinit var studentInfo: String

    /**
     * 默认的构造方法给 Room 框架使用
     */
    constructor(id: Int, name: String, age: Int) {
    
    
        this.id = id
        this.name = name
        this.age = age
    }

    /**
     * 使用 @Ignore 标签标注后
     * Room 就不会使用该构造方法了
     * 这个构造方法是给开发者使用的
     */
    @Ignore
    constructor(name: String, age: Int) {
    
    
        this.name = name
        this.age = age
    }

    /**
     * 使用 @Ignore 标签标注后
     * Room 就不会使用该构造方法了
     * 这个构造方法是给开发者使用的
     */
    @Ignore
    constructor(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.rvl

import android.content.Context
import android.util.Log
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [Student::class], version = 1, exportSchema = true)
abstract class StudentDatabase: RoomDatabase() {
    
    
    /**
     * 获取 数据库访问 对象
     * 这是必须要实现的函数
     */
    abstract fun studentDao(): StudentDao

    companion 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;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/han1202012/article/details/131199743