第1章:Room核心概念与关系映射总览

Android开发者使用SQLite存储关系型数据时,很容易陷入模板代码与手动管理数据库版本的泥潭。Room是Google官方推出的持久化库,它在SQLite之上提供了一层抽象,通过编译时注解处理器自动生成实现,将数据库操作与Java/Kotlin对象自然绑定。

本章作为全课程的开篇,用一张全局结构图帮你建立Room组件与关系映射的完整认知,并逐一介绍Entity、DAO、Database、关系类型及编译时验证的核心角色。

Room核心组件与关系映射总览图 Room核心组件与关系映射总览 @Database 数据库持有者 & 版本管理 @Entity 表结构映射 (Kotlin/Java Bean) @Dao 数据访问对象 (SQL + 方法) @Relation 关系注解 + 嵌套POJO 一对一 (1:1) User ↔ UserProfile 一对多 (1:N) User ↔ Playlist 多对多 (M:N) Playlist ↔ Song 编译时验证机制 • 表名/列名冲突 • SQL语法检查 • 实体与DAO一致性 数据迁移 Migration + 版本号策略 避免Schema冲突 ⚠️ 关系查询必须使用 嵌套对象 + @Transaction
💡 核心要点:Room并非全自动ORM。它要求开发者显式定义实体、DAO和数据库对象,但通过编译时注解处理提供类型安全、SQL验证和关系映射的自动化代码生成。映射关系(1:1、1:N、M:N)需配合@Relation与嵌套POJO实现。

1.1 实体(Entity)——表结构定义的强制契约

每个 @Entity 注解的类对应一个SQLite表。Room约定:每个实体必须有一个主键(@PrimaryKey),且构造函数参数必须映射所有列(除非使用@Ignore)。Kotlin用户推荐使用data class以减少模板代码。

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    @ColumnInfo(name = "user_name")
    val userName: String,
    val age: Int? = null
)

字段默认使用Kotlin属性名作为列名,可通过@ColumnInfo重命名。@Ignore用于忽略不在表中的字段。@Embedded可扁平化嵌套对象。

1.2 数据访问对象(DAO)——类型安全的SQL入口

DAO必须是接口或抽象类,使用@Dao注解。Room在编译时生成实现。每个操作必须用@Insert、@Update、@Delete或@Query标记。

@Dao
interface UserDao {
    @Insert
    suspend fun insert(user: User): Long

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: Long): User?

    @Query("SELECT * FROM users WHERE user_name LIKE :query")
    suspend fun searchUsers(query: String): List<User>

    @Delete
    suspend fun delete(user: User)
}
⚠️ 编译时验证生效点:Room会在编译期校验@Query中的SQL语法、参数占位符:param与函数参数是否匹配、返回值类型能否映射到实体。如果SQL写错表名或列名,编译直接报错。

1.3 数据库(Database)——中央持有者与版本管控

@Database注解类必须继承RoomDatabase,并定义获取所有DAO的抽象方法。数据库版本号用于迁移管理。

@Database(
    entities = [User::class, UserProfile::class, Playlist::class, Song::class],
    version = 1,
    exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun playlistDao(): PlaylistDao
}
属性说明示例
entities列出所有实体类[User::class, Song::class]
version数据库版本号,升级后需提供Migration1 → 2
exportSchema导出Schema到JSON文件用于测试true

1.4 关系映射类型(一对一、一对多、多对多)

Room不直接使用外键约束,而是通过嵌套POJO + @Relation注解来构建关系查询结果。列表对比三种关系的典型场景:

关系类型实体示例@Relation使用方式典型查询
一对一 (1:1)User ↔ UserProfile@Relation(parentColumn = "id", entityColumn = "userId")每个用户有其唯一档案
一对多 (1:N)User ↔ Playlist@Relation(parentColumn = "id", entityColumn = "ownerId") + List<Playlist>一个用户拥有多个播放列表
多对多 (M:N)Playlist ↔ Song使用关联表(PlaylistSongRef) + @Relation两个方向播放列表与歌曲多对多交叉
📌 关键区别:一对一/一对多只需在POJO中声明@Relation属性,多对多必须在@Relation中设置associateBy参数指定交叉表实体。

1.5 编译时验证机制——防错于未然

Room的注解处理器在编译时完成以下检查:

  1. 实体合法性:每个实体必须有一个且仅有一个@PrimaryKey;不能有多个自增主键。
  2. SQL语法:@Query内的SQL语句会被解析验证,不通过则编译失败。
  3. 参数与返回值:占位符:name与函数参数名称匹配;返回值类型必须能从查询结果构建(列名匹配字段或通过@ColumnInfo映射)。
  4. 关系注解一致性:parentColumn和entityColumn必须存在,且类型兼容。
  5. 索引与唯一约束:通过@Entity(indices = [])可在编译时声明索引,避免运行时full table scan。

得益于这些验证,很多数据库错误能在IDE敲代码阶段就被发现,大幅减少运行期崩溃。

⚠️ 常见陷阱:
• 实体中声明了@PrimaryKey(autoGenerate = true)但Kotlin属性用Int而非Long —— Room会报警告,自增主键推荐使用Long。
• DAO中@Query使用SELECT *但返回类型是单个对象的List —— 类型不匹配编译错误。
• 关系查询忘加@Transaction注解 —— 运行时报错:Cannot access database on the main thread(非直接原因,但关系查询一般需事务)。

本章建立了Room的全局图景。接下来的章节会深入每个关系映射类型的完整实现,手把手带你编写实体、DAO、关系POJO、@Transaction查询以及数据库构建代码。