2025年3月16日 星期日 甲辰(龙)年 月十五 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

android 开发-Room的使用

时间:02-07来源:作者:点击数:34

一、Room基本使用

  1. 添加插件
    • id 'kotlin-kapt'
  2. 添加依赖
    • implementation "androidx.room:room-runtime:2.3.0"
    • kapt "androidx.room:room-compiler:2.3.0"
  3. 布局
    在这里插入图片描述
    • <?xml version="1.0" encoding="utf-8"?>
    • <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    • xmlns:app="http://schemas.android.com/apk/res-auto"
    • xmlns:tools="http://schemas.android.com/tools"
    • android:layout_width="match_parent"
    • android:layout_height="match_parent"
    • tools:context=".MainActivity">
    • <EditText
    • android:id="@+id/salary_edit"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:ems="10"
    • android:inputType="numberDecimal"
    • app:layout_constraintBaseline_toBaselineOf="@+id/salary_text"
    • app:layout_constraintEnd_toEndOf="parent"
    • app:layout_constraintHorizontal_bias="0.0"
    • app:layout_constraintStart_toStartOf="@+id/guideline3" />
    • <Button
    • android:id="@+id/add_user_button"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:text="添加用户"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintHorizontal_bias="0.498"
    • app:layout_constraintLeft_toLeftOf="parent"
    • app:layout_constraintRight_toRightOf="parent"
    • app:layout_constraintTop_toBottomOf="@+id/salary_edit"
    • app:layout_constraintVertical_bias="0.051" />
    • <TextView
    • android:id="@+id/name_text"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:text="请输入名称:"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintEnd_toEndOf="@+id/age_text"
    • app:layout_constraintEnd_toStartOf="@+id/guideline2"
    • app:layout_constraintTop_toTopOf="parent"
    • app:layout_constraintVertical_bias="0.11" />
    • <androidx.constraintlayout.widget.Guideline
    • android:id="@+id/guideline2"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:orientation="vertical"
    • app:layout_constraintGuide_begin="123dp" />
    • <TextView
    • android:id="@+id/age_text"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:text="请输入年龄:"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintEnd_toStartOf="@+id/guideline2"
    • app:layout_constraintTop_toBottomOf="@+id/name_text"
    • app:layout_constraintVertical_bias="0.058" />
    • <TextView
    • android:id="@+id/salary_text"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:layout_marginStart="72dp"
    • android:text="请输入薪水:"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintEnd_toStartOf="@+id/guideline2"
    • app:layout_constraintHorizontal_bias="1.0"
    • app:layout_constraintStart_toStartOf="parent"
    • app:layout_constraintTop_toBottomOf="@+id/age_text"
    • app:layout_constraintVertical_bias="0.066" />
    • <androidx.constraintlayout.widget.Guideline
    • android:id="@+id/guideline3"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:orientation="vertical"
    • app:layout_constraintGuide_begin="132dp" />
    • <EditText
    • android:id="@+id/name_edit"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:ems="10"
    • android:inputType="textPersonName"
    • app:layout_constraintBaseline_toBaselineOf="@+id/name_text"
    • app:layout_constraintEnd_toEndOf="parent"
    • app:layout_constraintHorizontal_bias="0.0"
    • app:layout_constraintStart_toStartOf="@+id/guideline3" />
    • <EditText
    • android:id="@+id/age_edit"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:ems="10"
    • android:inputType="number"
    • app:layout_constraintBaseline_toBaselineOf="@+id/age_text"
    • app:layout_constraintEnd_toEndOf="parent"
    • app:layout_constraintHorizontal_bias="0.0"
    • app:layout_constraintStart_toStartOf="@+id/guideline3" />
    • <Button
    • android:id="@+id/query_user_button"
    • android:layout_width="wrap_content"
    • android:layout_height="wrap_content"
    • android:text="查询用户"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintEnd_toEndOf="parent"
    • app:layout_constraintHorizontal_bias="0.498"
    • app:layout_constraintStart_toStartOf="parent"
    • app:layout_constraintTop_toBottomOf="@+id/add_user_button"
    • app:layout_constraintVertical_bias="0.021" />
    • <ScrollView
    • android:layout_width="match_parent"
    • android:layout_height="wrap_content"
    • app:layout_constraintBottom_toBottomOf="parent"
    • app:layout_constraintEnd_toEndOf="parent"
    • app:layout_constraintStart_toStartOf="parent"
    • app:layout_constraintVertical_bias="0.04"
    • app:layout_constraintTop_toBottomOf="@+id/query_user_button">
    • <LinearLayout
    • android:layout_width="match_parent"
    • android:layout_height="wrap_content"
    • android:orientation="vertical">
    • <TextView
    • android:id="@+id/users_text"
    • android:layout_width="match_parent"
    • android:layout_height="wrap_content"/>
    • </LinearLayout>
    • </ScrollView>
    • </androidx.constraintlayout.widget.ConstraintLayout>
  4. 代码
    • @Entity
    • data class User(
    • @PrimaryKey(autoGenerate = true)
    • val uid: Int?,
    • @ColumnInfo(name = "user_name")
    • val name: String,
    • val age: Int,
    • val salary: Float
    • )
    • @Dao
    • interface UserDao {
    • @Query("SELECT * FROM user")
    • fun getAll(): List<User>
    • @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    • fun loadAllByIds(userIds: IntArray): List<User>
    • @Query("SELECT * FROM user WHERE user_name LIKE :name LIMIT 1")
    • fun findByName(name: String): User
    • @Insert
    • fun insertAll(vararg users: User)
    • @Delete
    • fun delete(user: User)
    • }
    • @Database(entities = [User::class], version = 1)
    • abstract class AppDatabase : RoomDatabase() {
    • abstract fun userDao(): UserDao
    • }
    • class MainActivity : AppCompatActivity() {
    • private lateinit var database: AppDatabase
    • private lateinit var userDao: UserDao
    • override fun onCreate(savedInstanceState: Bundle?) {
    • super.onCreate(savedInstanceState)
    • setContentView(R.layout.activity_main)
    • findViewById<Button>(R.id.add_user_button).setOnClickListener(::addUser)
    • findViewById<Button>(R.id.query_user_button).setOnClickListener(::queryUser)
    • thread {
    • database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "mydb.sqlite").build()
    • userDao = database.userDao()
    • }
    • }
    • fun addUser(view: View) {
    • val name = findViewById<EditText>(R.id.name_edit).text.toString()
    • val age = findViewById<EditText>(R.id.age_edit).text.toString().toInt()
    • val salary = findViewById<EditText>(R.id.salary_edit).text.toString().toFloat()
    • thread { userDao.insertAll(User(null, name, age, salary)) }
    • }
    • fun queryUser(view: View) {
    • thread {
    • val users = userDao.getAll()
    • val sb = StringBuilder()
    • users.forEach { sb.append(it).append('\n') }
    • runOnUiThread {
    • findViewById<TextView>(R.id.users_text).text = sb
    • }
    • }
    • }
    • override fun onDestroy() {
    • super.onDestroy()
    • database.close()
    • }
    • }

