room数据库升级

room数据库升级

一、操作步骤说明

  1. 增加数据库版本号

    @Database注解中增加版本号(version),比如从version 1升级到version 2。

    sql 复制代码
    @Database(
        entities = [
           Song::class,
        ],
        **version = 1**,//1->2
    )
    abstract class AppDataBase : RoomDatabase() {
    }
  2. 定义数据库变化

    根据需要修改的内容(添加表、修改表、删除表等),更新对应的Entity类和DAO接口

  3. 创建Migration对象

    • 创建一个Migration对象,该对象定义了数据库从旧版本升级到新版本时需要执行的操作。
    • 实现migrate()方法,编写SQL语句来处理结构变化或数据迁移
    kotlin 复制代码
    private val MIGRATION_1_2 = object : Migration(1, 2) {
          override fun migrate(database: SupportSQLiteDatabase) {
             //升级操作
             
          }
     }        
  4. 配置数据库实例

    • 在创建Room数据库实例时,通过.addMigrations()方法添加上一步创建的Migration对象。
    • 如果有多个版本的迁移,可以链式调用.addMigrations()添加多个Migration对象。
    kotlin 复制代码
    var appDatabase = Room.databaseBuilder(
                context,
                AppDataBase::class.java,
                DATABASE_NAME,
            ).apply {
                // 把初始下载权限表放这里了
                createFromAsset("init.db")
                addMigrations(
                    MIGRATION_1_2, 
                )
            }
  5. 测试迁移

    • 使用单元测试来确保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()不删除废弃的表,而是在业务中:

  1. 开启一个线程,每1000条的删除数据;
  2. 当数据删除完后,删除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 旧版本升级兼容------销毁重建

有两种方案:

方案一、

  1. 创建一个新表:创建一个新表,其结构与原表相同,除了需要修改的字段名。
  2. 复制数据:将原表中的数据复制到新表中,同时将需要修改的字段名的数据赋值到新的列名。
  3. 删除原表:删除原始的表。
  4. 重命名新表:将新表重命名为原始表的名字。

方案二、

  1. 重命名旧表:将原始表重命名为t_songs_old
  2. 创建一个新表:创建一个新表,其结构与原表相同,除了需要修改的字段名。
  3. 复制数据:将原表中的数据复制到新表中,同时将需要修改的字段名的数据赋值到新的列名。
  4. 删除原表:删除原始的表。

我们可以看到方案二如果不执行第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步也会很耗时,建议只迁移表中有用的数据(有时候业务中只会把数据缓存到表中不删除,这是表中有用数据很少,建议联表进行查询出有用数据进行迁移)

注意:

  1. 在升级时会阻塞sql查询,如果迁移数据很多(比如百万级)时则会影响用户体验,甚至导致业务异常,同时也会增加用户杀掉进程而使升级失败的概率------虽然下次打开app时也会再次执行数据升级流程;
  2. 数据量很大时会很耗时内存,升级的代码需要增加异常捕获,当遇到SQLiteFullException 时,需要发送一个事件,弹出一个内存不足,去清理空间的弹窗;
  3. 如何监听升级是否完成? 上面有说到在升级时会阻塞sql查询,所以我们只需要在进入主页时(确保是app启动后第一个sql语句)随意调一个sql查询,这样当sql方法有返回时,说明升级完成。
  4. migrate()内的数据库操作是事务的,在migrate()方法中执行的所有数据库操作都是作为一个事务来处理的,所以当该方法中进行大量数据的插入、删除时,会使数据库操作日志剧烈增加,导致升级时的内存、耗时很大
相关推荐
OceanBase数据库官方博客3 个月前
出行365:依托分布式数据库,让出行无忧 | OceanBase案例
oceanbase·分布式数据库·数据库升级
OceanBase数据库官方博客5 个月前
OceanBase开发者大会实录-李楠:4条路径+6大方案,关键业务系统数据库的升级之路
oceanbase·分布式数据库·数据库升级·开发者大会
编码熊(Coding-Bear)7 个月前
Android JetPack Compose+Room----实现搜索记录功能
sqlite·android jetpack·room·compose·搜索记录·搜索界面
袁震1 年前
Android--Jetpack--数据库Room详解二
android·android jetpack·mvvm·room·livedata
pgc_tel1 年前
Android Kotlin语言下的文件存储
android·开发语言·kotlin·数据库升级
命运之手1 年前
【Android】Room新手快速入门
android·orm·room