先说结论:Room 3.0 不是简单换个版本号,它是一次伤筋动骨的底层重构。7月1日 androidx.room3:room3-*:3.0.0 正式发布,包名、Maven坐标、核心API全换了一遍,支持KMP后你的数据库代码能跑在iOS和JVM桌面上。但升级过程远比我想象的折腾。
为什么这次升级非同一般
我之前做过不少库的版本升级------Room 2.5到2.6、Lifecycle升级、Compose版本对齐------基本都是改个版本号,修几个deprecated调用就完事了。Room 3.0不一样。Google直接把整个库搬到了新的namespace:androidx.room 变成了 androidx.room3,Maven Group ID从 androidx.room:room-runtime 变成了 androidx.room3:room3-runtime。 这不是随便改着玩的。Google的解释是:很多现有库(比如WorkManager)传递依赖Room 2.x,如果3.0还在原包名上升版本,会立刻引发二进制冲突。新包名相当于创建了一个"平行世界",允许新旧版本在同一个项目里共存------理论上。实际操作中,这个"共存"本身就是个坑,后面细说。
真正让我决定升级的动力是KMP。项目里数据库层的代码量不小,如果Entity和DAO能直接在iOS端复用,客户端团队至少能砍掉30%的重复代码。Room 3.0底层完全脱离了Android的SupportSQLite API,改用新的 androidx.sqlite 驱动接口,这才是KMP能跑起来的关键。
依赖声明改完,满屏红色报错
改完版本号,第一步当然是改依赖声明。我把 build.gradle.kts 里的Room相关依赖全换了:
scss
// 之前
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// 改后
implementation("androidx.room3:room3-runtime:3.0.0")
implementation("androidx.room3:room3-ktx:3.0.0")
ksp("androidx.room3:room3-compiler:3.0.0")
改完一sync,满屏红色报错。所有import语句都是旧的:import androidx.room.Entity、import androidx.room.Dao、import androidx.room.Query......你项目里用了多少Room注解,就有多少import要改。 我用IDE的全局替换搞定了大部分------把 androidx.room. 替换成 androidx.room3.。但这里有个容易漏的点:字符串里的包名引用。比如我在TypeConverter里用Gson反序列化,有个地方写了硬编码的类名引用,全局替换不会扫到字符串。跑单测的时候才发现NPE,排查半天才定位到。 还有个更隐蔽的问题:项目里有个自定义的 Migration,里面用到了 SupportSQLiteDatabase。这个类整个被干掉了,Room 3.0换成了 SQLiteConnection。我的Migration代码直接编译不过。
最痛的坑:SupportSQLite全家被移除
这可能是Room 3.0里迁移成本最高的一个变化。 Room 2.x的底层完全绑定在Android的 SupportSQLite API上------SupportSQLiteDatabase、SupportSQLiteOpenHelper、SupportSQLiteQuery,这些是Room和Android SQLite之间的桥梁。Room 3.0为了KMP,把这些全部替换成了新的 androidx.sqlite 驱动API。 影响面很大。我至少碰到了这几个地方:
数据库构建器必须设置Driver
scss
// Room 2.x
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.build()
// Room 3.0 ------ 必须显式提供SQLiteDriver
val db = Room.databaseBuilder<AppDatabase>(context, "app.db")
.setDriver(BundledSQLiteDriver()) // Android上用这个
.addMigrations(MIGRATION_1_2)
.build()
不加 setDriver() 直接运行时崩溃。报错信息倒是挺明确:No SQLiteDriver provided。但问题是文档里这个变更写得很隐蔽,我翻了好几页release notes才找到。
Migration签名变了
kotlin
// Room 2.x
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER")
}
}
// Room 3.0
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
connection.execSQL("ALTER TABLE user ADD COLUMN age INTEGER")
}
}
migrate() 的参数从 SupportSQLiteDatabase 变成了 SQLiteConnection。如果Migration里只用了 execSQL(),改起来很简单。但我的项目里有个复杂的Migration,用到了 db.beginTransaction() / db.setTransactionSuccessful() / db.endTransaction() 来手动控制事务。SQLiteConnection 上没有这套API了。 查了一下,Room 3.0把事务控制也改了。RoomDatabase.runInTransaction() 变成了 withWriteTransaction()(suspend函数),Migration里的事务则由Room自动管理------你不应该再手动控制。把那几行事务代码删掉,让Room自己管,反而是正确的做法。但这个行为变化在迁移指南里没提,我是对比了Room源码才确认的。 RoomDatabase.Callback也变了 onCreate() 和 onOpen() 的参数也从 SupportSQLiteDatabase 变成了 SQLiteConnection。我在 onOpen() 里做了数据库初始化的预填充逻辑,改完参数类型还要检查 SQLiteConnection 上有没有之前用的方法。大部分都有对应,但 compileStatement() 的返回类型变了,链式调用那里也需要调整。 说实话改到这里的时候我已经有点烦了。SupportSQLite的移除不是一个点,它是一整张网的替换,每个节点都可能踩到。而且编译器报错只会告诉你当前这行有问题,不会告诉你还有多少行等着你。
一个没想到的坑:KAPT被彻底干掉
Room 3.0只支持KSP,不再支持Java注解处理器(APT)和KAPT。 我项目里其实已经在用KSP了,所以本以为这个变化跟我没关系。结果还是踩到了------有个老模块还在用 kapt("androidx.room:room-compiler:2.6.1"),新Room 3.0的compiler根本不注册KAPT处理器。sync没问题,编译时KAPT找不到处理器直接报错。 更坑的是,这个老模块我平时很少动,第一次编译没跑到那个模块就没发现。后来跑全量构建才爆出来。教训:迁移的时候一定要跑全量构建,不要只编译你改过的模块。 把 kapt 换成 ksp 就行了:
scss
// 之前
kapt("androidx.room3:room3-compiler:3.0.0")
// 改后
ksp("androidx.room3:room3-compiler:3.0.0")
简单是简单,但如果你项目里有其他库也依赖KAPT(比如Dagger/Hilt的老版本),KSP和KAPT混用本身也可能出问题。好在Hilt早就支持KSP了,顺便一起迁了。
KMP跨平台:能跑,但有条件
把Android端的Room迁移搞定后,我试了一下KMP共享模块的配置。这是Room 3.0的核心卖点------同一个Entity和DAO在Android、iOS、JVM桌面、Web上都能用。 共享模块的 build.gradle.kts 大概长这样:
scss
kotlin {
androidTarget()
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
implementation("androidx.room3:room3-runtime:3.0.0")
}
}
}
但这里有个前提条件:你需要为每个平台提供对应的 SQLiteDriver。Android用 BundledSQLiteDriver,iOS需要用 NativeSQLiteDriver(来自 androidx.sqlite:sqlite-driver-native),JVM桌面用JDBC驱动的那个。 这意味着你的common代码里不能直接 Room.databaseBuilder,因为Driver是平台相关的。常见做法是在 expect/actual 里提供数据库实例的创建逻辑。 我的项目目前只在Android和JVM桌面端跑,iOS端还没接入。Android端一切正常,JVM桌面端配了JDBC Driver后也能跑。但要注意JDBC Driver需要你手动引入SQLite JDBC依赖,Room不会自动帮你拉。
另外一个容易忽略的点:Room 3.0的KMP支持要求你的Entity和DAO写在commonMain里,不能写在androidMain里。如果你的项目之前把Entity放在Android模块里,迁移到KMP共享模块需要把文件移过去,同时把Android特有的类型引用(比如 Context)去掉。这个工作量取决于你之前代码组织得怎么样------我的Entity比较干净,只需要改import,但如果Entity里有Android类型的字段,就得考虑怎么抽象了。
官方给的缓冲方案
Google也知道这次迁移太猛了,所以提供了一个兼容库 androidx.room3:room3-sqlite-wrapper。通过它可以临时获取 SupportSQLiteDatabase 的包装实例,用来兼容那些暂时没法改的旧代码。
scss
// 引入wrapper依赖
implementation("androidx.room3:room3-sqlite-wrapper:3.0.0")
// 使用
val supportDb = roomDatabase.getSupportWrapper()
supportDb.execSQL("...")
我用了两天,然后把它删了。原因很简单:这个wrapper只是一个过渡方案,未来会被废弃。而且它的行为和真正的 SupportSQLiteDatabase 有微妙差异------比如事务的行为不完全一致,Migration里用wrapper操作数据,偶现死锁。可能是我用法有问题,但既然最终都要迁到新API,不如一步到位。
我的迁移顺序建议
回过头看,如果让我重新来一遍,我会按这个顺序操作: 先把所有import和依赖声明从 androidx.room 换成 androidx.room3,确保编译通过。然后处理Migration------把 SupportSQLiteDatabase 换成 SQLiteConnection,删掉手动事务控制。接着改 Room.databaseBuilder,加上 setDriver(BundledSQLiteDriver())。再改 RoomDatabase.Callback。最后处理KAPT到KSP的切换。 每改一步就跑一次全量编译,别攒着一起改。SupportSQLite移除影响的地方太散了,不改一个确认一个,后面排查成本会指数级增长。
值不值得升级
如果你的项目只跑Android,而且Room 2.x用得好好的------不急。Room 2.x还会持续维护,3.0的KMP能力对你没有直接收益。强行升级只会增加工作量。 但如果你有跨平台的需求,或者项目正在规划KMP架构,Room 3.0是必须要过的坎。早升级,代码量小的时候改起来更轻松。等到Entity和DAO堆积到上百个的时候再迁,那个痛苦指数会完全不一样。 还有一点:Room 3.0的新包名设计意味着你可以渐进式迁移。项目里可以同时存在Room 2.x和3.0的依赖,老的模块继续用2.x,新的模块用3.0。不用一口气全改,先从独立模块开始试水是更稳的策略。 就写到这吧。Room 3.0的KMP故事才刚开始,后面有新的踩坑经验再聊。