二、导出Schema(数据库)

Schema有架构、纲目结构的意思,它其实代表的就是数据库的架构,通过这个架构我们就能创建数据库,所以Schema就是描述你的数据库长什么样,包括你数据库里面有哪些表,表里有哪些列,列是什么类型的等等的信息。在做开发时,数据库版本可能会修改,比如随着开发的进行,可能需要在一个表中增加多一些列,又或者增加多一些表,但是后面又想恢复回之前的数据库版本怎么办?或者想和前一个版本比较有什么改动怎么办?这时,我们就可以把每个版本的数据库的Schema导出来,比如第1个版本的数据库Schema导出来保存为1.json,第2个版本的数据库Schema导出来保存为2.json,每一个版本都使用版本号保存为对应的json,这样方便以后查看每个版本的数据库长什么样,或者方便我们进行恢复。

前面的例子,运行的时候会有一个警告,如下:

在这里插入图片描述

完整的警告信息如下:

警告: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provideroom.schemaLocationannotation processor argument OR set exportSchema to false.

翻译为中文如下:

警告: 没有为注解处理器提供Schema导出目录,所以我们不能导出schema。你可以提供room.schemaLocation注解处理器参数,或者设置exportSchema为false。

如何设置exportSchema为false我就不去找了,因为做为一个合格的开发人员肯定是要选择导出schema的。官方文档在此。示例如下:

  • android {
  • ...
  • defaultConfig {
  • ...
  • javaCompileOptions {
  • annotationProcessorOptions {
  • arguments += ["room.schemaLocation":"$projectDir/schemas".toString()]
  • }
  • }
  • }
  • }

