美文网首页
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