8、Room进阶:TypeConverter、关系映射、Migration、预填充数据库、测试、实战:复杂数据存储
Room 的基础用法,说白了就是增删改查。但真实项目里,数据哪有那么规矩?你存个 Date 对象,存个 List
嗯,这时候就得请出 TypeConverter 了。我个人习惯把 TypeConverter 看作是 Room 和复杂类型之间的翻译官。你告诉它怎么把复杂对象转成 Room 能存的基本类型,再告诉它怎么读回来,就完事了。
8.1 TypeConverter:让 Room 理解复杂类型
举个例子,你有个 User 实体,里面有个 Date 类型的 birthday 字段。Room 不认识 Date,怎么办?写个转换器:
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);
}
}
然后在 Database 类上加上注解:
@Database(entities = {User.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
// ...
}
搞定。Room 看到 Date 字段,会自动调用你的转换器。我在项目中遇到过有人把整个 Bitmap 对象存进数据库,结果转换器里写的是 Base64 编码——性能差到离谱。后来我建议只存图片路径,或者用 FileProvider 管理,才把问题解决。
8.2 关系映射:别被“关系”吓到
Room 不支持直接的对象引用,比如你有个 Order 实体,里面想直接放一个 List
那怎么处理一对多、多对多的关系?用嵌套查询或者 @Relation 注解。
举个例子,一个用户有多条订单:
public class UserWithOrders {
@Embedded
public User user;
@Relation(
parentColumn = "uid",
entityColumn = "userId"
)
public List<Order> orders;
}
然后在 DAO 里写个查询:
@Transaction
@Query("SELECT * FROM user")
public List<UserWithOrders> getUsersWithOrders();
@Transaction 是关键。不加它,Room 会报错,因为关联查询需要在一个事务里执行。我记得有次上线前测试,发现某个页面数据老是少几条,排查了半天,就是忘了加 @Transaction。
8.3 Migration:数据库升级不丢数据
App 发出去之后,数据库结构不可能一成不变。加个字段、改个表名,都是常事。Room 的 Migration 机制就是用来处理这个的。
假设你的数据库从版本 1 升到版本 2,要加一个 age 字段:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0");
}
};
然后在构建 Database 时加上:
Room.databaseBuilder(context, AppDatabase.class, "my-db")
.addMigrations(MIGRATION_1_2)
.build();
如果你没写 Migration,Room 会直接抛异常,或者根据 fallbackToDestructiveMigration() 策略把表删了重建。我曾经在测试环境里手滑,没写 Migration 就升级了版本,结果测试数据全没了,被测试妹子追着骂了一下午。
8.4 预填充数据库:App 安装就有数据
有些 App 第一次打开就需要展示一些基础数据,比如城市列表、分类标签。你不可能让用户等网络请求。Room 支持从 assets 目录下的预置数据库文件直接加载。
做法很简单:
- 在 assets 目录下放一个 databases/ 文件夹,里面放你的 .db 文件
- 构建时调用 createFromAsset()
Room.databaseBuilder(context, AppDatabase.class, "my-db")
.createFromAsset("databases/my-preloaded.db")
.build();
注意,预填充数据库的版本号必须和 Room 的版本号一致,否则 Room 会认为数据不匹配,直接重建。我建议你用一个脚本生成预填充数据库,每次改结构都重新生成一次,避免版本对不上。
8.5 测试:Room 代码必须测
数据库代码不测试,等于裸奔。Room 的测试其实很好写,因为它是纯本地操作,不依赖 Android 组件。
我一般用 AndroidX Test 里的 Room.inMemoryDatabaseBuilder 来创建内存数据库:
@RunWith(AndroidJUnit4.class)
public class UserDaoTest {
private AppDatabase database;
private UserDao userDao;
@Before
public void setUp() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
database = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
userDao = database.userDao();
}
@After
public void tearDown() {
database.close();
}
@Test
public void insertAndQuery() {
User user = new User("张三", 25);
userDao.insert(user);
List<User> users = userDao.getAll();
assertEquals(1, users.size());
assertEquals("张三", users.get(0).getName());
}
}
内存数据库的好处是快,每次测试完自动销毁,不会污染数据。我在 CI 流水线里跑这些测试,每次提交代码都会自动跑一遍,确保 DAO 没写错。
8.6 实战:复杂数据存储
最后,我们把这些知识点串起来,做一个实战案例:存储一个“用户订单详情”,里面包含用户信息、订单列表、每个订单的商品列表。
结构是这样的:
- User 表:uid, name, birthday
- Order 表:orderId, userId, totalPrice, createTime
- Product 表:productId, orderId, name, price, count
我们用 TypeConverter 处理 Date 和 BigDecimal:
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 fromBigDecimal(BigDecimal value) {
return value == null ? null : value.toPlainString();
}
@TypeConverter
public static BigDecimal toBigDecimal(String value) {
return value == null ? null : new BigDecimal(value);
}
}
然后定义关联查询的 POJO:
public class UserWithOrdersAndProducts {
@Embedded
public User user;
@Relation(
parentColumn = "uid",
entityColumn = "userId"
)
public List<OrderWithProducts> orders;
}
public class OrderWithProducts {
@Embedded
public Order order;
@Relation(
parentColumn = "orderId",
entityColumn = "orderId"
)
public List<Product> products;
}
DAO 里写一个查询:
@Transaction
@Query("SELECT * FROM user WHERE uid = :userId")
public UserWithOrdersAndProducts getUserDetail(String userId);
这样一次查询就能拿到用户、他的所有订单、每个订单的商品。性能上,Room 会帮你做嵌套查询,但如果你数据量特别大,建议分页加载,别一次性全查出来。
嗯,这个实战案例我在一个电商 App 里用过,效果还不错。唯一要注意的是,嵌套层级不要太深,三层以上基本就该考虑用其他方案了,比如直接查两次再手动组装。