设置好之后再运行项目就不会有那个警告了。导出的schema如下:

在这里插入图片描述

这是以数据库的版本号为文件名的,1.json中保存的内容就是版本为1的数据库的schema。json文件内容如下:

  • {
  • "formatVersion": 1,
  • "database": {
  • "version": 1,
  • "identityHash": "fd5050cafafa7b9afe3159a01557d8e1",
  • "entities": [
  • {
  • "tableName": "User",
  • "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `user_name` TEXT NOT NULL, `age` INTEGER NOT NULL, `salary` REAL NOT NULL)",
  • "fields": [
  • {
  • "fieldPath": "uid",
  • "columnName": "uid",
  • "affinity": "INTEGER",
  • "notNull": false
  • },
  • {
  • "fieldPath": "name",
  • "columnName": "user_name",
  • "affinity": "TEXT",
  • "notNull": true
  • },
  • {
  • "fieldPath": "age",
  • "columnName": "age",
  • "affinity": "INTEGER",
  • "notNull": true
  • },
  • {
  • "fieldPath": "salary",
  • "columnName": "salary",
  • "affinity": "REAL",
  • "notNull": true
  • }
  • ],
  • "primaryKey": {
  • "columnNames": [
  • "uid"
  • ],
  • "autoGenerate": true
  • },
  • "indices": [],
  • "foreignKeys": []
  • }
  • ],
  • "views": [],
  • "setupQueries": [
  • "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
  • "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fd5050cafafa7b9afe3159a01557d8e1')"
  • ]
  • }
  • }

三、实体类不要有多个构造函数

如前面例子中的User类,修改为如下:

  • @Entity
  • data class User(
  • @PrimaryKey(autoGenerate = true)
  • val uid: Int? = null,
  • @ColumnInfo(name = "user_name")
  • val name: String = "",
  • val age: Int = 0,
  • val salary: Float = 3000f
  • )

这样其实会生成多个构造函数,比如我们可以这样调用这个构造函数:

  • val user = User(name = "Even", age = 18)

Kotlin底层其实是通过生成多个重载的构造函数来实现的。

如果此时运行项目,也会报出一个异常,如下:

在这里插入图片描述

完整的警告信息如下:

警告: There are multiple good constructors and Room will pick the no-arg constructor. You can use the @Ignore annotation to eliminate unwanted constructors.

翻译为中文如下:

警告: 有多个构造函数,Room将选择无参数构的造函数。您可以使用@Ignore注释来消除不需要的构造函数。

因为我们使用的是Kotlin的默认参数来实现的多构造函数,所以没有办法使用@Ignore注释来消除不需要的构造函数。

虽然说这个警告不影响运行,但是做为强迫症是不允许出现警告的,我们可以去掉默认参数,然后添加工厂方法来实现重载,如下:

  • @Entity
  • data class User(
  • @PrimaryKey(autoGenerate = true)
  • val uid: Int?,
  • @ColumnInfo(name = "user_name")
  • val name: String,
  • val age: Int,
  • val salary: Float
  • ) {
  • companion object {
  • fun createUser(
  • uid: Int? = null,
  • name: String = "",
  • age: Int = 0,
  • salary: Float = 3000f
  • ) = User(uid, name, age, salary)
  • }
  • }

在需要创建User的时候,就可以使用createUser这个函数了,如下:

  • val user = User.createUser(name = "Even", age = 18)

这样再次运行时就不会提示有多个构造函数的警告了。

四、升级数据库(Migrating Room databases)

第一次升级

什么时候需要升级数据库?比如修改表结果,或者删除一个表,或者增加一个表,或者给一个已经存在的表增加多一表,或者删除一列等等,这时就需要以升级数据库的方式来进行。

