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 各司其职。

DAO 数据访问对象 @Insert 插入 @Query 查询 @Update 更新 @Delete 删除 所有操作都围绕实体类(Entity)进行,Room 自动生成实现代码

4.7 避坑总结

最后,我把这些年用 DAO 踩过的坑整理成一张表。你写代码时对照着看,能省不少调试时间。

注解 常见坑 解决方案
@Insert 主键冲突导致崩溃 使用 onConflict = REPLACE
@Query SQL 语法错误,运行时才暴露 写单元测试,或用 Room 的编译时校验
@Update 忘记设置主键,更新无效 更新前确保对象主键不为空
@Delete 只能按主键删除,不能按条件 条件删除用 @Query 写 DELETE 语句

嗯,DAO 的内容就这些。说白了就是四个注解,但每个注解背后都有不少细节。我个人建议你写 DAO 时多想想「如果数据不存在怎么办」、「如果并发操作怎么办」。想清楚了,代码就稳了。


公众号:蓝海资料掘金营,微信deep3321