☰
Android Room数据库迁移实战:从版本升级到表结构变更防坑指南
2026/10/14 16:45:23 网站建设 项目流程

做Android本地存储,Room用顺手之后,最怕听到的就是“改表结构”。我早年在一个笔记类项目上踩过一次大坑:加了字段没写迁移,灰度一放出去,升级用户点开App直接闪退,后台错误日志刷了一屏IllegalStateException。从那以后,凡是涉及Room数据库版本升级和迁移的地方,我都当成发版流程里的最高优先级来看。这篇想把整个版本升级/迁移机制讲透,从Room内部校验逻辑、常见迁移方案选型,到新增表、加列、类型转换、删列这些实战场景,再到迁移测试和防崩溃兜底,一次聊完。适合已经会用Room做基础CRUD、正要面对表结构变更的开发者。

1. 版本升级与迁移的本质:Room到底在背后查了什么

1.1 让Room“发现”数据库变了的第一道关口

Room本质上是对SQLite的一层封装,但它不是每次启动都直接去读表结构,而是维护了一份“预期结构”。这个预期结构来自你写好的实体类、DAO方法和@Database注解。当App打开数据库时,Room会读取数据库文件里实际存在的表结构,跟编译期生成的Schema信息做对比,对比的核心就是identity hash,一个基于表结构、索引、外键等元数据计算出来的哈希值。

如果数据库实际结构和预期一致,啥事没有,正常打开。一旦不一致,而且你没有提供对应的迁移逻辑,Room会直接抛异常。很多刚接触Room的人觉得它“太严格”,其实这正是它靠谱的地方:SQLite本身允许你在表结构错乱时继续读数据,但这种状态下查询结果可能完全不可信。Room选择宁可崩溃也不给你脏数据。

1.2 不写迁移的后果是一记精确打击

拿我的笔记项目举例。第一版note表长这样:

@Entity(tableName = "note") data class Note( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String?, val createdAt: Long )

后来需求要加分类,我在实体里加了category字段,顺手把@Database的version从1改成2,然后就觉得完事了。编译没问题,安装到模拟器上跑也没问题,因为模拟器上的数据库是新装的。结果灰度包推到用户手里,老用户的数据库是版本1,打开App直接崩,日志里清清楚楚写着:

Caused by: 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.

这还只是没改version的情况。我当时是改了version但没写迁移,错误就变成了:

A migration from 1 to 2 was required but not found.

所以记住一件事:版本号不是装饰品,它代表数据库结构的版本;实体类变了、索引变了、外键变了,都必须让版本号往上走,同时提供从旧版本到新版本的迁移路径。

1.3 迁移链与版本号的默契

Room的迁移单位是Migration对象,核心就是两个版本号加一段SQL执行逻辑:

val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE note ADD COLUMN category TEXT NOT NULL DEFAULT 'uncategorized'") } }

这里from是1,to是2,代表把版本1的数据库升级到版本2。Room在检测到当前库版本是1、期望版本是2时,会找到这个Migration并执行。如果当前版本是1、期望版本是3,而你只给了MIGRATION_1_2和MIGRATION_2_3,Room会把这两条串起来依次执行,变成一条迁移链。

我建议项目里所有表结构变更都按“逐级升级”来写,也就是1到2、2到3、3到4这样,而不是偷懒直接写一个1到4。原因很简单:你没法保证所有用户都按顺序升级。有的用户可能一直停在版本2,等某个版本才更新,那么2到3的迁移就必须存在;有的用户从版本1直接跳到版本4,Room会沿着1到2、2到3、3到4一条条执行,只要链路完整,数据就稳稳当当。直接写1到4,版本2、3的用户就全被卡死了。

2. 迁移方案选型:哪些路能走,哪些路是雷

2.1 最省事但最危险的fallbackToDestructiveMigration

如果只是想“别崩”,一行代码就能解决:

Room.databaseBuilder(context, AppDatabase::class.java, "note.db") .fallbackToDestructiveMigration() .build()

