3. Room 数据库搭建与迁移:单例模式、类型转换器、迁移策略与数据验证
3.1 单例模式数据库创建
原理:Room 数据库实例体积较大,且为重量级组件。多次创建会导致资源浪费、连接泄漏甚至应用崩溃。单例模式确保整个 App 生命周期内只存在一个 AppDatabase 对象,通过双重检查锁定(Double-Checked Locking)或静态内部类持有实例,保证线程安全与懒加载。
实现流程:
- 定义抽象类继承
RoomDatabase。 - 声明所有实体(
@Entity)和 DAO(@Dao)。 - 通过
Room.databaseBuilder()构建实例。 - 使用
volatile变量 +synchronized实现线程安全单例。
代码示例:
@Database(entities = {User.class, Task.class}, version = 2, exportSchema = true)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract TaskDao taskDao();
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "app_database.db")
.addCallback(roomCallback)
.addMigrations(MIGRATION_1_2)
.build();
}
}
}
return INSTANCE;
}
}
最佳实践:永远使用
context.getApplicationContext() 而非 Activity 或 Fragment 的 context,避免内存泄漏。
3.2 类型转换器(TypeConverter)
原理:Room 只支持基本数据类型及其封装、String、byte[]。对于 Date、List、Enum 等复杂类型,必须定义 TypeConverter 将其序列化为 Room 可存储的格式(如 Long 时间戳、JSON 字符串)。
实现流程:
- 创建静态方法,使用
@TypeConverter注解标记。 - 在
@Database注解的typeConverters属性中注册该转换器类。
代码示例:
public class Converters {
@TypeConverter
public static Long fromDate(Date date) {
return date == null ? null : date.getTime();
}
@TypeConverter
public static Date toDate(Long timestamp) {
return timestamp == null ? null : new Date(timestamp);
}
@TypeConverter
public static String fromList(List<String> list) {
return list == null ? null : new Gson().toJson(list);
}
@TypeConverter
public static List<String> toList(String json) {
return json == null ? null : new Gson().fromJson(json, new ListTypeToken<String>(){}.getType());
}
}
// 在 Database 类上注册
@Database(entities = {User.class, Task.class}, version = 2,
typeConverters = {Converters.class})
public abstract class AppDatabase extends RoomDatabase { ... }
3.3 迁移策略配置
原理:数据库 schema 变更(增删表/列、修改约束)后,Room 需要从旧版本升级到新版本。提供 Migration 对象,通过 SQL 语句手动编写迁移逻辑。若未提供迁移且版本不符,Room 会抛出 IllegalStateException(除非显式调用 fallbackToDestructiveMigration() 强制销毁重建)。
实现流程:
- 创建
Migration(startVersion, endVersion)实例。 - 在
migrate方法内执行 SQL(如ALTER TABLE ...或CREATE TABLE ...)。 - 通过
addMigrations(migration1, migration2...)注册到 Builder 链中。
代码示例:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Task ADD COLUMN priority INTEGER NOT NULL DEFAULT 0");
database.execSQL("CREATE TABLE IF NOT EXISTS `Log` (`id` INTEGER NOT NULL, " +
"`message` TEXT, PRIMARY KEY(`id`))");
}
};
危险操作:
fallbackToDestructiveMigration() 会在升级失败时删除所有数据并重建表。仅用于开发调试,正式发布前必须实现完整的 Migration。
3.4 数据验证
验证方法:
- Android Studio Database Inspector:直接运行 App 后,在 Tools → Database Inspector 中查看当前数据库内容、执行 SQL 查询。
- 单元测试(JUnit + Room in-memory):创建内存数据库,插入数据后验证迁移结果。
- UI 测试(Espresso):验证界面展示的数据一致性。
单元测试示例:
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB_NAME = "migration-test";
@Rule
public TestRule rule = new InstantTaskExecutorRule();
@Test
public void migrate1To2_containsPriorityColumn() {
SupportSQLiteDatabase db = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase.class, TEST_DB_NAME)
.addMigrations(MIGRATION_1_2)
.build()
.getOpenHelper()
.getWritableDatabase();
// 验证新列存在
Cursor cursor = db.query("PRAGMA table_info(Task)");
boolean foundPriority = false;
while (cursor.moveToNext()) {
if ("priority".equals(cursor.getString(cursor.getColumnIndex("name")))) {
foundPriority = true;
break;
}
}
assertTrue(foundPriority);
cursor.close();
db.close();
}
}
3.5 常见错误与性能优化
| 错误 / 问题 | 原因 | 解决方案 |
|---|---|---|
java.lang.IllegalStateException: Room cannot verify the data integrity | 版本号未同步或 Migration 缺失 | 检查 @Database(version) 与 Migration 起始版本一致 |
单例双重检查锁定中 INSTANCE 未 volatile | 指令重排可能导致返回未完全初始化的实例 | 添加 volatile 关键字 |
| 类型转换器未注册导致运行时崩溃 | Room 无法序列化自定义类型 | 在 @Database(typeConverters = {...}) 中注册 |
| 大量对象重复创建导致 GC 频繁 | 转换器内部创建过多临时对象(如 Gson 实例) | 将 Gson 提升为静态字段复用 |
3.6 整体流程图
下图展示了 Room 数据库创建、类型转换、迁移及验证的完整链路:
3.7 性能与稳定性建议
- 懒加载与缓存:单例数据库实例在首次调用
getInstance()时创建,避免冷启动时占用主线程。若需预加载,在 Application.onCreate() 中异步初始化。 - 减少转换器开销:将 Gson 实例定义为
static final,避免每转换一次新建对象。对于频繁读写的字段(如 Integer 列表),考虑直接使用Room.TypeConverter原生支持的类型。 - 迁移测试自动化:为每个 Migration 编写独立的单元测试,确保破坏性变更不会导致用户数据丢失。利用
Room.inMemoryDatabaseBuilder()快速验证。 - Schema 导出:在
@Database(exportSchema = true)配合 Gradle 配置room.schemaLocation,可生成 JSON Schema 文件,用于比较和版本控制。