用以前面的示例Demo为例,假设我们要给User表增加多一列,比如加一个地址,如下:

在这里插入图片描述

对应的User类需要添加一个address属性,如下:

  • @Entity
  • data class User(
  • @PrimaryKey(autoGenerate = true)
  • val uid: Int?,
  • @ColumnInfo(name = "user_name")
  • val name: String,
  • val age: Int,
  • val salary: Float,
  • val address: String
  • ) {
  • companion object {
  • fun createUser(
  • uid: Int? = null,
  • name: String = "",
  • age: Int = 0,
  • salary: Float = 3000f,
  • address: String
  • ) = User(uid, name, age, salary, address)
  • }
  • }

相应的MainActivity中需要给address属性赋值。然后,如果此时运行App,程序是正常运行的,但是点击“查询用户”按钮的时候,程序将会崩溃,异常如下:

  • AndroidRuntime: FATAL EXCEPTION: Thread-3
  • Process: cn.dazhou.roomhaha, PID: 5688
  • java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
  • at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154)
  • at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:195)
  • at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:266)
  • at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
  • at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
  • at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
  • at cn.dazhou.roomhaha.UserDao_Impl.getAll(UserDao_Impl.java:104)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:43)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:42)
  • at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

异常的提示信息如下:

Room cannot verify the data integrity. Looks like you’ve changed schema but forgot to update the version number. You can simply fix this by increasing the version number.

翻译为中文如下:

Room无法验证数据的完整性。看起来像是您已经更改了schema,但是忘记更新版本号。您可以通过增加版本号来简单地解决这个问题。

OK,那我们就增加一下数据库的版本号,如下:

  • @Database(entities = [User::class], version = 2)
  • abstract class AppDatabase : RoomDatabase() {
  • abstract fun userDao(): UserDao
  • }

我们把版本号1修改为2,然后重新运行,再次点击“查询用户”按钮,异常如下:

  • AndroidRuntime: FATAL EXCEPTION: Thread-3
  • Process: cn.dazhou.roomhaha, PID: 6360
  • java.lang.IllegalStateException: A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.
  • at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.java:117)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:177)
  • at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:256)
  • at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
  • at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
  • at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
  • at cn.dazhou.roomhaha.UserDao_Impl.getAll(UserDao_Impl.java:104)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:42)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:41)
  • at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

异常的提示信息如下:

A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration …) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

翻译为中文如下:

需要一个从1到2的迁移(migration),但没有找到。请提供必要的迁移路径(Migration path)通过RoomDatabase.Builder.addMigration(Migration …)或允许破坏性的迁移通过一个RoomDatabase.Builder.fallbackToDestructiveMigration*方法。

不知道为什么不叫升级,而叫迁移(migration),有可能Android框架内部是把先把数据库备份,然后删除当前数据库,然后创建一个新的数据库,然后把旧的数据库中的数据迁移到新数据库中,所以才使用迁移这个词。官方在此教程:迁移Room数据库。在本示例中,完成迁移的代码如下:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT")
  • }
  • }
  • database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "mydb.sqlite")
  • .addMigrations(migration_1_to_2)
  • .build()

