3. Room 数据库搭建与迁移:单例模式、类型转换器、迁移策略与数据验证

3.1 单例模式数据库创建

原理:Room 数据库实例体积较大,且为重量级组件。多次创建会导致资源浪费、连接泄漏甚至应用崩溃。单例模式确保整个 App 生命周期内只存在一个 AppDatabase 对象,通过双重检查锁定(Double-Checked Locking)或静态内部类持有实例,保证线程安全与懒加载。

实现流程:

  1. 定义抽象类继承 RoomDatabase。
  2. 声明所有实体(@Entity)和 DAO(@Dao)。
  3. 通过 Room.databaseBuilder() 构建实例。
  4. 使用 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() 强制销毁重建)。

实现流程:

  1. 创建 Migration(startVersion, endVersion) 实例。
  2. 在 migrate 方法内执行 SQL(如 ALTER TABLE ... 或 CREATE TABLE ...)。
  3. 通过 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 数据库创建、类型转换、迁移及验证的完整链路:

Room 数据库搭建与迁移流程 定义 Entity & DAO 实现 TypeConverter 单例 AppDatabase 编写 Migration 对象 在 @Database 中注册 Room.databaseBuilder() 数据验证 验证方式: • Database Inspector • 单元测试(Memory DB)

3.7 性能与稳定性建议

  • 懒加载与缓存:单例数据库实例在首次调用 getInstance() 时创建,避免冷启动时占用主线程。若需预加载,在 Application.onCreate() 中异步初始化。
  • 减少转换器开销:将 Gson 实例定义为 static final,避免每转换一次新建对象。对于频繁读写的字段(如 Integer 列表),考虑直接使用 Room.TypeConverter 原生支持的类型。
  • 迁移测试自动化:为每个 Migration 编写独立的单元测试,确保破坏性变更不会导致用户数据丢失。利用 Room.inMemoryDatabaseBuilder() 快速验证。
  • Schema 导出:在 @Database(exportSchema = true) 配合 Gradle 配置 room.schemaLocation,可生成 JSON Schema 文件,用于比较和版本控制。