为什么用 Room

Room 是 Google 官方的 SQLite 抽象层:编译期校验 SQL、返回类型安全、和 LiveData / Flow 天然集成。相比手写 SQLiteOpenHelper,Room 把「表」变成 Kotlin 类,把「查询」变成接口方法。

写错一个表名或列名,Room 在编译期就告诉你,而不是运行到一半才崩。

依赖只需引入 room-runtime、room-compiler 和 room-ktx。数据库一旦建好,增删改查都收敛到 DAO 里,业务层再也不用碰 SQL 字符串。

工程上还建议把数据库实例做成单例,用 Application 上下文初始化,避免 Activity 重建导致连接反复开关;涉及加密需求时,可以配合 SQLCipher 的 Room 版本,在打开数据库时传入密钥,让落盘数据默认处于加密状态。

实体与类型转换器

实体类用 @Entity 标注,主键用 @PrimaryKey,字段即列。遇到列表、枚举这类 SQLite 存不了的类型,用 TypeConverter 转换。

@Entity(tableName = "notes")
data class Note(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String,
  val tags: List<String>,
  val updatedAt: Long
)

class Converters {
  @TypeConverter
  fun fromTags(tags: List<String>): String = tags.joinToString(",")
  @TypeConverter
  fun toTags(value: String): List<String> = value.split(",")
}

转换器注册到 @Database 的 converters 数组里。字段增减要配迁移,后面专门讲。实体字段别放计算属性,那是 DAO 查询该做的事。

DAO 与查询

DAO 是数据访问的唯一入口:@Insert、@Update、@Delete 自动生成实现,复杂查询用 @Query 写 SQL。Room 会在编译期把 SQL 和表结构对一遍。

@Dao
interface NoteDao {
  @Insert
  suspend fun insert(note: Note): Long

  @Query("SELECT * FROM notes ORDER BY updatedAt DESC")
  fun observeAll(): Flow<List<Note>>

  @Query("SELECT * FROM notes WHERE id = :id")
  suspend fun findById(id: Long): Note?

  @Query("DELETE FROM notes WHERE id = :id")
  suspend fun deleteById(id: Long)
}

返回值用 Flow 而不是普通 List,数据一变 UI 自动更新。写 @Query 时 SQL 按表名列名写,别按 Kotlin 属性名,二者默认相同、改名后就分开了。

关系与关联查询

一对一、一对多关系用 @Relation 表达。父实体加一个 List 子实体字段,Room 自动做关联查询;涉及多条语句用 @Transaction 保证一致性。

data class NoteWithTags(
  @Embedded val note: Note,
  @Relation(parentColumn = "id", entityColumn = "noteId")
  val tags: List<Tag>
)

@Transaction
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
suspend fun getNotesWithTags(): List<NoteWithTags>

@Embedded 把另一张表或值对象铺进结果,@Relation 负责拉取子表。关系别建太深,两层以内最清晰,超过两层拆成多次查询反而更好读。

关于关系的使用,还有几个工程上的细节值得展开。首先,@Relation 查询在底层会生成两条 SQL,先用父表查询再取子表数据,所以在列表页一次性加载大量带标签的数据时,要考虑数据量是否可控;数据量较大时,建议改成分页加载或单独查询标签表,再在内存里按主键组装。其次,@Embedded 适合把经纬度、货币金额这类值对象直接平铺进实体,避免为它们单独建表,但要注意字段名前缀冲突,必要时用 prefix 参数区分。第三,关系的删除要显式处理,Room 不会自动级联删除,删除父记录后子表会留下孤儿数据,最好在 DAO 的删除方法里配合事务一起清理。把这些细节想清楚,关系模型才能在真实业务里稳定运行。