DAO 接口:@Dao、@Insert、@Query、@Update、@Delete 的用法与注意事项

DAO(Data Access Object)是 Room 的入口,也是你与数据库打交道的唯一通道。说白了,Room 帮你省去了写 Helper 和 Cursor 的繁琐,你只需要定义一个接口或抽象类,在上面贴几个注解,Room 就会在编译期帮你生成实现代码。

我在项目中见过不少新人把 DAO 写得像 SQL 脚本堆砌,结果一编译就报错。其实核心就五个注解:@Dao、@Insert、@Query、@Update、@Delete。掌握好它们,你就掌握了 Room 的数据操作能力。

DAO 接口注解关系图 @Database @Dao 接口 / 抽象类 @Insert @Query @Update @Delete 参数: Entity 或 List 参数: SQL + 返回值 参数: Entity 或 List 参数: Entity 或 List 所有 DAO 方法必须运行在合适的线程(@Query 读可主线程,写推荐后台线程)

一、@Dao:标记接口还是抽象类?

@Dao 可以修饰接口,也可以修饰抽象类。我个人习惯用接口——更简洁,而且 Room 会自动生成实现。抽象类的好处是你可以写一些默认方法或成员变量,但多数场景用不到。

小技巧:如果你需要在 DAO 中暴露 LiveData 或 Flow,接口完全够用。抽象类里的非抽象方法 Room 不会帮你实现,需要自己写,容易出错——我曾经踩过这个坑。

二、@Insert:别踩重复数据的坑

@Insert 用于插入一条或多条记录。方法参数可以是实体对象,也可以是实体集合。默认行为是「如果数据已存在(主键冲突),直接抛异常」。但你可以通过 @Insert(onConflict = OnConflictStrategy.REPLACE) 改为替换模式。

@Dao
public interface UserDao {
    // 插入单个用户
    @Insert
    long insertUser(User user);

    // 插入多个,返回每行插入的行id数组
    @Insert
    long[] insertUsers(List<User> users);

    // 冲突时替换
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertOrReplace(User user);
}

注意:返回值可以是 long(单条插入的 rowId)或 long[]/List<Long>(批量插入的 rowId)。如果插入失败且没有冲突策略,返回 -1。

避坑提醒:我在项目中遇到过连续插入大量数据导致 UI 卡死。原因是在主线程调用了 @Insert。Room 默认不允许在主线程写数据库——除非你调用 allowMainThreadQueries(),但强烈不建议这么做。批量插入请使用 Coroutine 或 RxJava 切到后台线程。

三、@Query:最灵活,也最容易翻车

@Query 是 DAO 的灵魂。你写任何 SQL 查询,Room 都会在编译期校验语法。返回值可以是 LiveData、Flow、RxJava 的 Flowable、或者普通 POJO。

@Dao
public interface UserDao {
    // 根据 id 查询,返回 LiveData 实现自动刷新
    @Query("SELECT * FROM user WHERE uid = :uid")
    LiveData<User> getUserById(int uid);

    // 查询年龄大于某个值的用户列表
    @Query("SELECT * FROM user WHERE age > :minAge")
    List<User> getUsersOlderThan(int minAge);

    // 自定义查询结果,只取部分字段
    @Query("SELECT name, age FROM user WHERE uid = :uid")
    UserNameAndAge getUserNameAndAge(int uid);
}

关键点:

  • 参数用冒号绑定到 SQL 里的变量名(如 :uid)。
  • 如果你返回 LiveData 或 Flow,当表数据变化时,Room 会自动重新查询并推送新结果。但要注意——只在数据库写操作提交后才会触发重新查询,不是实时监听 ContentProvider。
  • 如果查询结果不存在,Room 不会抛异常,而是返回 null(单个对象)或空列表。我建议在访问返回数据前做 null 检查,否则空指针找上门。

我的经验:曾经有一个需求是模糊搜索用户名,我写了 WHERE name LIKE '%:keyword%',结果 Room 报错。原因是 Room 不支持在 LIKE 里直接拼接冒号参数。正确写法是 LIKE '%' || :keyword || '%'。我花了半小时才查到这个坑——记录在案。

四、@Update 和 @Delete:别忘了检查影响的行数

@Update 和 @Delete 都根据主键来匹配记录。如果不指定主键(比如传了一个没有主键的临时对象),Room 不知道更新哪一行,会直接抛出异常。

@Dao
public interface UserDao {
    // 更新用户:根据主键 uid 匹配
    @Update
    int updateUser(User user);

    // 删除指定用户
    @Delete
    int deleteUser(User user);

    // 按条件删除(推荐用 @Query 写自定义 SQL)
    @Query("DELETE FROM user WHERE age < :minAge")
    int deleteUsersUnderAge(int minAge);
}

返回值 int 表示受影响的行数。我习惯判断返回值是否 > 0 来决定是否刷新 UI。如果返回值是 0,说明没有匹配到主键对应的记录——可能是数据已被删除,或者参数对象的主键字段为空。

曾经踩过的坑:我写过一个 @Update 方法,传入的 User 对象只有 id 和 name 字段,其他字段都是默认值。结果 Room 把数据库中整条记录的其他字段更新成了 null。因为 Room 的 @Update 会更新实体中所有非 null 字段?错了——它更新所有字段,即使字段值为 null 也会写入。解决方案:要么先查询完整实体再修改,要么用 @Query 写局部更新 SQL。

五、核心注意事项一览表

注解 常见用法 注意点
@Dao 标记接口或抽象类 接口简单;抽象类可加默认方法但易出错
@Insert long insert(T entity) 主键冲突默认抛异常;使用 OnConflictStrategy 避免崩溃
@Query 写任意 SQL,支持 LiveData 参数用冒号绑定;LIKE 时用 '%' || :param || '%';注意线程
@Update int update(T entity) 更新所有字段(即使 null);务必有主键且匹配
@Delete int delete(T entity) 根据主键删除;返回影响行数;自定义删除用 @Query

六、几个实用小贴士

  • 事务:如果你需要在 DAO 中组合多个操作(比如插入后更新),可以在方法上加 @Transaction 注解。注意:@Transaction 只能用于标记到 @Query 或 @Insert/@Update/@Delete 方法上,不能用于普通方法。我一般用 @Transaction + @Query 来执行多条 SQL。
  • 返回值类型:除了基本类型和实体,还可以返回 Cursor(不推荐),或者用 @ColumnInfo 注解的 POJO。如果你希望查询结果可观察,尽量用 LiveData 或 Flow,它们会在数据变化时自动通知。
  • 编译期校验:Room 会在编译时检查 @Query 里的 SQL 语法和参数名称。如果写错,IDE 会直接报红。利用好这一点,能避免很多运行时崩溃。

嗯,关于 DAO 注解的基本用法,今天就聊到这儿。上面提到的这些点,你在实际开发中可能都会遇到——尤其那个 LIKE 参数绑定的坑,我记忆犹新。动手写几个 DAO,把各个注解都试一遍,你会发现 Room 其实很贴心,只要能绕过它设的那些小陷阱。