美文网首页
JetPack<第七篇>:Room

JetPack<第七篇>:Room

作者: NoBugException | 来源:发表于2022-09-28 11:41 被阅读0次

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.png

schema目录下存在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 数据库中。

image.png

[本章完...]

相关文章

网友评论

      本文标题:JetPack<第七篇>:Room

      本文链接:https://www.haomeiwen.com/subject/njefartx.html