这套逻辑是:当Room发现自己处理不了迁移时,把所有表全部drop掉,再按新Schema重新创建。数据自然就没了,跟卸载重装差不多。

符合什么场景用?纯缓存类数据,比如搜索历史、临时筛选条件、可以重新拉取的远端数据。我在一个资讯类App里,热搜缓存表就是用破坏性迁移兜底的,因为那份数据丢了也不心疼,最多是用户下次刷新时重新请求一次。

但凡是用户主动产出的数据,比如笔记、记账记录、本地收藏,用这个就是灾难。有些开发者为了省事图个稳定,直接全局加上fallbackToDestructiveMigration,结果某次表结构变更导致老用户升级后本地数据全部消失,这种事故在应用商店评论区里能挂一个月。

如果真要给破坏性迁移加个保险,可以考虑fallbackToDestructiveMigrationFrom,它允许指定哪些版本可以破坏性重建,其他版本仍然要乖乖走Migration。

Room.databaseBuilder(context, AppDatabase::class.java, "note.db") .fallbackToDestructiveMigrationFrom(1, 2) .build()

意思是从版本1或版本2升级时,允许drop重建。其他版本升级,还是必须有明确Migration才行。这比全局兜底可控得多。

2.2 标准姿势:手写Migration类

这是最常用也是最稳妥的迁移方式。每个Migration对象只负责一段确定的表结构变化,执行的SQL自己控制,数据怎么倒腾自己决定。

val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE note ADD COLUMN category TEXT NOT NULL DEFAULT 'uncategorized'") } } val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ CREATE TABLE IF NOT EXISTS `tag` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `note_id` INTEGER NOT NULL, `name` TEXT NOT NULL, FOREIGN KEY(`note_id`) REFERENCES `note`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) """.trimIndent() ) } }

然后在构建数据库实例时统一注册:

Room.databaseBuilder(context, AppDatabase::class.java, "note.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build()

addMigrations可以传多个,Room会自己挑需要执行的迁移。手写最大的好处是清楚:每一段SQL都看得见摸得着,出问题时也可以直接在SQLite工具里手动复现。

2.3 降级与版本回退怎么处理

多数人只关心升级,但实际运营中,总有一部分用户因为新版本有bug被强制回滚到旧版本。SQLite的数据库结构往下退,想平滑降级几乎不可能,所以Room默认对降级也是抛异常。

可以手动开启降级时的破坏性重建:

Room.databaseBuilder(context, AppDatabase::class.java, "note.db") .fallbackToDestructiveMigrationOnDowngrade() .build()

我的建议是,如果你的产品有灰度回退机制,这行代码最好加上。否则一旦发出去的版本有问题被回退,老版本App打开新版本建出来的数据库就会直接崩,而且会一直崩,用户只能卸载重装。开启降级破坏性重建虽然丢数据,但至少App能打开。

3. 核心实操:常用迁移场景的SQL与代码实现

这一章我用笔记App的完整演进过程串一遍。版本1到版本5,共四次表结构变更,每一段都有对应的实体变化和Migration代码。

3.1 场景一:新增一张表

版本2引入了标签功能,新建tag表,note表不动。实体类:

@Entity( tableName = "tag", foreignKeys = [ ForeignKey( entity = Note::class, parentColumns = ["id"], childColumns = ["noteId"], onDelete = ForeignKey.CASCADE ) ] ) data class Tag( @PrimaryKey(autoGenerate = true) val id: Long = 0, val noteId: Long, val name: String )

迁移SQL:

val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( """ CREATE TABLE IF NOT EXISTS `tag` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `note_id` INTEGER NOT NULL, `name` TEXT NOT NULL, FOREIGN KEY(`note_id`) REFERENCES `note`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE ) """.trimIndent() ) db.execSQL("CREATE INDEX IF NOT EXISTS `index_tag_note_id` ON `tag` (`note_id`)") } }

注意,Room在编译期生成的Schema里会包含索引定义。如果你实体上用了@Index注解,实际建表SQL会带上索引。手写Migration时别漏了,否则运行时Room校验索引对不上,照样给你抛异常。