可以看到,Migration类的功能就是要添加SQL语句来执行数据库的变更部分,如上面的代码功能为:为User表添加一列,Text类型的address列。再次运行示例,点击“查询用户”按钮,还是挂异常,如下:

  • AndroidRuntime: FATAL EXCEPTION: Thread-3
  • Process: cn.dazhou.roomhaha, PID: 6609
  • java.lang.IllegalStateException: Migration didn't properly handle: User(cn.dazhou.roomhaha.User).
  • Expected:
  • TableInfo{name='User', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, uid=Column{name='uid', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, user_name=Column{name='user_name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, salary=Column{name='salary', type='REAL', affinity='4', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
  • Found:
  • TableInfo{name='User', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, uid=Column{name='uid', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, user_name=Column{name='user_name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, salary=Column{name='salary', type='REAL', affinity='4', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
  • at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.java:103)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:177)
  • at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:256)
  • at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
  • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
  • at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
  • at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
  • at cn.dazhou.roomhaha.UserDao_Impl.getAll(UserDao_Impl.java:104)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:50)
  • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:49)
  • at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

看主要的异常提示信息,如下:

Migration didn’t properly handle: User(cn.dazhou.roomhaha.User).

翻译如下:

迁移没有正确处理:User(cn.dazhou.roomhaha.User)。

而且我们看异常中还有显示TableInfo(表信息),部分信息如下:

TableInfo{name=‘User’, columns={address=Column{name=‘address’, type=‘TEXT’, affinity=‘2’, notNull=true, primaryKeyPosition=0, defaultValue=‘null’}

可以看到,address这个列的notNull=true,说明address列不允许为null。这应该是发生在执行迁移操作的时候,迁移的时候Android框架不知道如何处理之前旧的记录,因为那些记录没有address值。基于这个猜想,我把数据库导出到电脑,使用DataGrip查看一下User表,如下:

在这里插入图片描述

果然,address列还没有添加进来呢,这说明下面的语句执行之后升级数据库的代码还没有执行:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT")
  • }
  • }
  • database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "mydb.sqlite")
  • .addMigrations(migration_1_to_2)
  • .build()
  • userDao = database.userDao()

因为这个语句是在App运行的时候就调用了的,如果此时execSQL中的代码有被执行的话,则App一运行应该就会崩溃了。我们也可以加入一个输出语句来验证这个情况,如下:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • Log.i("MainActivity", "升级语句被执行")
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT")
  • }
  • }

再次运行项目,输出语句并没有打印,说明了我们的猜想是对的,此时点击“查询用户”按钮,这个时候Android框架才去执行升级语句,因为此时输出语句被打印出来了,而且随即程序就崩溃了,如下:

  • 2021-11-30 15:36:04.737 7104-7212/cn.dazhou.roomhaha I/MainActivity: 升级语句被执行
  • --------- beginning of crash
  • 2021-11-30 15:36:04.746 7104-7212/cn.dazhou.roomhaha E/AndroidRuntime: FATAL EXCEPTION: Thread-3
  • Process: cn.dazhou.roomhaha, PID: 7104
  • java.lang.IllegalStateException: Migration didn't properly handle: User(cn.dazhou.roomhaha.User).
  • 。。。

既然知道了原因,那我们就为address加上一个默认值,使其不会为null,如下:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • Log.i("MainActivity", "升级语句被执行")
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT DEFAULT ''")
  • }
  • }

这里需要特别注意,我们是在升级语句中给address列添加了默认值,值为一个空字符串。但是,如果用户没有安装版本为1的app,而是直接安装版本为2的app,则不会执行到这个升级语句,所以,为了保证一致性,User类中也需要为address添加同样的设置,在User的address属性上添加如下注解:

  • @ColumnInfo(defaultValue = "")
  • val address: String

再次运行,还是崩溃,依旧是之前的异常,这就奇怪了,都加了默认值了还不行。导出数据查看User表中依旧是没有address列的,说明数据库升级还是没成功,那SQL语句执行完成了没有啊?抱着这个疑问,我再增加一个输出语句,如下:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • Log.i("MainActivity", "升级语句准备执行")
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT DEFAULT ''")
  • Log.i("MainActivity", "升级语句执行完毕")
  • }
  • }

再次运行App,并点击“查询用户”按钮,输出 如下:

  • 2021-11-30 15:57:39.473 8142-8257/cn.dazhou.roomhaha I/MainActivity: 升级语句准备执行
  • 2021-11-30 15:57:39.474 8142-8257/cn.dazhou.roomhaha I/MainActivity: 升级语句执行完毕
  • --------- beginning of crash
  • 2021-11-30 15:57:39.483 8142-8257/cn.dazhou.roomhaha E/AndroidRuntime: FATAL EXCEPTION: Thread-3
  • Process: cn.dazhou.roomhaha, PID: 8142
  • java.lang.IllegalStateException: Migration didn't properly handle: User(cn.dazhou.roomhaha.User).
  • 。。。

可以看到SQL语句执行完了,后面才挂的异常。

