4. DAO基础:增删改查操作
DAO,全称 Data Access Object,在 Room 里就是你的"数据库翻译官"。说白了,你不需要写一行 SQLite 的模板代码,只用在接口上写几个注解,Room 就会自动帮你生成实现。我刚开始用 Room 时最爽的就是这点——再也不用写上百行的 SQLiteOpenHelper 了。
但别小看这几个注解。增删改查看似简单,用不对照样翻车。我曾经在项目里因为 @Insert 的冲突策略没配好,导致线上用户数据丢失了一部分……嗯,从那以后我对每个注解都多留了个心眼。
核心要点:DAO 是一个接口或抽象类,用 @Dao 标记。每个方法用 @Insert、@Update、@Delete、@Query 注解,Room 自动生成 SQL 执行。
4.1 @Insert — 插入数据
@Insert 是最简单的。你传一个实体对象进去,就插一条。你传多个,它批量插。你传一个数组或集合,也支持。
@Dao
interface UserDao {
@Insert
fun insertUser(user: User)
@Insert
fun insertUsers(users: List<User>)
@Insert
fun insertUserReturnLong(user: User): Long // 返回新插入行的 rowId
}
返回值可以是什么?
void或Unit— 不要返回值Long— 单条插入时返回 rowIdList<Long>— 批量插入时返回每个 rowId
我曾经踩过的坑:如果不指定冲突策略,默认是 OnConflictStrategy.ABORT,重复插入主键会直接崩溃。记得设置 @Insert(onConflict = OnConflictStrategy.REPLACE) 或 IGNORE。
你想想看,如果你的表有 UNIQUE 约束,插入重复数据时直接抛异常,用户体验多差。我一般都用 REPLACE,除非业务明确要求保留旧数据。
4.2 @Update — 更新数据
更新同样简单。Room 会根据实体类的 @PrimaryKey 来匹配要更新的行。注意:如果传进来的对象主键不存在,Room 不会抛异常,但也不会更新任何行。
@Dao
interface UserDao {
@Update
fun updateUser(user: User): Int // 返回受影响的行数
@Update
fun updateUsers(users: List<User>)
}
返回值 int 非常实用。我在项目里经常用这个返回值判断更新是否成功——如果返回 0,说明没找到对应记录,可以给用户一个提示。
小技巧:@Update 默认更新所有字段。如果你只想更新部分字段,不要用 @Update,要用自定义 @Query。
4.3 @Delete — 删除数据
删除的逻辑和更新类似,也是根据主键匹配。注意:如果你传一个只设了主键、其他字段为 null 的对象,Room 也能删——它只看主键。
@Dao
interface UserDao {
@Delete
fun deleteUser(user: User): Int
@Delete
fun deleteUsers(users: List<User>)
}
返回值 int 同样是受影响行数。我记得有一次线上 bug,是因为传了一个主键不存在的对象,delete 返回 0,但调用方以为删成功了……所以检查返回值很重要。
4.4 @Query — 自定义查询
@Query 才是 Room 的杀手锏。你可以写任意 SQL,Room 会在编译时帮你检查语法。参数用冒号绑定。
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersOlderThan(minAge: Int): List<User>
@Query("SELECT * FROM users WHERE name LIKE '%' || :keyword || '%'")
fun searchUsers(keyword: String): List<User>
@Query("SELECT COUNT(*) FROM users")
fun getUserCount(): Int
@Query("UPDATE users SET name = :newName WHERE id = :userId")
fun updateNameById(userId: Long, newName: String): Int
}
支持返回的类型:
List<Entity>— 同步查询,注意不能在主线程调用(除非 allowMainThreadQueries)LiveData<List<Entity>>— 自动刷新,UI 层观察Flow<List<Entity>>— Kotlin 协程友好,支持响应式PagingSource— 配合 Paging 3 做分页- 基本类型(Int、Long、String 等)— 聚合查询
避坑指南:我曾经在 @Query 里写了字符串拼接 "..." + param,结果被 Room 编译报错。Room 要求参数只能用 :param 或 :arg1 绑定,不能用字符串拼接——这是为了防止 SQL 注入。
4.5 综合示例:UserDao
下面是一个完整的 DAO,涵盖了日常开发中 90% 的场景。
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>)
@Update
suspend fun update(user: User): Int
@Delete
suspend fun delete(user: User): Int
@Query("SELECT * FROM users ORDER BY id ASC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
@Query("DELETE FROM users")
suspend fun deleteAll()
@Query("SELECT COUNT(*) FROM users")
suspend fun getUserCount(): Int
}
注意:我习惯把所有 DAO 方法都写成 suspend 或返回 Flow,这样在协程里调用非常干净。如果你用 LiveData,注意 LiveData 的 observe 会在主线程回调。
4.6 知识结构速查表
| 注解 | 作用 | 常见返回值 | 注意事项 |
|---|---|---|---|
| @Insert | 插入一行或多行 | Long / List<Long> | 冲突策略默认 ABORT,建议显式指定 |
| @Update | 按主键更新 | int | 只更新已有主键的行,否则无影响 |
| @Delete | 按主键删除 | int | 检查返回值确认是否删除成功 |
| @Query | 自定义 SQL | 灵活(List, Flow, LiveData 等) | 参数用 :param 绑定;编译时检查 SQL |
4.7 DAO 操作核心流程图
下面这张图帮你理清增删改查分别对应哪个注解,以及各自的关键点。
4.8 小结
DAO 是 Room 的灵魂,四个注解覆盖了所有数据库操作。我个人建议新手先记住:
- 插入用 @Insert,别忘了冲突策略。
- 更新用 @Update,检查返回值确认。
- 删除用 @Delete,同样看返回值。
- 复杂查询用 @Query,Room 会在编译时帮你校验 SQL。
你想想看,只用四个注解就能搞定本地持久化,Room 把我们从繁琐的 SQLite 代码中解放出来了。多练习几次,你会发现这些注解其实比写原生 SQL 更安全、更清晰。
最后提醒:DAO 方法不要写成 suspend 加 LiveData 的混合模式——要么全部用协程,要么全部用 LiveData。我曾经混用过,结果出现线程切换的诡异 bug,调试了一下午……