room数据库升级
一、操作步骤说明
-
增加数据库版本号
在
@Database
注解中增加版本号(version),比如从version 1升级到version 2。sql@Database( entities = [ Song::class, ], **version = 1**,//1->2 ) abstract class AppDataBase : RoomDatabase() { }
-
定义数据库变化
根据需要修改的内容(添加表、修改表、删除表等),更新对应的Entity类和DAO接口
-
创建Migration对象
- 创建一个Migration对象,该对象定义了数据库从旧版本升级到新版本时需要执行的操作。
- 实现
migrate()
方法,编写SQL语句来处理结构变化或数据迁移
kotlinprivate val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { //升级操作 } }
-
配置数据库实例
- 在创建Room数据库实例时,通过
.addMigrations()
方法添加上一步创建的Migration对象。 - 如果有多个版本的迁移,可以链式调用
.addMigrations()
添加多个Migration对象。
kotlinvar appDatabase = Room.databaseBuilder( context, AppDataBase::class.java, DATABASE_NAME, ).apply { // 把初始下载权限表放这里了 createFromAsset("init.db") addMigrations( MIGRATION_1_2, ) }
- 在创建Room数据库实例时,通过
-
测试迁移
- 使用单元测试来确保Migration正确无误地执行了预期的数据库变化。
- 测试包括但不限于表结构变化、数据迁移的正确性、数据完整性等。
二、常见升级
表定义
sql
@Entity(tableName = "t_song")
data class Song(
@PrimaryKey
@ColumnInfo(name = "song_id")
val songId: String,//歌曲id
@ColumnInfo(name = "name")
val songName: String,//歌曲名称
@ColumnInfo(name = "type")
val songType: Int,//歌曲类型:1:歌曲 2:听书
)
2.1 增加一个普通字段、索引
2.1.1 Entity类修改
kotlin
@Entity(tableName = "t_song")
data class Song(
@PrimaryKey
@ColumnInfo(name = "song_id")
val songId: String,//歌曲id
@ColumnInfo(name = "name")
val songName: String,//歌曲名称
@ColumnInfo(name = "type")
val songType: Int,//歌曲类型:1:歌曲 2:听书
//新增albumId字段,且创建索引
@ColumnInfo(name = "album_id",index = true)
val albumId: String?=null,//专辑id
)
2.1.1 旧版本升级兼容即创建Migration对象,
kotlin
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//升级操作
//增加url字段
database.execSQL("ALTER TABLE t_songs ADD COLUMN `album_id` TEXT")
//增加索引
database.execSQL("CREATE INDEX IF NOT EXISTS `index_t_songs_album_id` ON `t_songs` (`album_id`)")
}
}
2.2 删除表
2.2.1 移除Entity和DAO:
- 从代码中移除
@Entity
注解的Song
类(假设Song
是对应t_songs
表的Entity)。 - 同时移除与
Song
类相关的DAO接口。
2.2.2 删除旧表
kotlin
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//升级操作
// 删除t_songs表
database.execSQL("DROP TABLE IF EXISTS t_songs")
}
}
t_songs表数据有几百万条时,执行DROP TABLE IF EXISTS t_songs
会很耗时,经测试400w条时就会达到十几秒。在升级时会阻塞数据库的操作进而影响业务的处理,很可能导致UI界面加载不出数据一直转圈圈。所以我们可以采用在数据库升级时即migrate()不删除废弃的表,而是在业务中:
- 开启一个线程,每1000条的删除数据;
- 当数据删除完后,删除t_songs表
代码如下:
kotlin
class MainViewModel{
@Inject
lateinit var appDataBase: Lazy<AppDataBase>,
fun deleteTSong(){
viewModelScope.launch(Dispatchers.IO) {
val count = getTSongsCount()
if(count >= 0){
val page = count / 1000 + 1//大概率不会整除,直接+1
val writableDatabase: SupportSQLiteDatabase = appDataBase.get().openHelper.writableDatabase
repeat(page) {
//这里的分页删除好傻,耗时毫秒级
writableDatabase.execSQL("DELETE FROM t_songs WHERE song_id IN (SELECT song_id FROM t_songs LIMIT 1000)")
//重点 这里延迟120ms,是为了这里删除不要独占数据库操作,,如果不理解 想想cpu时间片
delay(120)
}
//没数据了就删除表
appDataBase.get().openHelper.writableDatabase.execSQL("DROP TABLE IF EXISTS t_songs")
}
}
}
/**
* 获取t_songs表大小
*/
private suspend fun getTSongsCount(): Int {
return withContext(Dispatchers.IO) {
var count = -1
runCatching{
appDataBase.get().query("SELECT count(*) FROM t_songs_temp", null).use {
if (it.moveToFirst()) {
count = it.getInt(0)
}
}
}
count
}
}
}
2.3 修改字段名或者类型或者增加主键
针对这种不能在旧表上修改的需求,我们只能新建一个新表然后把旧表中的数据复制到新表中------销毁重新。
例如,我们的t_songs
增加了一个曲库比如tme, 这时song_id就可能在两个曲库中重复了,所以要增加sourceId表示曲库id,song_id + source_id 一起作为主键。
2.3.1 修改Entity
kotlin
@Entity(tableName = "t_song", primaryKeys = ["song_id","song_type","source_id"])
data class Song(
@ColumnInfo(name = "song_id")
val songId: String,//歌曲id
@ColumnInfo(name = "name")
val songName: String,//歌曲名称
@ColumnInfo(name = "type")
val songType: Int,//歌曲类型:1:歌曲 2:听书
//增加source_id
@ColumnInfo(name = "source_id")
val sourceId: Int,//曲库id
)
2.3.2 旧版本升级兼容------销毁重建
有两种方案:
方案一、
- 创建一个新表:创建一个新表,其结构与原表相同,除了需要修改的字段名。
- 复制数据:将原表中的数据复制到新表中,同时将需要修改的字段名的数据赋值到新的列名。
- 删除原表:删除原始的表。
- 重命名新表:将新表重命名为原始表的名字。
方案二、
- 重命名旧表:将原始表重命名为t_songs_old
- 创建一个新表:创建一个新表,其结构与原表相同,除了需要修改的字段名。
- 复制数据:将原表中的数据复制到新表中,同时将需要修改的字段名的数据赋值到新的列名。
- 删除原表:删除原始的表。
我们可以看到方案二如果不执行第4步,我们可以通过App Inspection查看迁移前后的表方便我们调试,还有就是第4步 如果旧表中数据量很大时可以把步骤4放在业务中进行慢慢删除,综上 推荐方案二。
kotlin
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
//1. 重命名旧表
database.execSQL("ALTER TABLE t_songs RENAME TO t_songs_temp")
//2. 如果有索引,则删除旧表的索引
database.execSQL("DROP INDEX index_t_songs_album_id")
//3. 创建新表,这里的语句建议查看自动生成AppData_Imp类里的代码,复制过来,防止自己写错了
database.execSQL("CREATE TABLE IF NOT EXISTS `t_song` (`song_id` TEXT NOT NULL, `song_id` TEXT NOT NULL, `name` TEXT NOT NULL, `source_id` INTEGER NOT NULL, PRIMARY KEY(`song_id, `source_id`))")
//4. 如果旧表有索引,则再建一个索引
database.execSQL("CREATE INDEX IF NOT EXISTS `index_t_songs_album_id` ON `t_songs` (`album_id`)")
//5.复制数据
database.execSQL("INSERT OR REPLACE INTO t_songs (song_id,song_name,song_type,source_id) SELECT song_id,song_name,song_type,0 AS source_id FROM t_songs_old")
//6.删除原表
database.execSQL("DROP TABLE IF EXISTS t_songs_old")
}
}
如果第6点很耗时建议参考2.2节,如果数据量很大第5步也会很耗时,建议只迁移表中有用的数据(有时候业务中只会把数据缓存到表中不删除,这是表中有用数据很少,建议联表进行查询出有用数据进行迁移)
注意:
- 在升级时会阻塞sql查询,如果迁移数据很多(比如百万级)时则会影响用户体验,甚至导致业务异常,同时也会增加用户杀掉进程而使升级失败的概率------虽然下次打开app时也会再次执行数据升级流程;
- 数据量很大时会很耗时内存,升级的代码需要增加异常捕获,当遇到
SQLiteFullException
时,需要发送一个事件,弹出一个内存不足,去清理空间的弹窗; - 如何监听升级是否完成? 上面有说到在升级时会阻塞sql查询,所以我们只需要在进入主页时(确保是app启动后第一个sql语句)随意调一个sql查询,这样当sql方法有返回时,说明升级完成。
- migrate()内的数据库操作是事务的,在migrate()方法中执行的所有数据库操作都是作为一个事务来处理的,所以当该方法中进行大量数据的插入、删除时,会使数据库操作日志剧烈增加,导致升级时的内存、耗时很大