3.2 场景二:给已有表加一列

版本3要给note加category字段。SQLite的ALTER TABLE ADD COLUMN功能很弱,它只支持往表末尾追加一列,而且这一列不能带PRIMARY KEY、UNIQUE等约束,不能是NOT NULL(除非给了默认值)。

val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE note ADD COLUMN category TEXT NOT NULL DEFAULT 'uncategorized'") } }

这里给了默认值'uncategorized',老数据自动归入“未分类”,不会因为NOT NULL约束报错。实体类对应加上:

@Entity(tableName = "note") data class Note( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String?, val createdAt: Long, val category: String = "uncategorized" )

这段看着简单,但我见过不少人在这里翻车:想加一个带UNIQUE约束的列,直接写ALTER TABLE note ADD COLUMN code TEXT UNIQUE,SQLite直接报错。所以只要涉及复杂约束的加列,老老实实走“建新表、导数据、删旧表、重命名”的四步流程。

3.3 场景三:列类型变更与数据搬运

版本4的需求来了:category字段原来是TEXT,现在要改成INTEGER枚举,比如1代表工作、2代表学习、3代表生活。SQLite不支持直接改列类型,只能重建表。

这是迁移里最考验功力的一步。完整流程分四步:

第一步,建一张临时表note_new,结构按照新期望来:

CREATE TABLE note_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL, category INTEGER NOT NULL DEFAULT 3 )

第二步,把老数据搬过去,同时做类型转换:

INSERT INTO note_new (id, title, content, created_at, category) SELECT id, title, content, created_at, CASE category WHEN 'work' THEN 1 WHEN 'study' THEN 2 ELSE 3 END FROM note

第三步,删除旧表。

第四步,把note_new重命名为note。

Kotlin对应代码:

val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("PRAGMA defer_foreign_keys = ON") db.execSQL( """ CREATE TABLE note_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL, category INTEGER NOT NULL DEFAULT 3 ) """.trimIndent() ) db.execSQL( """ INSERT INTO note_new (id, title, content, created_at, category) SELECT id, title, content, created_at, CASE category WHEN 'work' THEN 1 WHEN 'study' THEN 2 ELSE 3 END FROM note """.trimIndent() ) db.execSQL("DROP TABLE note") db.execSQL("ALTER TABLE note_new RENAME TO note") db.execSQL("CREATE INDEX IF NOT EXISTS index_note_category ON note(category)") } }

这里有个关键细节:我加了PRAGMA defer_foreign_keys = ON。因为DROP TABLE note时,如果note被tag表的外键引用,SQLite在事务里对外键的检查会立刻触发,很可能直接报FOREIGN KEY constraint failed。很多教程让你在迁移开头写PRAGMA foreign_keys = OFF,但Room执行迁移时是包在事务里的,PRAGMA foreign_keys在事务内是no-op,根本不生效。defer_foreign_keys才是事务内可以用的那个开关,它把外键约束检查延迟到整个事务提交时。这样我们就能在事务中间先DROP旧表、再RENAME新表,只要提交时外键关系是完整的就能通过。

还有个替代方案:不要急着DROP被外键引用的旧表,先把引用它的tag表也一起重建,调整外键指向新表。但在复杂场景下,defer_foreign_keys明显更省事。

3.4 场景四:删除一列

版本5决定把content列干掉,因为内容都迁到远端了,本地只留摘要。SQLite在3.35.0之前不支持DROP COLUMN,Android设备上SQLite版本碎片化很严重,所以稳妥起见还是用重建表方案。

val MIGRATION_4_5 = object : Migration(4, 5) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("PRAGMA defer_foreign_keys = ON") db.execSQL( """ CREATE TABLE note_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title TEXT NOT NULL, created_at INTEGER NOT NULL, category INTEGER NOT NULL DEFAULT 3 ) """.trimIndent() ) db.execSQL( """ INSERT INTO note_new (id, title, created_at, category) SELECT id, title, created_at, category FROM note """.trimIndent() ) db.execSQL("DROP TABLE note") db.execSQL("ALTER TABLE note_new RENAME TO note") db.execSQL("CREATE INDEX IF NOT EXISTS index_note_category ON note(category)") } }

