Room CRUD操作:DAO方法编写与LiveData/Flow配合
写DAO方法这事儿,说简单也简单,说坑也不少。
我记得刚用Room那会儿,还傻乎乎地在主线程查数据库,结果直接爆IllegalStateException。后来才明白,Room内置了线程检查——你必须在子线程执行数据库操作。但如果你用LiveData或Flow做返回值,这些脏活累活Room会自动帮你调度好。
这一章我带你走一遍最常用的四个操作:插入、查询、更新、删除,顺便聊聊怎么跟LiveData、Flow打成一片。
核心原则:DAO方法返回LiveData或Flow时,Room会自动在后台线程执行查询,并把结果post到主线程。你只需要关心数据流,不用手动切换线程。
1. 插入操作:@Insert
插入最简单,直接在DAO里写个方法,加上@Insert注解。
@Dao
public interface UserDao {
@Insert
void insert(User user); // 普通插入
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertOrReplace(User user); // 冲突时替换,返回 rowId
@Insert
void insertAll(List<User> users); // 批量插入
}
个人习惯:我一般把onConflict = REPLACE用在唯一键更新场景。曾经有个项目没配这个,结果重复插入直接崩了……你想想看,线上崩溃日志里全是SQLiteConstraintException,多吓人。
@Insert支持返回long或List<Long>,代表插入行的rowId。如果你不需要,用void也行。
2. 查询操作:@Query
查询是重头戏。Room要求你在@Query注解里写原生的SQL,注意不是JPQL,是纯SQLite语法。
@Dao
public interface UserDao {
// 同步查询,必须放到后台线程
@Query("SELECT * FROM users WHERE id = :userId")
User getUserSync(int userId);
// 返回 LiveData,自动在后台执行并通知主线程
@Query("SELECT * FROM users WHERE age > :minAge")
LiveData<List<User>> getUsersOlderThan(int minAge);
// 返回 Flow(Kotlin 写法,但 Java 里也能用,需 kotlinx-coroutines)
@Query("SELECT * FROM users ORDER BY name ASC")
Flow<List<User>> getAllUsersFlow();
}
这里有个坑:LiveData的初始值问题。如果你查询的结果集很大,第一次观察LiveData时可能卡一下。我建议把查询条件做得精确些,不要一股脑全查出来。
我曾经踩过的坑:在@Query里写了JOIN,但没给列起别名,导致Room编译时死活报错——字段名冲突。Room要求select出来的列必须能映射到实体字段。如果你用SELECT * FROM table1 JOIN table2 ...,两个表有同名列,Room会懵。解决办法:要么用别名,要么写个POJO接收。
3. 更新操作:@Update
@Update按主键匹配,更新非主键字段。如果你只传一个实体对象,Room会拿它的主键去SQLite里找,然后把其他字段覆盖上去。
@Dao
public interface UserDao {
@Update
int update(User user); // 返回受影响的行数
@Update
int updateUsers(List<User> users); // 批量更新
}
关键点:@Update要求实体必须包含主键,否则Room会在编译期报错。你想想看,如果你用了@PrimaryKey(autoGenerate = true),但插入时没设置主键,Room会自动生成。但在更新时,你必须提供正确的id,否则它更新的是另一条记录(或者0行)。
实际项目中,我更推荐用@Query写update语句,尤其是只更新个别字段时。因为@Update会覆盖整个对象,如果你只想改一个字段,就得先查出来再改,多了一次查询。
@Query("UPDATE users SET name = :newName WHERE id = :userId")
int updateName(int userId, String newName);
4. 删除操作:@Delete
@Delete和@Update类似,也是按主键匹配。
@Dao
public interface UserDao {
@Delete
int delete(User user); // 根据主键删除
@Delete
int deleteAll(List<User> users); // 批量删除
// 我还是习惯用 @Query 直接写 SQL,更灵活
@Query("DELETE FROM users WHERE age < :maxAge")
int deleteYoungerThan(int maxAge);
}
| 操作 | 注解 | 返回值 | 是否支持活数据 |
|---|---|---|---|
| 插入 | @Insert | long / void | 否,需手动刷新 |
| 查询 | @Query | T / LiveData / Flow | 是,推荐 |
| 更新 | @Update / @Query | int | 否,但数据变化会自动触发查询类型的LiveData |
| 删除 | @Delete / @Query | int | 否,同上 |
5. 与LiveData / Flow的整合机制
Room是怎么让LiveData自动更新的?其实很简单:Room内部维护了一个RoomTrackingLiveData,它会在后台注册一个ContentObserver监听对应表的变化。只要那张表有任何行被插入、更新、删除,LiveData就会重新执行查询并发射新数据。
Flow的原理类似,但结合了Kotlin协程的冷流特性,在Android上表现更轻量。我个人偏好用Flow,因为它可以配合flowOn(Dispatchers.IO)明确线程,而且没有LiveData的setValue/postValue混淆问题。
注意:如果DAO方法同时返回LiveData和没有LiveData的查询,更新数据时只有那些带观察者的查询才会收到通知。但Room会通过InvalidationTracker统一管理,你不用担心手动通知。我曾经看到一个项目在更新数据后手动调用postValue——完全多余,Room自己就会搞定。
6. 实战案例:一个完整的DAO
给你看个我自己项目里的DAO片段,结合了所有操作和LiveData:
@Dao
public interface TaskDao {
@Query("SELECT * FROM tasks WHERE completed = 0 ORDER BY priority DESC")
LiveData<List<Task>> getActiveTasks();
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertTask(Task task);
@Update
int updateTask(Task task);
@Query("UPDATE tasks SET completed = 1 WHERE id = :taskId")
int completeTask(long taskId);
@Delete
int deleteTask(Task task);
}
在ViewModel里,你直接用Transformations.map或者switchMap处理,或者在Kotlin里直接用flatMapLatest。其实写法挺固定的,你写两遍就熟了。
最后啰嗦一句:不要在主线程调同步的DAO方法。哪怕你只是做个小demo,Room也会抛异常。要么用asyncTask,要么用LiveData,或者用RxJava/Coroutine。我强烈建议你从一开始就习惯Flow或LiveData,后面省很多事。
核心总结:写好CRUD,记住三件事——SQL写对、线程别堵、用LiveData/Flow做观察者。这样你后面写Repository层就轻松多了。