在 Android 中使用 Room 框架 , 创建 SQLite 数据库时 , 有时需要预填充一些数据 , 这些数据一般都是来自 assets 资源目录 ;
如果用户首次打开应用 , 就会从 assets 资源目录中获取 SQLite 数据库文件 , 将该文件中的数据读取出来 , 并存储到 Room 数据库中 ;
想要预填充数据 , 需要创建 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 数据库工具 , 界面如下图所示 ;
参考 【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 类型的数据库文件 ;
将上个章节生成的 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
}
如果不设置 数据库 初始化数据 , 则输出的日志如下 :
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)]
设置了 预填充数据 后 , 执行效果如下 :
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 工具制作的 预填充数据 文件 ;
该实体类中 , 暂时只保留 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)
);
在 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;
}
}
}