这里有几个容易踩的坑:

  • 新表漏了旧表原有的索引,跑完迁移后Room会报索引丢失。所以每次重建表,都把原表上的索引和约束完整写回来。
  • 如果原表有自增主键,新表主键也要带AUTOINCREMENT,并且要保留sqlite_sequence里的计数器信息。不过SQLite在插入显式id时,遇到比当前序列大的id会自动更新序列,所以INSERT时带上id一般没问题。
  • 如果表上有触发器,重建表时触发器并不会跟着迁移,需要手动重建。触发器在Room里用得少,但一旦用了就很容易漏。

4. 迁移测试:把灾难扼杀在发版之前

4.1 先让Room把Schema导出到本地

手写Migration容易犯错,尤其是重建表场景,一个字符写错就前功尽弃。好在Room支持编译期导出Schema JSON文件,迁移测试就是拿这些文件做基准校验的。

先配置导出路径。Kapt项目在build.gradle里:

kapt { arguments { arg("room.schemaLocation", "$projectDir/schemas") arg("room.incremental", "true") } }

KSP项目:

ksp { arg("room.schemaLocation", "$projectDir/schemas") }

配置完重新编译,项目目录下会生成schemas文件夹,里面按版本号放着一堆JSON文件。这个文件描述了对应版本数据库的完整结构,包括表、索引、外键、触发器。务必提交到版本控制里,别加gitignore。

4.2 用MigrationTestHelper写自动测试

迁移测试需要引入测试依赖:

androidTestImplementation("androidx.room:room-testing:2.6.1")

然后写一个测试类。MigationTestHelper是JUnit4的Rule,它会读取编译期生成的Schema文件,按指定版本创建数据库,再执行迁移并自动校验迁移后的结构是否和期望Schema一致。

@RunWith(AndroidJUnit4::class) class MigrationTest { @get:Rule val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java ) @Test fun migrate1To2_shouldAddTagTable() { // 创建版本1的数据库,并插入一条笔记 helper.createDatabase(TEST_DB, 1).apply { execSQL( "INSERT INTO note (id, title, content, created_at) VALUES (1, '测试', '第一条', 123)" ) close() } // 执行1到2的迁移,并自动校验结构 helper.runMigrationsAndValidate( TEST_DB, 2, true, MIGRATION_1_2 ) } companion object { private const val TEST_DB = "migration-test.db" } }

createDatabase的版本号填1,Room会读取schemas/1.json来建表。runMigrationsAndValidate第二个参数是目标版本,第三个参数validateDroppedTables表示是否校验被删除的表确实被清理掉了。

4.3 数据校验:光验证结构还不够

结构校验只能保证表存在、列对得上,不能保证数据在迁移过程中没丢。所以测试里最好在迁移前插入一批代表真实业务的数据,迁移后读出来断言。

比如测试3到4的category类型转换:

@Test fun migrate3To4_shouldConvertCategoryTextToInt() { helper.createDatabase(TEST_DB, 3).apply { execSQL( "INSERT INTO note (id, title, content, created_at, category) VALUES (1, '工作', '内容', 100, 'work')" ) execSQL( "INSERT INTO note (id, title, content, created_at, category) VALUES (2, '生活', '内容', 200, 'life')" ) close() } helper.runMigrationsAndValidate(TEST_DB, 4, true, MIGRATION_3_4) helper.openDatabase(TEST_DB).apply { query("SELECT id, category FROM note ORDER BY id").use { cursor -> assertTrue(cursor.moveToFirst()) assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow("category"))) cursor.moveToNext() assertEquals(3, cursor.getInt(cursor.getColumnIndexOrThrow("category"))) } close() } }

推荐至少覆盖三类用例:

  • 空表迁移:验证SQL在没有任何数据时也能执行成功。
  • 少量数据迁移:验证数据能正确搬运。
  • 大数据量迁移:不能天天在测试里跑几万条,但至少要单独跑一次,确认SQL性能和事务没有问题。

