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 — 单条插入时返回 rowId
  • List<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 操作核心流程图

下面这张图帮你理清增删改查分别对应哪个注解,以及各自的关键点。

DAO 操作核心流程图 DAO 接口 @Insert 返回值:Long / List<Long> 冲突策略:ABORT/REPLACE/IGNORE @Update 返回值:int (受影响行数) 按主键匹配,不更新则返回0 @Delete 返回值:int (受影响行数) 只根据主键删除 @Query 返回值:List/Flow/LiveData等 参数用 :param 绑定 提示:所有 DAO 方法都支持 suspend 或协程 Flow,避免在主线程阻塞

4.8 小结

DAO 是 Room 的灵魂,四个注解覆盖了所有数据库操作。我个人建议新手先记住:

  • 插入用 @Insert,别忘了冲突策略。
  • 更新用 @Update,检查返回值确认。
  • 删除用 @Delete,同样看返回值。
  • 复杂查询用 @Query,Room 会在编译时帮你校验 SQL。

你想想看,只用四个注解就能搞定本地持久化,Room 把我们从繁琐的 SQLite 代码中解放出来了。多练习几次,你会发现这些注解其实比写原生 SQL 更安全、更清晰。

最后提醒:DAO 方法不要写成 suspend 加 LiveData 的混合模式——要么全部用协程,要么全部用 LiveData。我曾经混用过,结果出现线程切换的诡异 bug,调试了一下午……