DAO 接口:@Dao、@Insert、@Query、@Update、@Delete 的用法与注意事项
DAO(Data Access Object)是 Room 的入口,也是你与数据库打交道的唯一通道。说白了,Room 帮你省去了写 Helper 和 Cursor 的繁琐,你只需要定义一个接口或抽象类,在上面贴几个注解,Room 就会在编译期帮你生成实现代码。
我在项目中见过不少新人把 DAO 写得像 SQL 脚本堆砌,结果一编译就报错。其实核心就五个注解:@Dao、@Insert、@Query、@Update、@Delete。掌握好它们,你就掌握了 Room 的数据操作能力。
一、@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 其实很贴心,只要能绕过它设的那些小陷阱。