4.4 多版本升迁矩阵:最容易被忽略的坑

一个真实项目发过五个版本,每个版本结构都不一样,那么用户可能从1、2、3、4这些任意旧版本升级到最新版本5。这要求你测试时把每一条可能的路径都覆盖到。

最少要覆盖这些组合:

  • 1 -> 5:走完整链条1_2、2_3、3_4、4_5
  • 2 -> 5:走链条2_3、3_4、4_5
  • 3 -> 5:走3_4、4_5
  • 4 -> 5:走4_5
  • 同级不动:5 -> 5,确认没有多余的迁移被触发

我在项目里是直接写一个参数化测试,把起始版本和迁移链作为参数循环跑。另外建议在多个API级别的模拟器上跑。Android不同系统版本内置的SQLite版本不一样,SQL语法兼容性也差很多,尤其是我前面提到的DROP COLUMN、RENAME COLUMN这类特性,低版本设备上可能就是另一套行为。

5. 新一代自动化迁移:AutoMigration能用但别乱用

5.1 @AutoMigration的适用范围

Room 2.4.0后提供了一套自动迁移机制。对于“加表、加列、建索引”这类简单的、不需要数据搬运的变更,可以不用手写Migration对象:

@Database( entities = [Note::class, Tag::class], version = 3, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3) ], exportSchema = true ) abstract class AppDatabase : RoomDatabase() { companion object { const val DB_NAME = "note.db" } }

只要schemas文件夹里保留了版本1和版本2的JSON,Room就能自动算出差异,生成对应的迁移SQL。实测下来,加表、加列这种场景确实省事,出错概率也低。

5.2 何时该换回手写Migration

AutoMigration有它的短板:它只擅长“无损”的纯结构变更,一旦涉及数据转换、列重命名、删除列这类语义操作,它就不够聪明了。

比如版本4的category类型从TEXT转INTEGER,这种CASE WHEN转换逻辑它做不了。再比如把content列改名为body,Room提供了@RenameColumn注解来声明,但底层还是要执行ALTER TABLE RENAME COLUMN,Android低版本SQLite不支持时就会出问题。

@RenameColumn( entityName = "note", fromColumnName = "content", toColumnName = "body" ) class AppAutoMigrationSpec : AutoMigrationSpec

我的判断标准很简单:如果这个变更不需要关心老数据怎么处理,用AutoMigration;如果老数据需要搬、需要转、需要去重、需要和远端同步,一律手写Migration。混用也是允许的,同一套数据库里AutoMigration和手写Migration可以同时存在,但千万别把同一条升级路径既配AutoMigration又配手写Migration,那样Room会不知道听谁的。

5.3 Schema文件与自动迁移的协作

AutoMigration依赖编译期导出的Schema JSON,所以使用它的前提同样是配置好room.schemaLocation。在持续集成环境里,如果发现schema JSON没提交、自动迁移找不到基准,编译直接失败,这是好事,总比线上崩了强。

还有一点:Schema JSON尽量别手动改。有时候为了绕过某个编译问题,会有人去手改JSON,这种做法后患无穷,因为Room回读JSON做结构对比时,任何不一致都会变成运行时崩溃。

6. 常见问题排查与避坑实录

6.1 Room无法验证数据完整性

错误信息通常是Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

排查步骤:

  • 确认@Database注解里的version确实比之前大。
  • 确认实体类、DAO里涉及的字段、表名、索引、外键和编译时的Schema一致。
  • 重新编译一次,观察schemas文件夹里是否生成了新版本JSON。
  • 如果还是报错,用DB Browser打开本地数据库,对比PRAGMA user_version,看它是否停留在旧版本。

这个错最常见的原因是开发过程中改了实体类,但忘了改version,或者version改了但没写迁移。

6.2 缺少从旧版本到新版本的迁移路径

错误信息类似A migration from 2 to 3 was required but not found.