这真是见了鬼了,几经折磨,后来发现其实不是见了鬼了,而是自己学业不精,我们来看看升级数据库时的两个改变,如下:

  1. 升级的SQL语句:
    • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT DEFAULT ''")
  2. 升级的User类
    • @Entity
    • data class User(
    • @PrimaryKey(autoGenerate = true)
    • val uid: Int?,
    • @ColumnInfo(name = "user_name")
    • val name: String,
    • val age: Int,
    • val salary: Float,
    • @ColumnInfo(defaultValue = "")
    • val address: String
    • )

    我们之前说过,如果App从版本1升级到2,那么它会执行SQL的升级语句,而如果用户没安装版本1,而是直接就安装版本2了,则升级的SQL语句不会执行,此时Room会直接通过User类帮我们创建对应的表,包含address列。所以,不论是使用SQL语句创建的address列,还是Room通过User类创建的address列,列的结构应该一样,但是我们这里却并不一样,因为User类中的address是非空的,所以Room会创建非空的address列,而我们写的SQL语句中创建的address是可空的,在升级数据库的时候,Room会验证SQL语句中的address和User中的address的结构,如果它们的结构不一样,则会抛出异常,这就是我们之前看到的那个异常,如下:

    • java.lang.IllegalStateException: Migration didn't properly handle: User(cn.dazhou.roomhaha.User).

    我们只是看了异常前面的信息,其实关键的信息在后面,如下:

    • AndroidRuntime: FATAL EXCEPTION: Thread-3
    • Process: cn.dazhou.roomhaha, PID: 6609
    • java.lang.IllegalStateException: Migration didn't properly handle: User(cn.dazhou.roomhaha.User).
    • Expected:
    • TableInfo{name='User', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, uid=Column{name='uid', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, user_name=Column{name='user_name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, salary=Column{name='salary', type='REAL', affinity='4', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
    • Found:
    • TableInfo{name='User', columns={address=Column{name='address', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}, uid=Column{name='uid', type='INTEGER', affinity='3', notNull=false, primaryKeyPosition=1, defaultValue='null'}, age=Column{name='age', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}, user_name=Column{name='user_name', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}, salary=Column{name='salary', type='REAL', affinity='4', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
    • at androidx.room.RoomOpenHelper.onUpgrade(RoomOpenHelper.java:103)
    • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onUpgrade(FrameworkSQLiteOpenHelper.java:177)
    • at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:256)
    • at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)
    • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
    • at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
    • at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
    • at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)
    • at cn.dazhou.roomhaha.UserDao_Impl.getAll(UserDao_Impl.java:104)
    • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:50)
    • at cn.dazhou.roomhaha.MainActivity$queryUser$1.invoke(MainActivity.kt:49)
    • at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

    这里的关键信息如下:

    • Expected:
    • TableInfo 。。。
    • Found:
    • TableInfo 。。。

    哎,明明人家的异常信息已经说的很清楚了,我们就是眼瞎,所以以后看异常信息的时候不要错过任何的信息了,一定要看完整了,这里它期望的是第一个TableInfo,但是得到的是第二个TableInfo,那我们就对比一下这两个表信息哪里不同就能很快的知道原因了,发现不同的地方如下:

    • {address=Column{name='address', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=0, defaultValue='null'}
    • {address=Column{name='address', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0, defaultValue='null'}

    这里,第一行的address列的notNull=true,而第二行的address列的notNull=false,这就是原因了,User类中声明的address列属性为非空,即notNull=true,而SQL语句中声明的address列属性为可空,即notNull=false,找到原因就很好解决了,我们就可以让两处的address设计保持一样即可,比如把SQL语句中的address添加非空声明,如下:

  • val migration_1_to_2 = object : Migration(1, 2) {
  • override fun migrate(database: SupportSQLiteDatabase) {
  • Log.i("MainActivity", "升级语句准备执行")
  • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT NOT NULL DEFAULT ''")
  • Log.i("MainActivity", "升级语句执行完毕")
  • }
  • }

此时再运行App,再点击“查询用户”按钮,效果如下:

在这里插入图片描述

可以看到查询出来一条记录,这条记录是在数据库版本为1的时候就插入进去的,当时还没有address列,后来升级到版本2后有了address列,因为我们设计的默认值为空字符串,所以旧记录的address值就为空字符串。下面我们添加一条新用户,并再次查询,效果如下:

在这里插入图片描述

第二次升级

前面我们已经进行了一次升级了,如果需要再一次进行升级,则此时的版本号应该为3,而且之前的升级代码不能删除,因为如果用户只安装了版本1的话,此时要升级版本3,则它需要先升级到版本2,再从2升级到3。比如我们这次要删除salary,嗯,一搞就搞到神奇的事情,SQLite中不支持删除列的操作,那我们就来个改名操作吧,把User表名改为Employee,并增加一个部门id(department_id),代码如下:

  1. 版本2改变3
    • @Database(entities = [User::class], version = 3)
    • abstract class AppDatabase : RoomDatabase() {
    • abstract fun userDao(): UserDao
    • }
  2. 升级数据库的SQL语句
    • val migration_1_to_2 = object : Migration(1, 2) {
    • override fun migrate(database: SupportSQLiteDatabase) {
    • database.execSQL("ALTER TABLE User ADD COLUMN address TEXT NOT NULL DEFAULT ''")
    • }
    • }
    • val migration_2_to_3 = object : Migration(2, 3) {
    • override fun migrate(database: SupportSQLiteDatabase) {
    • database.execSQL("ALTER TABLE User RENAME TO Employee")
    • database.execSQL("ALTER TABLE Employee ADD COLUMN deparment_id INTEGER NOT NULL DEFAULT 0")
    • }
    • }
    • database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "mydb.sqlite")
    • .addMigrations(migration_1_to_2, migration_2_to_3)
    • .build()
  3. 对应的User类要重命名为Employee,并增加deparmentId属性,如下:
    • @Entity
    • data class Employee(
    • @PrimaryKey(autoGenerate = true)
    • val uid: Int?,
    • @ColumnInfo(name = "user_name")
    • val name: String,
    • val age: Int,
    • val salary: Float,
    • @ColumnInfo(defaultValue = "")
    • val address: String,
    • @ColumnInfo(name = "deparment_id", defaultValue = "0")
    • val deparmentId: Int
    • ) {
    • companion object {
    • fun createEmployee(
    • uid: Int? = null,
    • name: String = "",
    • age: Int = 0,
    • salary: Float = 3000f,
    • address: String,
    • deparmentId: Int,
    • ) = Employee(uid, name, age, salary, address, deparmentId)
    • }
    • }
  4. 因为修改了表名,所以UserDao中的查询语句中的表也需要修改,类名也改为EmployeeDao,如下:
    • @Dao
    • interface EmployeeDao {
    • @Query("SELECT * FROM Employee")
    • fun getAll(): List<Employee>
    • @Query("SELECT * FROM Employee WHERE uid IN (:userIds)")
    • fun loadAllByIds(userIds: IntArray): List<Employee>
    • @Query("SELECT * FROM Employee WHERE user_name LIKE :name LIMIT 1")
    • fun findByName(name: String): Employee
    • @Insert
    • fun insertAll(vararg employees: Employee)
    • @Delete
    • fun delete(employee: Employee)
    • }
    因为我们修改了Dao,Room会根据Dao生成一些实现类,所以我们再好Clean一下,以清除之前生成的那些UserDao的生成类,然后再次运行App,点击“查询用户”按钮,效果如下:
    在这里插入图片描述
    顺便把数据库导出到DataGrip中看看结构,如下:
    在这里插入图片描述
    OK,升级顺利完成!

第三次升级(添加一个新表)

自动升级(自动迁移)

在Android文档中,使用English来查看,不要看中文的,因为中文的没把自动升级这个内容加入过来,原文连接在此,官方说在2.4.0-alpha01或更高版本的Room支持自动迁移,目前(2021年11月30日)最新的正式版本还是2.3.0,所以不推荐使用,等到2.4.0的正式版出来我们再用,那时候它即支持自动迁移,也支持手动迁移,像给一个表添加列的话使用自动迁移就行了,像一些特殊的情况,比如修改表名这种操作,估计还是得用手动迁移。具体的自动迁移实现请观看官方文档,这里就暂时不写了。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门