id 'kotlin-kapt'
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
<?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>
@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导出来,比如第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)
这样再次运行时就不会提示有多个构造函数的警告了。
什么时候需要升级数据库?比如修改表结果,或者删除一个表,或者增加一个表,或者给一个已经存在的表增加多一表,或者删除一列等等,这时就需要以升级数据库的方式来进行。
用以前面的示例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语句执行完了,后面才挂的异常。
这真是见了鬼了,几经折磨,后来发现其实不是见了鬼了,而是自己学业不精,我们来看看升级数据库时的两个改变,如下:
database.execSQL("ALTER TABLE User ADD COLUMN address TEXT DEFAULT ''")
@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),代码如下:
@Database(entities = [User::class], version = 3)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
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()
@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)
}
}
@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,点击“查询用户”按钮,效果如下:在Android文档中,使用English来查看,不要看中文的,因为中文的没把自动升级这个内容加入过来,原文连接在此,官方说在2.4.0-alpha01或更高版本的Room支持自动迁移,目前(2021年11月30日)最新的正式版本还是2.3.0,所以不推荐使用,等到2.4.0的正式版出来我们再用,那时候它即支持自动迁移,也支持手动迁移,像给一个表添加列的话使用自动迁移就行了,像一些特殊的情况,比如修改表名这种操作,估计还是得用手动迁移。具体的自动迁移实现请观看官方文档,这里就暂时不写了。