本质是当前用户库里的版本号与目标版本之间,找不到可执行的Migration。解决办法是把中间链路补齐。我之前就干过这事:版本1跳到3只写了MIGRATION_1_3,结果版本2的用户升级时直接崩。后来老老实实补了MIGRATION_1_2和MIGRATION_2_3,并在addMigrations里全部注册。

6.3 迁移后数据丢了一半

如果迁移执行成功但数据不对,先别着急改代码,把用户手机上的数据库文件拉出来,用SQLite工具手动跑一遍你的迁移SQL,看数据在哪一步开始变少。

常见的丢数据原因:

  • INSERT SELECT时没把全部字段带上,尤其新表增加了非空字段,SELECT出来的行列数对不上。
  • 重建表时把主键或自增属性搞丢,导致插入行为异常。
  • WHERE条件写错,比如多表关联时写成了内连接,把没有匹配数据的老记录过滤掉了。
  • 迁移里用了事务之外的逻辑,部分表被删除但新表没建成功。

6.4 外键约束导致的迁移失败

如果你在建表时启用了外键,迁移过程中又涉及删除表或改表名,就很容易碰到外键约束错误。解决办法我在前面已经演示过:迁移SQL开头执行PRAGMA defer_foreign_keys = ON,把外键检查延迟到事务提交时。这点在官方文档里写得很隐晦,属于实操中才踩得到的坑。

6.5 升级后首次打开非常卡,甚至ANR

如果迁移涉及大表重建,耗时可能好几秒。Room的迁移发生在第一次访问数据库时,谁触发了数据库创建谁就背着这个耗时。我在一个记账App里碰到过,老用户升级后进入首页,主线程正好要查数据库,迁移直接卡了三四秒,最后触发ANR。

缓解手段有几个:

  • 在启动流程里提前触发一次数据库访问,把它放到后台线程,让迁移在用户看到首页之前完成。
  • 启动页做一个过渡状态,不让用户过早进入需要查库的界面。
  • 复杂迁移写成多条小SQL,分步执行,减少单条语句锁表时间。

代码层面,可以在Application里开一个后台线程提前打开数据库:

Thread { database.openHelper.writableDatabase }.start()

这只适合做预迁移,别在这里做业务查询。另外要控制Roow实例,如果应用里到处Room.databaseBuilder().build(),每个实例都会触发一次迁移,既浪费时间又容易出并发问题。

6.6 版本号只增不减,降级要有兜底

版本号必须越来越大。如果发出去的版本有bug想回退到旧版,旧版App打开新版数据库会崩。所以我在前面建议加fallbackToDestructiveMigrationOnDowngrade(),至少保证降级后App能打开,最多就是用户本地数据重置。如果你连重置都不能接受,那就要做远端数据同步和本地备份,让降级后能重新拉取数据。

写在最后的经验总结

Room的版本升级和迁移,写SQL只是表象,真正考验的是对用户数据的责任心。我踩过几次坑之后形成了自己的工作清单,每一步都不省:

  • 任何实体类变动,先确认是否需要升版本号。
  • 先配置好schemaLocation,每次编译后检查Schema JSON有没有正常生成,并且提交到git。
  • 简单变更优先手写Migration,别偷懒用破坏性迁移兜底。
  • 复杂迁移一律按“建新表、导数据、删旧表、改名、重建索引”的流程,并在开头加defer_foreign_keys。
  • 每次迁移都写Instrumentation测试,至少跑一条空数据、一条有数据、一条多版本跳级。
  • 发布前在多个API级别模拟器上跑一遍迁移测试,确认SQLite版本差异不会坑你。
  • 大表迁移必须在后台触发,别让用户感受到卡顿。

最后分享一个小技巧:迁移完成后,随手打印一条日志,记录当前库的user_version和每个表的数据行数。这样线上出问题后,至少能根据日志快速判断用户是从哪个版本升上来、迁移过程中卡在哪一步。我就是在一次线上事故里靠这个日志定位到是索引缺失导致的崩溃,十分钟就锁定了问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询