4、DAO数据访问对象:@Insert插入、@Query查询、@Update更新、@Delete删除
DAO,全称 Data Access Object,数据访问对象。说白了,它就是你和数据库之间的「翻译官」。你想想看,Room 帮你把 SQLite 的脏活累活都干了,但总得有个接口让你告诉 Room「我要查数据」、「我要存数据」吧?这个接口就是 DAO。
我个人习惯把 DAO 看作是数据库操作的「菜单」。你点菜(调用方法),后厨(Room)帮你做菜(执行 SQL)。你完全不用关心 SQLite 的 Cursor 怎么遍历、ContentValues 怎么拼装——这些 Room 都替你搞定了。
核心要点:DAO 必须是一个接口(interface)或者抽象类(abstract class),并且要用 @Dao 注解标记。所有数据库操作都定义在这里。
4.1 @Insert:插入数据,就这么简单
先看最常用的插入操作。@Insert 注解可以加在方法上,参数就是你要插入的实体对象。
@Dao
public interface UserDao {
@Insert
void insertUser(User user);
@Insert
void insertUsers(List<User> users);
}
嗯,这里要注意:@Insert 默认是「全部插入」模式。什么意思?如果你插入的数据主键已经存在,它会直接抛异常。我在项目中遇到过这种情况——用户快速点击保存按钮,导致重复插入,直接崩了。
避坑指南:如果你希望「有则更新,无则插入」,可以用 @Insert(onConflict = OnConflictStrategy.REPLACE)。我曾经因为没加这个参数,线上出了好几次主键冲突的崩溃。
@Insert 方法可以返回 long 或 long[],表示插入行的 ID。我个人习惯返回 long,方便后续操作。
@Insert
long insertUser(User user); // 返回新插入行的ID
4.2 @Query:查询,最灵活也最危险
@Query 是 DAO 里最强大的注解。你可以在里面写任意 SQL 语句,Room 会帮你把结果映射成对象。但强大也意味着责任——SQL 写错了,编译期不会报错,运行时才崩。
@Dao
public interface UserDao {
@Query("SELECT * FROM users")
List<User> getAllUsers();
@Query("SELECT * FROM users WHERE id = :userId")
User getUserById(int userId);
@Query("SELECT * FROM users WHERE age > :minAge")
List<User> getUsersOlderThan(int minAge);
}
看到那个 :userId 了吗?这是 Room 的参数绑定语法。你方法里传的参数名,前面加个冒号,Room 会自动帮你拼到 SQL 里。我个人觉得这比写 ContentValues 舒服多了。
警告:@Query 里的 SQL 是纯文本,Room 不会帮你校验表名、列名是否存在。我曾经手滑把表名写错了,编译通过,运行直接崩。所以每次写完 @Query,我建议你跑一遍单元测试。
查询还可以返回 LiveData 或 Flow,这样数据变化时 UI 会自动更新。这个特性我特别喜欢——不用手动刷新列表了。
@Query("SELECT * FROM users")
LiveData<List<User>> getAllUsersLive();
4.3 @Update:更新,注意主键
@Update 和 @Insert 很像,但它根据主键来匹配要更新的行。你传进去的对象,Room 会拿它的主键去数据库里找,找到了就更新其他字段。
@Dao
public interface UserDao {
@Update
int updateUser(User user);
@Update
int updateUsers(List<User> users);
}
返回值 int 表示受影响的行数。如果返回 0,说明没找到匹配的主键。我在项目中遇到过一个问题:更新时忘了给对象设置主键,结果 Room 找不到记录,更新了个寂寞。
小技巧:@Update 也支持 onConflict 策略。不过我个人很少用,因为更新操作一般不会冲突。但如果你批量更新,建议加上事务保护。
4.4 @Delete:删除,同样靠主键
@Delete 和 @Update 的逻辑一模一样——根据主键匹配。你传一个对象进去,Room 找到主键对应的行,删掉。
@Dao
public interface UserDao {
@Delete
int deleteUser(User user);
@Delete
int deleteUsers(List<User> users);
@Query("DELETE FROM users WHERE age < :age")
int deleteUsersYoungerThan(int age);
}
看到第三个方法了吗?@Delete 注解只能按主键删。如果你想按条件删,比如「删除所有年龄小于18岁的用户」,那就得用 @Query 写 DELETE 语句。这个坑我踩过——一开始以为 @Delete 可以传条件,结果发现它只认主键。
注意:@Delete 如果传了一个数据库中不存在的对象(主键没匹配上),不会报错,只是返回 0。所以记得检查返回值。
4.5 综合示例:一个完整的 DAO
下面是一个实际项目中我常用的 DAO 写法。包含了增删改查,以及一些实用技巧。
@Dao
public interface UserDao {
// 插入,冲突时替换
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertUser(User user);
// 批量插入
@Insert
long[] insertUsers(List<User> users);
// 查询所有,返回LiveData
@Query("SELECT * FROM users ORDER BY id DESC")
LiveData<List<User>> getAllUsers();
// 按ID查询
@Query("SELECT * FROM users WHERE id = :id")
LiveData<User> getUserById(int id);
// 模糊查询
@Query("SELECT * FROM users WHERE name LIKE '%' || :keyword || '%'")
List<User> searchUsers(String keyword);
// 更新
@Update
int updateUser(User user);
// 按条件删除
@Query("DELETE FROM users WHERE lastActive < :timestamp")
int deleteInactiveUsers(long timestamp);
// 清空表
@Query("DELETE FROM users")
void deleteAllUsers();
}
个人经验:我建议每个 DAO 都加上一个「清空表」的方法。开发测试时特别有用,不用手动去删数据库文件。但上线前记得把这个方法去掉,或者加权限控制。
4.6 DAO 的知识体系
下面这张图帮你理清 DAO 的核心脉络。你看,所有操作都围绕实体类展开,@Insert、@Query、@Update、@Delete 各司其职。
4.7 避坑总结
最后,我把这些年用 DAO 踩过的坑整理成一张表。你写代码时对照着看,能省不少调试时间。
| 注解 | 常见坑 | 解决方案 |
|---|---|---|
| @Insert | 主键冲突导致崩溃 | 使用 onConflict = REPLACE |
| @Query | SQL 语法错误,运行时才暴露 | 写单元测试,或用 Room 的编译时校验 |
| @Update | 忘记设置主键,更新无效 | 更新前确保对象主键不为空 |
| @Delete | 只能按主键删除,不能按条件 | 条件删除用 @Query 写 DELETE 语句 |
嗯,DAO 的内容就这些。说白了就是四个注解,但每个注解背后都有不少细节。我个人建议你写 DAO 时多想想「如果数据不存在怎么办」、「如果并发操作怎么办」。想清楚了,代码就稳了。