10、数据存储(下):Room数据库进阶
上一章我们把 Room 的基本用法过了一遍。说实话,那只是冰山一角。真正到了生产环境,你会发现事情远没那么简单——数据迁移、版本管理、还有那个让人又爱又恨的 SharedPreferences。今天我们就来把这些硬骨头啃掉。
10.1 DAO 的进阶玩法
DAO 是 Room 的灵魂。我见过太多人把 DAO 写成简单的 CRUD,其实它能做的事情远不止这些。
10.1.1 复杂查询与返回类型
你想想看,实际项目中哪有那么多简单的查询?往往是多表关联、分组统计、模糊搜索。Room 的 DAO 支持直接返回 LiveData 或 Flow,这样数据变化时 UI 会自动更新。
@Dao
public interface UserDao {
// 返回 LiveData,自动监听变化
@Query("SELECT * FROM users WHERE age > :minAge")
LiveData<List<User>> getUsersOlderThan(int minAge);
// 返回 Flow,Kotlin 协程友好
@Query("SELECT * FROM users WHERE name LIKE '%' || :keyword || '%'")
Flow<List<User>> searchUsers(String keyword);
// 多表关联查询
@Query("SELECT u.name, o.orderId, o.totalPrice " +
"FROM users u INNER JOIN orders o ON u.id = o.userId " +
"WHERE u.id = :userId")
LiveData<List<UserOrder>> getUserOrders(int userId);
}
嗯,这里要注意:返回 LiveData 时,Room 会在后台线程自动执行查询,然后通过主线程通知观察者。你不需要自己处理线程切换,这点很贴心。
10.1.2 事务处理
我在项目中遇到过一个问题:同时插入订单和更新用户余额,结果订单写进去了,余额更新失败了。数据就不一致了。这时候就需要事务。
@Dao
public interface OrderDao {
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
LiveData<UserWithOrders> getUserWithOrders(int userId);
@Transaction
default void placeOrder(Order order, User user) {
insertOrder(order);
updateUserBalance(user);
}
@Insert
void insertOrder(Order order);
@Update
void updateUserBalance(User user);
}
@Transaction 注解保证里面的操作要么全部成功,要么全部回滚。我曾经因为没加这个注解,导致线上数据出了大问题,从那以后我再也不敢偷懒了。
10.2 Entity 的高级特性
Entity 不只是简单的数据类。Room 提供了很多注解来帮你处理复杂的数据结构。
10.2.1 嵌套对象与类型转换器
有时候一个字段需要存储复杂对象,比如地址信息。Room 不支持直接存储对象,但你可以用 @TypeConverters 来解决。
public class Address {
public String city;
public String street;
public String zipCode;
}
public class Converters {
@TypeConverter
public static Address fromString(String value) {
// 假设用 JSON 序列化
return new Gson().fromJson(value, Address.class);
}
@TypeConverter
public static String fromAddress(Address address) {
return new Gson().toJson(address);
}
}
@Entity
@TypeConverters(Converters.class)
public class User {
@PrimaryKey
public int id;
public String name;
public Address address; // 自动转换
}
说白了,TypeConverter 就是告诉 Room:「这个字段怎么存、怎么读」。我习惯把所有的转换器放在一个类里统一管理,这样代码更清晰。
10.2.2 索引与唯一约束
数据库查询慢,很多时候是因为没加索引。Room 支持在 Entity 上直接定义索引。
@Entity(indices = {
@Index(value = "name"),
@Index(value = {"city", "street"}, unique = true)
})
public class User {
@PrimaryKey
public int id;
public String name;
public String city;
public String street;
}
unique = true 表示这个组合值不能重复。我在做用户系统时,就用它来保证邮箱和手机号的唯一性,比在代码里判断要靠谱得多。
10.3 Database 的版本管理
App 更新了,数据库结构变了,怎么办?Room 提供了版本管理和迁移机制。
10.3.1 定义迁移
每次数据库结构变化,你都需要写一个 Migration 对象,告诉 Room 怎么从旧版本升级到新版本。
@Database(entities = {User.class, Order.class}, version = 2)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
public abstract OrderDao orderDao();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
// 添加新表
database.execSQL(
"CREATE TABLE IF NOT EXISTS `orders` (" +
"`orderId` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`userId` INTEGER NOT NULL, " +
"`totalPrice` REAL NOT NULL, " +
"`createTime` INTEGER NOT NULL, " +
"FOREIGN KEY(`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE" +
")"
);
// 添加新列
database.execSQL("ALTER TABLE `users` ADD COLUMN `avatar` TEXT");
}
};
}
创建数据库时,把迁移策略传进去:
Room.databaseBuilder(context, AppDatabase.class, "my_database")
.addMigrations(AppDatabase.MIGRATION_1_2)
.build();
10.3.2 破坏性迁移
如果迁移写错了,或者用户从很老的版本直接升级,Room 会抛出异常。这时候可以用 destructiveMigrationFallback 来兜底——直接删掉旧数据重建。
Room.databaseBuilder(context, AppDatabase.class, "my_database")
.fallbackToDestructiveMigration()
.build();
我曾经在测试环境用过这个,结果忘了改回来就发布了。用户数据全没了,那叫一个惨。嗯,从那以后我每次发布前都会检查 migration 配置。
10.4 DataStore 替代 SharedPreferences
SharedPreferences 的问题大家都知道:不支持异步、容易 ANR、类型不安全。Google 官方推荐用 DataStore 替代它。
10.4.1 Preferences DataStore
适合存储简单的键值对,比如用户设置、开关状态。
// 创建 DataStore
val Context.dataStore by preferencesDataStore(name = "settings")
// 读取
val themeMode: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[booleanPreferencesKey("dark_mode")] ?: false
}
// 写入
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[booleanPreferencesKey("dark_mode")] = enabled
}
}
DataStore 使用 Flow 来暴露数据,天然支持协程和生命周期感知。你想想看,SharedPreferences 的 commit 和 apply 那套东西,是不是瞬间就不香了?
10.4.2 Proto DataStore
如果你需要存储结构化数据,比如用户配置对象,可以用 Proto DataStore。它基于 Protocol Buffers,类型安全,性能更好。
// 定义 proto 文件
message UserPreferences {
bool dark_mode = 1;
int32 font_size = 2;
string language = 3;
}
// 创建 DataStore
val Context.userPreferencesStore by protoDataStore(
fileName = "user_preferences.pb",
serializer = UserPreferencesSerializer
)
// 读取
val userPreferences: Flow<UserPreferences> = context.userPreferencesStore.data
// 写入
suspend fun updateFontSize(size: Int) {
context.userPreferencesStore.updateData { preferences ->
preferences.toBuilder()
.setFontSize(size)
.build()
}
}
10.5 知识体系总览
下面这张图总结了本章的核心知识点,你可以把它当作一个快速参考。
这张图把 Room 进阶的三个核心模块串起来了。DAO 负责数据操作,Entity 定义数据结构,Database 管理版本和迁移。三者配合好了,你的数据层才能稳如磐石。
DataStore 这块,我建议你尽快把项目里的 SharedPreferences 替换掉。虽然迁移需要一点工作量,但长期来看绝对值得。毕竟,谁也不想因为一个简单的配置读写导致 ANR 吧?
好了,这一章的内容就到这里。Room 的进阶用法其实还有很多,比如预填充数据库、多数据库实例、测试辅助等。但掌握了今天讲的这些,你已经能应对绝大多数生产场景了。