ROOM 持久性库在 SQLite 的基础上提供一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
该库可帮助您在运行应用的设备上创建应用数据的缓存。此缓存充当应用的单一可信来源,使用户能够在应用中查看关键信息的一致副本,无论用户是否具有互联网连接。
一、添加依赖
如果仅对Java支持,请添加以下依赖:
// ===============Room java===============
implementation "androidx.room:room-runtime:2.4.3" // room依赖(java)
annotationProcessor "androidx.room:room-compiler:2.4.3" // 注解处理器(java)
如果对Kotlin支持,请添加以下依赖:
// ===============Room kotlin===============
implementation "androidx.room:room-ktx:2.4.3" // room依赖(kotlin)
kapt "androidx.room:room-compiler:2.4.3" // 注解处理器(kotlin)
二、数据库的定义
/**
* 数据库文件
*
* @Database 数据库注解
* entities 数据库中表格集合
* version 数据库版本号
* exportSchema 默认为true
* true:将数据库的配置导出来,存储到 ”/当前模块/schemas/AppDateBase路径/1.json“, 1为数据库版本号version
* false:数据库的配置不会导出来
*/
@Database(entities = [],version = 1,exportSchema = false)
abstract class AppDataBase:RoomDatabase() {
companion object{
@Volatile
private var instance:AppDataBase? = null
fun getInstance(context:Context):AppDataBase{
return instance?: synchronized(this) {
instance?:buildDataBase(context)
.also {
instance = it
}
}
}
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 当数据库创建的时候执行
}
})
.build()
}
}
}
【1】定义一个抽象类,防止外部直接以 "AppDataBase()" 方式新建实例;
【2】必须继承 RoomDatabase 类
【3】使用 @Database 注解所修饰
【4】entities 存放表的集合
【5】version是数据库的版本号,数据库升级的时候需要改下版本号
【6】exportSchema 默认值为true
true:将数据库的配置导出来,存储到 ”/当前模块/schemas/AppDateBase路径/1.json“, 1为数据库版本号version
false:数据库的配置不会导出来
【7】AppDataBase必须做成单例
【8】指定数据库名称
Room.databaseBuilder(context,AppDataBase::class.java,"room.db")
【9】当数据库被创建时的回调处理
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
三、表的定义
/**
* 用户表
*/
@Entity(tableName = "users")
data class User (
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val userId: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int
)
需要将定义好的表添加到 @Database
的 entities 数组中:
@Database(entities = [User::class],version = 1,exportSchema = false)
@Entity:定义表所需的注解,默认表名就是类名,类名默认手写字母大写,所以实际创建好的表名就是手写字母大写的。
一般情况下,我们都会主动指定一个小写的表名,使用 tableName 关键字可以指定具体的表名。
另外,在使用 sql 语句时,是忽视大小写的,无论表名是大写还是小写,sql 语句中的表名可以随意大小写切换。
@PrimaryKey:指定唯一主键,autoGenerate = true,表示该主键会自增长。
一般情况下,只要设置了自增长,那么该字段就是唯一主键。
当插入记录时,如果想覆盖相同的记录,那么,当主键相同时,则直接覆盖。当主键id等于0,则默认自增长,永远都不可能覆盖,因为id始终不同,room认为不是相同记录。当主键id不为0,才会覆盖重复记录。
在实际开发中,使用自增长的场景并不是很多,因为主键的自增长很难确定记录的唯一性,一不小心就插入了重复的记录,导致数据的冗余。所以,我们可以根据具体场景,决定是否使用主键的自增长。
primaryKeys:可以使用 primaryKeys 去指定主键,具体代码如下:
@Entity(tableName = "users", primaryKeys = ["id"])
data class User (
@ColumnInfo(name = "id")
val userId: Long = 0
}
primaryKeys 和 @PrimaryKey无法同时使用,所以使用 primaryKeys 时,请去掉 @PrimaryKey。
primaryKeys 无法让主键自增长,但是可以将多个字段做为主键,它被定义为 复合主键
,代码如下:
@Entity(tableName = "users", primaryKeys = ["id", "user_account"])
data class User (
@ColumnInfo(name = "id")
val userId: Long = 0,
@ColumnInfo(name = "user_account") val account: String // 账号
}
在有些场景中,需要将两个字段做为主键,以确保记录的唯一性。primaryKeys 数组中的字段是表的字段名称。
@ColumnInfo: 表示表的字段,name 是表字段的名称, typeAffinity 可以指定表字段的类型:
@ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER) val userId: Long = 0
typeAffinity 支持的类型有:
ColumnInfo.UNDEFINED:未定义类型
ColumnInfo.TEXT:文本类型(strings)
ColumnInfo.INTEGER:整数类型(integers or booleans)
ColumnInfo.REAL:浮点型(floats or doubles)
ColumnInfo.BLOB:二进制数据(binary data)
一般情况下, typeAffinity 可以不需要专门赋值,因为变量本身是有数据类型的,当创建表时,字符串的字段会自动识别。
@Embedded:将一个对象中所有的字段做为表的字段,防止多表创建。Address 代码如下:
/**
* 地址
*/
data class Address(
val street:String,val state:String,val city:String,val postCode:String
)
在 users 表中,会新增 street、state、city、postCode 字段。
@Ignore:被忽视的字段,不会做为数据库表的字段,比如有如下表的定义:
/**
* 用户表
*/
@Entity(tableName = "users", primaryKeys = ["id", "user_account"])
data class User (
@ColumnInfo(name = "id")
val userId: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int,
@Ignore val test: Int
) {
constructor(userId: Long, account: String, pwd: String, name: String, address: Address, status: Int):
this(userId, account, pwd, name, address, status, 0)
}
我们重点来看一下 @Ignore
注解,被次注解修饰的变量 test
,不会称为表的字段,此时,在主构造函数中,test 变化其实是多余的,它的唯一作用是当作临时变量,用于其它操作,最好是直接去掉 @Ignore
修饰的变量,如果为了满足其它操作,不能去掉,此时需要增加一个次构造函数,如下:
constructor(userId: Long, account: String, pwd: String, name: String, address: Address, status: Int):
this(userId, account, pwd, name, address, status, 0)
次构造函数的形参中需要包含所有的表字段,并且不能有 被 @Ignore 修饰的变量
,同时次构造函数必须继承主构造函数。
如果,次构造函数中的形参没有包括所有的表字段,那边次构造函数必须被 @Ignore
修饰:
@Ignore
constructor(userId: Long): this(userId, "", "", "",
Address("", "", "", ""), 1, 0)
一旦被 @Ignore
修饰之后,生成数据库表时,将忽视该次构造函数。
四、创建CURD操作
@Dao
interface UserDao {
@Query("select * from users where id = :id")
fun getUserById(id: Long): User
@Query("select * from users")
fun getAllUsers(): List<User>
// OnConflictStrategy.REPLACE: 如果记录已存在,则直接替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUser(user: User)
@Delete
fun deleteUserByUser(user: User)
@Query("delete from users where id = :id ")
fun deleteUserById(id: Long)
@Update
fun updateUserByUser(user: User)
@Query("update users set user_name = :updateName where id = :id")
fun update(id: Long, updateName: String)
}
UserDao 是一个接口,@Dao
标记数据操作接口。
@Query:执行sql语句,它可以执行CURD的各种操作
@Insert:插入记录,形参只能是记录对象User
@Delete:删除记录,形参只能是记录对象User
@Update:更新记录,形参只能是记录对象User
onConflict :冲突策略,一般使用 OnConflictStrategy.REPLACE
,即如果记录重复,则直接覆盖,其它策略不考虑
五、执行CURD操作
插入数据代码如下(其它操作不演示):
class RoomActivity : ScopedActivity() {
private lateinit var binding : ActivityRoomBinding
private var userDao: UserDao? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRoomBinding.inflate(layoutInflater)
setContentView(binding.root)
userDao = AppDataBase.getInstance(applicationContext).userDao()
binding.userInsert.setOnClickListener { // 添加用户
launch(Dispatchers.Default) { // 数据库操作不能放在主线程
val address = Address("xxx街道", "xxxStatus", "xxx城市", "xxx邮编")
val user = User(0, "NobugException", "123456", "zhangsan", address, 1)
userDao?.addUser(user)
}
}
}
}
数据库的CURD操作不能在主线程中执行,否则会报错。
除非在数据库配置中允许在主线程运行:
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
.allowMainThreadQueries() // 是否允许在主线程中执行数据库的增删改查
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
.build()
}
allowMainThreadQueries 方法允许CURD在主线程执行。
虽然这样做可以让CURD在主线程操作,但是最好不要这样做,CURD是一个耗时操作,放在主线程中执行会对性能造成一定的影响。
六、外键和索引
外键是为了保持数据一致性,完整性,主要目的是控制存储在外键表中的数据,使两张表形成关联,外键只能引用外表中的列的值。
现在有3张表:
用户表:
/**
* 用户表
*/
@Entity(tableName = "user")
data class User (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "user_id") val id: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int
)
书籍表:
/**
* 书籍表
*/
@Entity(tableName = "book")
data class Book(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "book_id") var id: Long, // 图书id
@ColumnInfo(name = "book_name") var name: String, // 书名
@ColumnInfo(name = "book_price") var price: Float // 价格
)
喜好表:
/**
* 爱好表
*/
@Entity(tableName = "hobby", foreignKeys = [
ForeignKey(entity = User::class, parentColumns = ["user_id"], childColumns = ["user_id"], onDelete = CASCADE),
ForeignKey(entity = Book::class, parentColumns = ["book_id"], childColumns = ["book_id"], onDelete = CASCADE)
], indices = [
Index(value = ["user_id"], unique = true),
Index(value = ["book_id"], unique = false)
])
data class Hobby(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "hobby_id") val id: Long = 0, // 爱好ID
@ColumnInfo(name = "user_id") var user_id: Long, // 用户ID
@ColumnInfo(name = "book_id") var book_id: Long // 图书ID
)
其中,喜好表使用了外键和索引。
ForeignKey:外键
Index:索引
当表存在外键时,表和表之间会存在关联,当删除表或者表中数据时,会删除失败,因为存在外键的关联关系,所以会删除失败。当然,以上代码添加了 onDelete = CASCADE
,当删除表或者一条记录时,对应关联表的记录也会一起删除,所以不会存在因为外键原因,删除失败的问题。
现在,有一个需求,即已知一条喜好数据,查询出对应的用户名和书名。
目前需求已经实现,如下图:
image.png下面直接贴一下剩下的代码,大家有兴趣的话可以自己调试一下,至于外键和索引的概念以及使用场景并不是本节重点内容。
Address.kt
/**
* 地址
*/
data class Address(
val street:String,val state:String,val city:String,val postCode:String
)
AppDataBase.kt
/**
* 数据库文件
*
* @Database 数据库注解
* entities 数据库中表格集合
* version 数据库版本号
* exportSchema 默认为true
* true:将数据库的配置导出来,存储到 ”/当前模块/schemas/AppDateBase路径/1.json“, 1为数据库版本号version
* false:数据库的配置不会导出来
*/
@Database(entities = [User::class, Book::class, Hobby::class],version = 1,exportSchema = false)
abstract class AppDataBase:RoomDatabase() {
// 得到UserDao
abstract fun userDao():UserDao
// 得到BookDao
abstract fun bookDao():BookDao
// 得到HobbyDao
abstract fun hobbyDao():HobbyDao
companion object{
@Volatile
private var instance:AppDataBase? = null
fun getInstance(context:Context):AppDataBase{
return instance?: synchronized(this) {
instance?:buildDataBase(context)
.also {
instance = it
}
}
}
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
// .allowMainThreadQueries() // 是否允许在主线程中执行数据库的增删改查
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 1、在表格中添加数据,如果不存在数据库,则新建数据库,执行回调
// 2、在表格中添加数据,如果已存在数据库,则不执行回调
}
})
.build()
}
}
}
BookDao.kt
@Dao
interface BookDao {
@Query("select * from book where book_id = :id")
fun getBookById(id: Long): Book
@Query("select * from book")
fun getAllBooks(): List<Book>
// OnConflictStrategy.REPLACE: 如果记录已存在,则直接替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addBook(book: Book)
@Delete
fun deleteBookByBook(book: Book)
@Query("delete from book where book_id = :id ")
fun deleteBookById(id: Long)
@Update
fun updateBookByBook(book: Book)
@Query("update book set book_name = :updateName where book_id = :id")
fun update(id: Long, updateName: String)
}
HobbyDao.kt
@Dao
interface HobbyDao {
@Query("select * from hobby where hobby_id = :id")
fun getHobbyById(id: Long): Hobby
@Query("select * from hobby")
fun getAllHobbys(): List<Hobby>
// OnConflictStrategy.REPLACE: 如果记录已存在,则直接替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addHobby(hobby: Hobby)
@Delete
fun deleteHobbyByHobby(hobby: Hobby)
@Query("delete from hobby where hobby_id = :id ")
fun deleteHobbyById(id: Long)
@Update
fun updateHobbyByHobby(hobby: Hobby)
}
UserDao.kt
@Dao
interface UserDao {
@Query("select * from user where user_id = :id")
fun getUserById(id: Long): User
@Query("select * from user")
fun getAllUsers(): List<User>
// OnConflictStrategy.REPLACE: 如果记录已存在,则直接替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addUser(user: User)
@Delete
fun deleteUserByUser(user: User)
@Query("delete from user where user_id = :id ")
fun deleteUserById(id: Long)
@Update
fun updateUserByUser(user: User)
@Query("update user set user_name = :updateName where user_id = :id")
fun update(id: Long, updateName: String)
}
RoomActivity.kt
class RoomActivity : ScopedActivity() {
private lateinit var binding : ActivityRoomBinding
private var userDao: UserDao? = null
private var bookDao: BookDao? = null
private var hobbyDao: HobbyDao? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRoomBinding.inflate(layoutInflater)
setContentView(binding.root)
userDao = AppDataBase.getInstance(applicationContext).userDao()
bookDao = AppDataBase.getInstance(applicationContext).bookDao()
hobbyDao = AppDataBase.getInstance(applicationContext).hobbyDao()
binding.userInsert.setOnClickListener { // 添加用户
launch(Dispatchers.Default + coroutineExceptionHandler) { // 数据库操作不能放在主线程
val address = Address("xxx街道", "xxxStatus", "xxx城市", "xxx邮编")
val userList = ArrayList<User>()
userList.add(User(1, "NobugException1", "123456", "zhangsan1", address, 1))
userList.add(User(2, "NobugException2", "123456", "zhangsan2", address, 1))
userList.add(User(3, "NobugException3", "123456", "zhangsan3", address, 1))
userList.add(User(4, "NobugException4", "123456", "zhangsan4", address, 1))
userList.add(User(5, "NobugException5", "123456", "zhangsan5", address, 1))
userList.add(User(6, "NobugException6", "123456", "zhangsan6", address, 1))
userList.add(User(7, "NobugException7", "123456", "zhangsan7", address, 1))
for (index in userList.indices) {
userDao?.addUser(userList[index])
}
}
}
binding.bookInsert.setOnClickListener { // 插入书籍
launch(Dispatchers.Default + coroutineExceptionHandler) {
val bookList = ArrayList<Book>()
bookList.add(Book(1, "Java疯狂讲义", 58F))
bookList.add(Book(2, "C++从入门到放弃", 78F))
bookList.add(Book(3, "智能家居入门篇", 68F))
for (index in bookList.indices) {
bookDao?.addBook(bookList[index])
}
}
}
binding.hobbyInsert.setOnClickListener { // 插入喜好
launch(Dispatchers.Default + coroutineExceptionHandler) {
val hobbyList = ArrayList<Hobby>()
hobbyList.add(Hobby(1, 1, 1))
hobbyList.add(Hobby(2, 2, 2))
hobbyList.add(Hobby(3, 3, 3))
hobbyList.add(Hobby(4, 4, 1))
hobbyList.add(Hobby(5, 5, 2))
hobbyList.add(Hobby(6, 6, 3))
hobbyList.add(Hobby(7, 7, 1))
for (index in hobbyList.indices) {
hobbyDao?.addHobby(hobbyList[index])
}
}
}
binding.searchByBobby.setOnClickListener { // 根据喜好,查询用户名和图书名
launch(Dispatchers.Default + coroutineExceptionHandler) {
// 已知喜好数据
val hobby = Hobby(5, 5, 2)
// 根据喜好查询用户名
val user = userDao?.getUserById(hobby.user_id)
val book = bookDao?.getBookById(hobby.book_id)
if (user == null || book == null) {
binding.textContent.text = "查找不到数据"
return@launch
}
binding.textContent.text = user?.name + "喜欢的书籍是:《" + book.name + "》"
}
}
}
}
ScopedActivity.kt
abstract class ScopedActivity: AppCompatActivity(), CoroutineScope by MainScope(){
protected val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineCotext, exception ->
println("exception:${exception}")
val exceptionStacks = exception.stackTrace
for (index in exceptionStacks.indices) {
println("exception:" + exceptionStacks[index])
}
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
表格数据如下:
image.png image.png image.png image.png从表中可以看到具体的数据,已经将外键的 On Delete
属性设置为 CASCADE
之后可以成功删除数据。
七、升级
涉及到数据库升级,大部分场景是新增一个字段和删除一个字段。
【1】新增字段:
user表中新增user_temp字段:
/**
* 用户表
*/
@Entity(tableName = "user")
data class User (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "user_id") val id: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int,
@ColumnInfo(name = "user_temp") val temp: Int
)
AppDataBase中添加升级代码:
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
.addMigrations(MIGRATION_1_2)
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
}
})
.build()
}
Room采用 Migration 升级方式,addMigrations
函数可以添加升级策略:
// 当数据库第一次被操作时,并且满足1-2升级条件时,则从1升级到2
private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//执行升级相关操作,新增user_temp字段
database.execSQL("ALTER TABLE user ADD COLUMN user_temp INTEGER NOT NULL DEFAULT 1")
}
}
用户表中添加 user_temp 字段:
/**
* 用户表
*/
@Entity(tableName = "user")
data class User (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "user_id") val id: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int,
@ColumnInfo(name = "user_temp") val temp: Int
)
将数据库版本改成2:
@Database(entities = [User::class, Book::class, Hobby::class],version = 2,exportSchema = false)
当数据库版本为2的app覆盖数据库版本为1的app时,并且app覆盖安装之后,数据库被第一次操作,那么将触发从1到2的升级代码。
以此类推,也可以添加 从2升级到3、从3升级到4,从1升级到4,从2升级到4
的策略。
新增字段比较简单,但是Room不支持直接删除一个字段,想要删除一个字段,必须要按如下步骤操作:
假如需要删除 user 表的 user_temp 字段,那么步骤如下:
1、创建user_temp表,user_temp字段要去掉
2、将user中的数据复制到user_temp表
3、删除user表
4、 将user_temp表重命名成user
用户表中删除 user_temp 字段:
/**
* 用户表
*/
@Entity(tableName = "user")
data class User (
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "user_id") val id: Long = 0,
@ColumnInfo(name = "user_account") val account: String, // 账号
@ColumnInfo(name = "user_pwd") val pwd: String, // 密码
@ColumnInfo(name = "user_name") val name: String,
@Embedded var address: Address, // 地址
@ColumnInfo(name = "user_status") val status: Int
// @ColumnInfo(name = "user_temp") val temp: Int
)
添加2-3升级策略:
// 当数据库第一次被操作时,并且满足2-3升级条件时,则从2升级到3
private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
//执行升级相关操作,删除user_temp字段
// 创建user_temp表,user_temp字段要去掉
database.execSQL("CREATE TABLE user_temp (" +
"user_id INTEGER PRIMARY KEY NOT NULL, " +
"user_account TEXT NOT NULL, " +
"user_pwd TEXT NOT NULL, " +
"user_name TEXT NOT NULL, " +
"street TEXT NOT NULL, " +
"state TEXT NOT NULL, " +
"city TEXT NOT NULL, " +
"postCode TEXT NOT NULL, " +
"user_status INTEGER NOT NULL)")
// 将user中的数据复制到user_temp表
database.execSQL("INSERT INTO user_temp (user_id, user_account, user_pwd, user_name, street, state, city, postCode, user_status) " +
"SELECT user_id, user_account, user_pwd, user_name, street, state, city, postCode, user_status " +
"FROM user")
// 删除user表
database.execSQL("DROP TABLE user")
// 将user_temp表重命名成user
database.execSQL("ALTER TABLE user_temp RENAME TO user")
}
}
添加策略:
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
最后, 修改数据库版本号:
@Database(entities = [User::class, Book::class, Hobby::class],version = 3,exportSchema = false)
八、数据库的异常处理
在升级的过程中,可能会发生异常,可能导致异常的原因有:
1、数据库version过高,并且没有对应的升级策略;
2、数据库version过低,数据库版本回退;
3、CURD操作失败
一旦数据库发生异常,那么数据库就不可用,且无法恢复,除非卸载并重装应用,或者手动删除本地的数据库文件。
基于数据库无法恢复的问题,我们还需要添加数据库异常处理:
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
// .allowMainThreadQueries() // 是否允许在主线程中执行数据库的增删改查
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration()
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 1、在表格中添加数据,如果不存在数据库,则新建数据库,执行回调
// 2、在表格中添加数据,如果已存在数据库,则不执行回调
}
})
.build()
}
添加 fallbackToDestructiveMigration
函数可以规避数据库异常,一旦发生异常,数据库的数据将全部清除或者部分清除,但是表结构还在,并且数据库可恢复。
九、Schema文件
Room在每次数据库升级的过程中,都会导出一个Schema文件,这是一个json格式的文件,其中包含了数据库的基本信息,有了该文件,开发者能清楚的知道数据库的历次变更情况,最大程序地帮助开发者排查问题。
在定义数据库类时,将 exportSchema
改成 true,并且在build.gradle文件中添加配置:
defaultConfig {
...
javaCompileOptions { // 配置Java编译的选项
annotationProcessorOptions { // 在注解处理器选项中配置一个key-value
// 指定schema导出位置
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
schema导出位置如下:
image.pngschema目录下存在3个文件:1.json、2.json、3.json,1、2、3分别表示数据库版本。
1.json:数据库版本为1时的配置和操作信息;
2.json:数据库版本为2时的配置和操作信息;
3.json:数据库版本为3时的配置和操作信息;
这些文件可以帮助开发者配置数据库升级问题。
十、预填充数据库
首先准备一个数据库文件,放入assets目录下:
image.png然后与填充该数据库:
private fun buildDataBase(context: Context):AppDataBase{
return Room
.databaseBuilder(context,AppDataBase::class.java,"room.db")
// .allowMainThreadQueries() // 是否允许在主线程中执行数据库的增删改查
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration()
.createFromAsset("db/student.db")
.addCallback(object :RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 1、在表格中添加数据,如果不存在数据库,则新建数据库,执行回调
// 2、在表格中添加数据,如果已存在数据库,则不执行回调
}
})
.build()
}
createFromAsset
可以实现预填充数据库,当app一次启动后第一次操作数据库之后,将 student.db 中的表格放入 room 数据库中。
[本章完...]
网友评论