room数据库升级
一、操作步骤说明
-
增加数据库版本号
在
@Database
注解中增加版本号(version),比如从version 1升级到version 2。@Database(entities = [Song::class,],**version = 1**,//1->2 ) abstract class AppDataBase : RoomDatabase() { }
-
定义数据库变化
根据需要修改的内容(添加表、修改表、删除表等),更新对应的Entity类和DAO接口
-
创建Migration对象
- 创建一个Migration对象,该对象定义了数据库从旧版本升级到新版本时需要执行的操作。
- 实现
migrate()
方法,编写SQL语句来处理结构变化或数据迁移
private val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {//升级操作}}
-
配置数据库实例
- 在创建Room数据库实例时,通过
.addMigrations()
方法添加上一步创建的Migration对象。 - 如果有多个版本的迁移,可以链式调用
.addMigrations()
添加多个Migration对象。
var appDatabase = Room.databaseBuilder(context,AppDataBase::class.java,DATABASE_NAME,).apply {// 把初始下载权限表放这里了createFromAsset("init.db")addMigrations(MIGRATION_1_2, )}
- 在创建Room数据库实例时,通过
-
测试迁移
- 使用单元测试来确保Migration正确无误地执行了预期的数据库变化。
- 测试包括但不限于表结构变化、数据迁移的正确性、数据完整性等。
二、常见升级
表定义
@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类修改
@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对象,
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 删除旧表
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表
代码如下:
class MainViewModel{@Injectlateinit var appDataBase: Lazy<AppDataBase>,fun deleteTSong(){viewModelScope.launch(Dispatchers.IO) {val count = getTSongsCount()if(count >= 0){val page = count / 1000 + 1//大概率不会整除,直接+1val writableDatabase: SupportSQLiteDatabase = appDataBase.get().openHelper.writableDatabaserepeat(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 = -1runCatching{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
@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放在业务中进行慢慢删除,综上 推荐方案二。
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步也会很耗时,建议只迁移表中有用的数据(有时候业务中只会把数据缓存到表中不删除,这是表中有用数据很少,建议联表进行查询出有用数据进行迁移)