2. Room工作原理深度拆解:SQLite封装、DAO、实体、数据库升级
Room是Android官方提供的持久化库,本质是对底层SQLite的轻量封装。理解Room的核心组件——数据库(Database)、DAO(数据访问对象)、实体(Entity)以及数据库升级机制,是正确使用Room的前提。本章从源码执行链路出发,逐层拆解Room如何将Kotlin/Java注解编译为SQLite操作,并指导你在实际项目中避开常见陷阱。
2.1 Room核心概念与职责划分
| 组件 | 职责 | 编译产物 |
|---|---|---|
| @Entity | 定义表结构:列名、类型、主键、索引 | SQLite CREATE TABLE 语句 |
| @Dao | 封装数据访问方法:增删改查(CRUD) | 实现类(自动生成) + SQL语句 |
| @Database | 持有数据库实例、DAO引用、版本号 | RoomDatabase 子类 + 预编译SQL |
| Migration | 定义数据库版本升级时的数据迁移逻辑 | ALTER TABLE / INSERT / CREATE 等SQL |
2.2 工作流程拆解:从注解到SQL执行
Room使用Java注解处理器在编译阶段自动生成中间代码。以下是一个典型DAO查询方法的完整执行链路:
流程步骤(以 @Query("SELECT * FROM user WHERE id = :userId") 为例):
- 编译期:Room注解处理器解析
@Query注解,提取SQL模板和参数占位符,生成UserDao_Impl.java。 - 运行期(首次调用):Room调用
SQLiteStatement将SQL模板预编译compileStatement,缓存id列的索引。 - 执行:绑定参数
userId,执行simpleQueryForString,将Cursor映射为User对象(自动调用readUserFromCursor)。 - 缓存和LiveData:若返回
LiveData,Room注册InvalidationTracker观察表变化,表数据变更时自动重新查询。
// 编译后生成的 UserDao_Impl 片段(为清晰做了简化)
@Override
public User getUserById(final long userId) {
final String _sql = "SELECT * FROM user WHERE id = ?";
final RoomSQLiteQuery _query = RoomSQLiteQuery.acquire(_sql, 1);
_query.bindLong(1, userId);
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _query, false, null);
try {
final User _result;
if (_cursor.moveToFirst()) {
_result = __entityConversionResult.getUserFromCursor(_cursor);
} else {
_result = null;
}
return _result;
} finally {
_cursor.close();
_query.release();
}
}
2.3 实体(Entity)映射规则与避坑
实体类通过 @Entity 注解映射为数据库表。每个字段默认成为一列,字段名即列名。可通过 @ColumnInfo(name = "full_name") 自定义列名。
常见陷阱:
- 基本类型 vs 包装类型:
int默认值为0,Integer可为null。若数据库允许NULL,应使用包装类型。 - 主键不可变:
@PrimaryKey字段推荐使用long或String且设置为不可变(val)。 - 嵌入式对象:
@Embedded可将嵌套对象的字段展开到同一张表,避免关联查询。
@Entity(tableName = "user")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "full_name")
val name: String?,
val age: Int,
// 嵌入 Address 对象的字段:street, city
@Embedded
val address: Address
)
2.4 DAO 设计模式与性能关键点
| DAO方法类型 | 生成SQL/行为 | 注意事项 |
|---|---|---|
@Insert | INSERT INTO ... | 返回 long 表示新插入行的ID;@Insert(onConflict = OnConflictStrategy.REPLACE) |
@Update | UPDATE ... WHERE id = ? | 实体必须包含完整主键;更新行数通过返回值 int 获取 |
@Delete | DELETE FROM ... WHERE id = ? | 同样要求主键完整 |
@Query | 编译期校验SQL语法 | 支持 IN 参数、LIMIT、JOIN 等标准SQL |
性能警告:避免在DAO中写
@Query 时使用泛型 List<Object> 或 Cursor 返回,这会绕过类型安全映射且增加内存抖动。推荐始终返回实体对象或数据传输对象(DTO)。
2.5 数据库升级(Migration)机制
当实体定义变更(新增/删除/重命名列、修改约束),必须提高数据库版本号,并提供 Migration 对象告知Room如何迁移数据。Room不支持自动迁移,开发者需手动编写SQL。
// 从版本1升级到版本2:新增列 age
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
// 从版本2升级到版本3:重命名列 name -> full_name
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE user RENAME COLUMN name TO full_name")
}
}
// 构建时传入迁移列表
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
最佳实践:始终先备份用户数据再执行迁移;对于复杂表结构调整(如合并/拆分表),推荐先用
CREATE TABLE new_ + INSERT INTO ... SELECT + DROP TABLE old 策略。
2.6 Room 工作流程全链路 SVG 图
2.7 实践清单:Room使用的10条黄金规则
- 始终使用
@PrimaryKey且字段不可变:防止Room意外修改主键导致数据不一致。 - DAO方法返回
LiveData或Flow:利用Room的自动观察能力,避免手动刷新。 - 避免在DAO中写复杂JOIN:拆分为多次查询并用
@Transaction保证原子性。 - 数据库升级必须提供
Migration:绝不要设置fallbackToDestructiveMigration()在生产环境。 - 使用
@ColumnInfo显式命名列:避免因字段重命名导致混淆。 - 优先使用
List<Entity>而非Cursor:提升类型安全和性能。 - 大数据量操作放在后台线程:Room数据库操作默认在主线程抛出异常(除非调用
allowMainThreadQueries())。 - 合理使用
@Embedded和@Relation:减少嵌套查询并保持数据结构清晰。 - 利用
@Transaction将多个DAO调用组合为事务:保证数据一致性。 - 数据库版本号严格递增:每次发布新版本如修改表结构,必须加1且不可回退。
案例分析(常见错误修复):
某项目在v1中
某项目在v1中
User 实体包含 name 列,v2需要重命名为 full_name。开发者忘记编写 MIGRATION_1_2,只增加版本号导致应用闪退。正确做法:编写迁移SQL ALTER TABLE user RENAME COLUMN name TO full_name 并加入 addMigrations。若数据不重要,权衡后可调用 fallbackToDestructiveMigration() 并警告用户。