Expo Updates 数据库迁移实战指南:为 expo-updates 本地 SQLite 添加安全可靠的 schema 迁移

Expo Updates 数据库迁移实战指南:为 expo-updates 本地 SQLite 添加安全可靠的 schema 迁移 Expo Updates 数据库迁移实战指南为 expo-updates 本地 SQLite 添加安全可靠的 schema 迁移【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expoexpo-updates 会在用户设备上维护一个本地 SQLite 数据库用于记录已下载的更新清单manifest、元数据、状态以及组成更新的各个 asset资源是 OTA 更新机制正常运转的地基。本文以 packages/expo-updates/guides/migrations.md 为主干结合仓库中 Android 与 iOS 两端的最新源码实现完整讲解为 expo-updates 数据库添加迁移的标准流程从 Android 端基于 Room 的手动迁移十一部曲到 iOS 端基于 sqlite3 自研迁移系统的八步法再到两端各自的 schema 校验与数据保全测试。读完本文你将掌握在 Android 与 iOS 上为 expo-updates 数据库安全演进 schema 的完整方法论并理解其背后的失败回退与事务保护机制。为什么数据库迁移必须慎之又慎expo-updates 的数据库虽然只存在于每台设备的本地但它承载的是 OTA 更新体系的关键状态哪些更新已下载、哪些已启动成功、哪些 asset 正在使用、哪些可以回收Reaper 清理逻辑依赖它。schema 一旦需要演进例如新增url、headers列或把manifest列改为非空就必须通过迁移来升级已经存在于成千上万台用户设备上的旧库。正如原文档所强调的这些迁移发生在用户设备上一旦出错我们对这些设备没有任何集中控制权。服务器端可以灰度、可以回滚但设备本地数据库升级失败可能导致更新功能不可用、数据丢失甚至崩溃而且开发者无法事后远程修复。因此迁移代码的每一个细节都值得用最高标准对待并在合并前经过充分的测试验证。从当前仓库源码看这份谨慎已经沉淀为制度Android 端使用 Google 的 Room 库管理数据库schema 由 Entity 类自动生成版本号显式声明在Database注解上当前为13见 UpdatesDatabase.ktiOS 端直接使用 sqlite3 C 库用数据库文件名承载版本号当前最新文件名为expo-v11.db见 UpdatesDatabaseInitialization.swift并自研了一套与 Room 功能对齐的迁移系统。常见陷阱三个最容易翻车的地方原文档用专门一节列举了迁移工作中最典型的三个失误结合源码可以看得更清楚定义了迁移却忘记把它注册进数据库构建器。Android 端必须在UpdatesDatabase.getInstance()的addMigrations(...)调用中显式列出迁移见 UpdatesDatabase.ktiOS 端必须把新迁移类加入 UpdatesDatabaseMigrationRegistry.swift 的migrations()数组。漏掉这一步的后果非常严重Room 会因为没有对应的迁移路径而回退到破坏性迁移destructive migration——直接删掉旧表重建所有用户数据付之东流。文档特别提到以前就发生过这种事。iOS 端没有自动化的 schema 校验必须人工反复确认迁移产出的库 schema 与新建库 schema 完全一致。这一点后面会详细展开。Android 端临时开启 schema 导出后切勿把临时改动提交进仓库对应第 7 步。背景知识Android 用 RoomiOS 用裸 sqlite3在动手之前先理解两端的技术底座差异这是整个迁移流程设计的前提。AndroidRoom 自动生成 schemaAndroid 端通过 Room 操作数据库entity 类UpdateEntity、AssetEntity、UpdateAssetEntity、JSONDataEntity被编译期注解处理器转化为建表 SQLDAO 提供类型安全的读写接口。数据库类上标注了当前版本Database( entities [UpdateEntity::class, UpdateAssetEntity::class, AssetEntity::class, JSONDataEntity::class], exportSchema false, version 13 ) TypeConverters(Converters::class) abstract class UpdatesDatabase : RoomDatabase() { ... }关键点在于我们无法在语言层面直接控制 SQLite 的最终 schema它完全由 Room 依据 Entity 类推导。所以迁移的本质是修改 Entity 类让新库长成新样子再手写Migration对象让旧库也变成新样子两者必须严格一致。当前仓库里已经积累了 9 个手动迁移4→5、5→6、6→7、7→8、8→9、9→10、10→11、11→12、12→13且fallbackToDestructiveMigration()被显式启用作为最后兜底见 UpdatesDatabase.kt。iOS文件名即版本号iOS 端没有用任何 ORM 包装直接调用 sqlite3 API。为了模拟 Room 的版本概念设计了一个巧妙的约定数据库文件名中的数字就是 schema 版本号。当前最新文件名expo-v11.db代表 v11 schema历史文件则依次为expo-v4.db、expo-v5.db……直到expo-v10.db。初始化逻辑在 UpdatesDatabaseInitialization.swift 中如果最新文件名对应的库已存在直接使用跳过迁移否则遍历迁移注册表找到现存的最旧版本文件migration.filename匹配把它改名为最新文件名打开该库按顺序执行从该版本起到最新版的每一个迁移任何一步失败则返回失败由上层决定回退策略。这段逻辑还包含了对损坏数据库的归档处理打开失败且错误码为SQLITE_CORRUPT/SQLITE_NOTADB时会把损坏文件改名归档保留现场供排查再新建一个干净库见 UpdatesDatabaseInitialization.swift。从当前源码看两端迁移版本并不同步Android 已推进到 v13而 iOS 注册表只登记到 10→11见 UpdatesDatabaseMigrationRegistry.swift。这与原文档中可以复用 Android 迁移的 SQL 语句但两端 schema 略有差异要小心的提示相互印证。Android 端迁移流程Room 手动迁移十一部曲原文档给出了 Android 端添加手动迁移的 11 个步骤下面逐条展开并结合仓库源码给出实现细节。关于 Room 自动生成迁移的重要说明仓库当前使用的 Room 版本已经支持自动生成迁移autogenerated migrations。你可以按官方指南尝试这种方式但需要注意仓库中所有已存在的迁移都是在自动迁移功能出现之前手写的因此你的新迁移在写法、流程上会和现有手动迁移以及下述步骤有所差异无论采用哪种方式第 7~11 步schema 导出验证 数据保全测试都仍然必须执行这是不可跳过的质量底线。第一步修改 Entity 类在packages/expo-updates/android/src/main/java/expo/modules/updates/db/entity/下找到需要变更的 Entity 类UpdateEntity、AssetEntity等做出想要的 schema 变更。例如当前最新迁移 12→13 的目标就是在updates表上新增url和headers两列。第二步提升 Database 版本号把UpdatesDatabase.kt中Database注解的version参数加一。这一步之后Room 会检测到应用代码声明的版本 设备上库的版本但找不到对应迁移路径默认会执行破坏性迁移——这正是我们接下来要用手动迁移去阻止的。第三步先提交这批改动先提交 Entity 变更 版本号提升让新 schema有一个明确的提交记录便于后续 schema 导出时对照 diff。第四步编写新的 Migration 对象在UpdatesDatabase.kt的companion object中按既有惯例添加一个Migration(from, to)对象。仓库中已经沉淀了两种典型写法可以直接参考纯加列类迁移如 8→9、9→10、12→13直接ALTER TABLE ADD COLUMN即可val MIGRATION_8_9: Migration object : Migration(8, 9) { override fun migrate(db: SupportSQLiteDatabase) { db.runInTransactionWithForeignKeysOff { execSQL(ALTER TABLE assets ADD COLUMN extra_request_headers TEXT) } } }需要重建表的结构性迁移改列可空性、加 NOT NULL 列、重命名列等SQLite 的ALTER TABLE能力有限标准做法是建新表 → 拷贝数据 → 删旧表 → 改名十二步法。以 11→12 为例把updates.manifest改为 NOT NULLval MIGRATION_11_12: Migration object : Migration(11, 12) { override fun migrate(db: SupportSQLiteDatabase) { db.runInTransactionWithForeignKeysOff { execSQL(CREATE TABLE new_updates (id BLOB NOT NULL, scope_key TEXT NOT NULL, commit_time INTEGER NOT NULL, runtime_version TEXT NOT NULL, launch_asset_id INTEGER, manifest TEXT NOT NULL, status INTEGER NOT NULL, keep INTEGER NOT NULL, last_accessed INTEGER NOT NULL, successful_launch_count INTEGER NOT NULL DEFAULT 0, failed_launch_count INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(id), FOREIGN KEY(launch_asset_id) REFERENCES assets(id) ON UPDATE NO ACTION ON DELETE CASCADE )) execSQL( INSERT INTO new_updates (...) SELECT ... FROM updates WHERE manifest IS NOT NULL ) execSQL(DROP TABLE updates) execSQL(ALTER TABLE new_updates RENAME TO updates) execSQL(CREATE INDEX index_updates_launch_asset_id ON updates (launch_asset_id)) execSQL(CREATE UNIQUE INDEX index_updates_scope_key_commit_time ON updates (scope_key, commit_time)) } } }注意这里数据拷贝阶段可以趁机做数据变换比如 5→6 迁移把旧metadata列改名为manifest并顺手填入last_accessed时间戳7→8 迁移给所有存量 update 补successful_launch_count 1避免回滚机制误伤历史更新。这些细节都值得新迁移学习完整实现见 UpdatesDatabase.kt。仓库为这类重建表场景封装了两个事务辅助方法runInTransaction标准事务包裹runInTransactionWithForeignKeysOff在事务前后临时关闭/恢复外键约束——因为按 SQLite 的 ALTER TABLE 文档。如果迁移涉及列操作建议先通读 SQLite 的ALTER TABLE语义文档尤其是如何修改列的默认值/可空性/重命名等限制。第五步把迁移加入数据库构建器在getInstance()中把新迁移追加到addMigrations(...)的参数列表里.addMigrations( MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_12_13 // 新增的迁移在这里 ) .fallbackToDestructiveMigration()这是文档反复强调的最容易遗漏的关键一步漏注册的迁移将永远不被执行Room 最终会以破坏性迁移收场。第六步提交迁移代码此时迁移代码 注册形成第二个提交点便于审查者聚焦于迁移本身的正确性。第七步临时开启 schema 导出构建后必须撤销这是一个临时改动用于让 Room 生成新库的 JSON schema 快照把UpdatesDatabase.kt中Database的exportSchema改为true在 android/build.gradle 中确认androidTest.assets.srcDirs已包含src/androidTest/schemas目录并取消注释room.schemaLocation参数当前仓库中该参数处于注释状态这正是本步骤要求临时开启的内容。然后构建一个依赖 expo-updates 的项目例如 Android 版 Expo Go——构建会触发 Room 注解处理器把当前 Entity 推导出的完整 schema导出为 JSON 文件。第八步检查自动生成的 JSON schema构建成功后应能在packages/expo-updates/android/src/androidTest/schemas/expo.modules.updates.db.UpdatesDatabase/下看到新生成的 JSON schema 文件。这个目录下的任何内容都不允许手动修改它是后续所有迁移测试的标准答案。如果此步报错通常意味着你的迁移并不完全正确——即迁移产出的库 schema 与新建库 schema 不一致。回到第四步检查。第九步撤销临时改动提交 JSON schema把exportSchema改回false恢复 build.gradle 中被注释的行然后提交生成的 JSON schema 文件。这一步务必确认没有把第七步的临时改动混进提交。第十步编写迁移测试在 UpdatesDatabaseMigrationTest.kt 中为你的迁移新增一个测试方法。测试机制如下用MigrationTestHelper.createDatabase(TEST_DB, fromVersion)创建一个旧版本的库用原生 SQL 语句绝不能使用 Room DAO因为 DAO 永远绑定最新 schema向旧库插入代表性数据调用helper.runMigrationsAndValidate(TEST_DB, toVersion, true, UpdatesDatabase.MIGRATION_X_Y)执行迁移——Room 会自动校验迁移后 schema 与 JSON schema 快照一致手动执行db.execSQL(PRAGMA foreign_keysON)开启外键Room 测试默认不开启必须自己打开才能验证外键约束仍然有效最后用 SQL 查询断言数据被正确保留/变换并额外验证外键约束、级联删除等行为未被破坏。仓库中现成的测试模式见 UpdatesDatabaseMigrationTest.kt非常值得照抄testMigrate4To5完整演示了插数据 → 迁移 → 逐条断言 → 验证外键约束插入非法引用应抛SQLiteConstraintException→ 验证级联删除的完整链路。另外注意createDatabase之后必须先db.close()再跑迁移否则无法打开。第十一步跑通测试并提交确保新测试通过./gradlew :expo-updates:connectedAndroidTest之类的方式运行 androidTest连同迁移代码、注册、JSON schema 一起提交。大功告成。文档附注PR #14499 曾作为一份完整的 Android 迁移合入参考可按此粒度组织你的提交历史Entity 变更 → 迁移代码 → schema 快照 → 测试。iOS 端迁移流程裸 sqlite3 自研迁移八步法iOS 端没有 Room 的 schema 导出与自动化校验一切靠纪律与人工复核。由于我们直接与 sqlite3 打交道也没有 Entity 层schema 长什么样完全由UpdatesDatabaseInitialization.swift里的LatestSchema字符串常量决定当前最新为 v11 schema见 UpdatesDatabaseInitialization.swift。第一步把旧 schema 快照复制进测试文件从 UpdatesDatabaseInitialization.swift 中把当前生效的LatestSchema复制为一个新的静态常量放进测试文件。仓库的 DatabaseInitializationTests.swift 中已经有UpdatesDatabaseV4Schema、UpdatesDatabaseV5Schema、UpdatesDatabaseV6Schema等一串历史 schema 常量照此模式追加即可——这个常量将作为迁移起点用于测试。第二步修改正式 schema 并提升文件名版本号在UpdatesDatabaseInitialization.swift中对LatestSchema做出你的 schema 变更并把LatestFilename中的版本号 1例如从expo-v11.db改为expo-v12.db。如果新增了 NOT NULL 列很可能还需要同步修改 UpdatesDatabase.swift 中的某些 SQL 语句例如向updates表插入数据时必须带上新列。单元测试会替你抓住这类遗漏。第三步实现新的迁移类在packages/expo-updates/ios/EXUpdates/Database/Migrations/下创建实现UpdatesDatabaseMigration协议的新类。协议定义见 UpdatesDatabaseMigration.swiftinternal protocol UpdatesDatabaseMigration { var filename: String { get } // 旧数据库文件名如 expo-v10.db func runMigration(onDatabase db: OpaquePointer) throws // 迁移逻辑 }实现时注意三点filename必须返回旧库的文件名——它是迁移系统的版本定位器。初始化逻辑正是靠它判断设备上现存哪个版本的库并从那里开始顺序执行迁移runMigration中遇到不可恢复的错误时抛错或按旧式写法返回失败上层会据此回退到破坏性迁移——这比半迁移状态或损坏的库好得多Android 迁移的 SQL 语句大部分可以直接复用但两端 schema 有细微差异例如列的约束、索引定义不完全相同务必对照两端最新 schema 逐一核对。协议还配套了两个事务/外键辅助工具可以在迁移中直接使用见 UpdatesDatabaseMigration.swiftdb.withTransaction { trx in ... }自动BEGIN/COMMIT失败时抛错trx.safeExecOrRollback(sql:)SQL 执行失败时自动ROLLBACKdb.withForeignKeysOff { ... }重建表前临时关闭外键。参考当前最新的 10→11 迁移新增url、headers列internal final class UpdatesDatabaseMigration10To11: UpdatesDatabaseMigration { private(set) var filename: String expo-v10.db func runMigration(onDatabase db: OpaquePointer) throws { try db.withTransaction { trx in try trx.safeExecOrRollback(sql: ALTER TABLE updates ADD COLUMN url TEXT; ALTER TABLE updates ADD COLUMN headers TEXT; ) } } }完整实现见 UpdatesDatabaseMigration10To11.swift。而涉及重建表 关闭外键的写法可以参考 UpdatesDatabaseMigration4To5.swiftwithForeignKeysOff 建新表/拷贝/删旧/改名。第四步把新迁移注册进注册表在 UpdatesDatabaseMigrationRegistry.swift 的migrations()数组中按从旧到新的顺序追加return [ UpdatesDatabaseMigration4To5(), UpdatesDatabaseMigration5To6(), UpdatesDatabaseMigration6To7(), UpdatesDatabaseMigration7To8(), UpdatesDatabaseMigration8To9(), UpdatesDatabaseMigration9To10(), UpdatesDatabaseMigration10To11(), // 新增UpdatesDatabaseMigration11To12() ]注册表顺序即执行顺序这一点与 Android 的addMigrations完全同构。漏注册的后果同样是被跳过迁移、最终走向破坏性重建。第五步人工双检 schema 一致性这是整个 iOS 流程中最关键的一步与 Android 不同iOS 目前没有任何自动化手段校验迁移后的 schema 新建库 schema所以必须反复人工确认——把迁移类里每个CREATE TABLE/ALTER TABLE的最终形态与LatestSchema逐列、逐约束、逐索引地比对。文档甚至用(Really, just do it.)来强调其重要性。第六步新增初始化测试在 DatabaseInitializationTests.swift 中新增测试验证数据被正确保留/迁移。仓库中现成的测试模式如第 933、1000 行附近分别用UpdatesDatabaseMigration9To10()、UpdatesDatabaseMigration10To11()驱动初始化值得参考用第一步复制的旧 schema 常量建一个旧版本库插入代表性数据以旧文件名初始化传入新迁移initializeDatabase(withSchema:filename:inDirectory:shouldMigrate:migrations:logger:)允许你精确控制起点与迁移列表断言迁移后数据完好、schema 与LatestSchema一致。与 Android 测试不同这里不需要手动开启外键——UpdatesDatabaseInitialization初始化时已经通过PRAGMA foreign_keysON自动处理了见 UpdatesDatabaseInitialization.swift。第七步再确认一次 schema 一致再做一遍第五步的人工比对。重点检查新库 schema 与迁移后 schema 是否逐字节一致包括索引、外键、默认值。第八步全部测试通过后提交确保DatabaseInitializationTests与UpdatesDatabaseTests全部通过然后提交。完成失败回退机制宁可重建不要半迁移两端都设计了迁移失败则回退到破坏性重建的兜底策略这是刻意的产品决策——一个干净的新库永远好过一个处于未知中间状态的库AndroidfallbackToDestructiveMigration()确保 Room 在找不到迁移路径或迁移抛异常时删除旧数据按新 schema 重建见 UpdatesDatabase.ktiOSmigrateDatabase返回false时初始化逻辑会删除旧库文件并按LatestSchema从零重建若打开时发现库损坏SQLITE_CORRUPT/SQLITE_NOTADB还会先把损坏文件改名归档再重建为事后排查保留现场见 UpdatesDatabaseInitialization.swift。这意味着如果你的迁移写错了最坏情况不是崩溃而是所有用户被迫重新下载更新——但这依然是一次数据灾难所以测试环节怎么强调都不过分。两端对比速查维度AndroidRoomiOS裸 sqlite3schema 定义方式Entity 类自动生成LatestSchema字符串常量版本号载体Database(version N)数据库文件名expo-vN.db迁移接口Migration(from, to)migrate(db)UpdatesDatabaseMigration协议filenamerunMigration注册位置getInstance()的addMigrations(...)UpdatesDatabaseMigrationRegistry.migrations()schema 自动校验有MigrationTestHelper JSON schema 快照无必须人工逐列比对数据保全测试UpdatesDatabaseMigrationTest.ktDatabaseInitializationTests.swift外键处理测试中需手动PRAGMA foreign_keysON初始化类自动开启失败兜底fallbackToDestructiveMigration()删除/归档旧库后按最新 schema 重建核心文件索引迁移指南原文packages/expo-updates/guides/migrations.mdAndroid 数据库与全部迁移实现packages/expo-updates/android/src/main/java/expo/modules/updates/db/UpdatesDatabase.ktAndroid 迁移测试packages/expo-updates/android/src/androidTest/java/expo/modules/updates/db/UpdatesDatabaseMigrationTest.ktAndroid schema 导出配置packages/expo-updates/android/build.gradleiOS 数据库初始化与迁移调度packages/expo-updates/ios/EXUpdates/Database/UpdatesDatabaseInitialization.swiftiOS 迁移协议与事务工具packages/expo-updates/ios/EXUpdates/Database/Migrations/UpdatesDatabaseMigration.swiftiOS 迁移注册表packages/expo-updates/ios/EXUpdates/Database/Migrations/UpdatesDatabaseMigrationRegistry.swiftiOS 迁移示例UpdatesDatabaseMigration10To11.swift、UpdatesDatabaseMigration4To5.swiftiOS 初始化与迁移测试packages/expo-updates/ios/Tests/DatabaseInitializationTests.swift、packages/expo-updates/ios/Tests/UpdatesDatabaseTests.swift掌握这套迁移流程后当你需要为 expo-updates 的更新数据模型增加新字段、调整约束或重构表结构时就能在 Android 与 iOS 两端都写出经过验证、可安全推送到成千上万台用户设备的迁移代码。【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考