Room CRUD操作:DAO方法编写与LiveData/Flow配合

写DAO方法这事儿,说简单也简单,说坑也不少。

我记得刚用Room那会儿,还傻乎乎地在主线程查数据库,结果直接爆IllegalStateException。后来才明白,Room内置了线程检查——你必须在子线程执行数据库操作。但如果你用LiveData或Flow做返回值,这些脏活累活Room会自动帮你调度好。

这一章我带你走一遍最常用的四个操作:插入、查询、更新、删除,顺便聊聊怎么跟LiveData、Flow打成一片。

核心原则:DAO方法返回LiveData或Flow时,Room会自动在后台线程执行查询,并把结果post到主线程。你只需要关心数据流,不用手动切换线程。

Room CRUD + LiveData/Flow 数据流 Activity / Fragment ViewModel DAO 接口 @Insert / @Update / @Delete / @Query RoomDatabase 编译时生成实现 SQLite LiveData/Flow 💡 DAO 返回 LiveData/Flow 时, Room 自动在 IO 线程查询并通知 UI

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);
}
操作 注解 返回值 是否支持活数据
插入@Insertlong / void否,需手动刷新
查询@QueryT / LiveData / Flow是,推荐
更新@Update / @Queryint否,但数据变化会自动触发查询类型的LiveData
删除@Delete / @Queryint否,同上

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层就轻松多了。