您当前的位置:首页 > 计算机 > 编程开发 > 安卓(android)开发

android 开发-Room的使用

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

一、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的正式版出来我们再用,那时候它即支持自动迁移,也支持手动迁移,像给一个表添加列的话使用自动迁移就行了,像一些特殊的情况,比如修改表名这种操作,估计还是得用手动迁移。具体的自动迁移实现请观看官方文档,这里就暂时不写了。

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