第4章 两种方案的数据模型对比:表结构定义、关系映射

4.1 核心概念

数据模型是应用系统在持久层组织数据的“骨架”,它决定了数据如何存储、如何约束以及如何关联。在蓝牙场景中,数据模型需要承载设备信息、扫描记录、连接日志等实体,因此表结构定义与关系映射是两个最核心的设计维度:

  • 表结构定义:规定每个实体对应哪张表、有哪些字段、字段的类型与约束(主键、非空、唯一等)。
  • 关系映射:描述表与表之间的关联方式(一对一、一对多、多对多),以及在查询时如何将这些关联还原为完整的业务对象。

下表从整体视角对比 SQLite 与 Room 在数据模型上的设计哲学差异:

对比维度SQLite 方案Room 方案
表结构定义方式手写 CREATE TABLE SQL 语句使用 @Entity 注解标记数据类
字段映射手动编写列名、类型、约束通过注解属性自动映射字段
主键声明PRIMARY KEY 写在 DDL 中@PrimaryKey 注解标注字段
关系表达外键约束 + JOIN 查询@Relation 注解 + 嵌套对象
关系还原手动编写 JOIN 逻辑,逐行组装对象自动将关联表数据填充到注解标记的字段中
编译期校验无校验,运行时 DDL 报错才暴露编译期检查表结构、关系注解的合法性

4.2 SQLite 的数据模型:手写 DDL 与手动关系

在纯 SQLite 方案中,开发者需要直接编写 DDL(数据定义语言) 来声明表结构。蓝牙场景中一个典型的设备表定义如下:

CREATE TABLE ble_device (
    id            INTEGER   PRIMARY KEY AUTOINCREMENT,
    name          TEXT       NOT NULL,
    mac_address   TEXT       UNIQUE NOT NULL,
    first_seen_at INTEGER    NOT NULL
);

CREATE TABLE scan_record (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    device_id   INTEGER  NOT NULL,
    rssi        INTEGER  NOT NULL,
    scanned_at  INTEGER  NOT NULL,
    FOREIGN KEY (device_id) REFERENCES ble_device(id)
);

关系映射完全依赖外键约束和手写 JOIN。当需要查询某个设备的所有扫描记录时,开发者必须自己拼接 SQL、遍历游标并手动组装对象图。这种方式灵活但极易出错,尤其在嵌套关联超过两层时,代码量会急剧膨胀。

4.3 Room 的数据模型:注解驱动与自动关系

Room 将表结构定义与关系映射提升到注解声明的层面,开发者只需在 Kotlin / Java 数据类上添加注解,Room 在编译期自动生成对应的 DDL 与 CRUD 代码。同样蓝牙场景的实体用 Room 表示为:

@Entity(tableName = "ble_device")
data class BleDevice(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    val name: String,
    @ColumnInfo(name = "mac_address")
    val macAddress: String,
    @ColumnInfo(name = "first_seen_at")
    val firstSeenAt: Long
)

@Entity(
    tableName = "scan_record",
    foreignKeys = [ForeignKey(
        entity = BleDevice::class,
        parentColumns = ["id"],
        childColumns = ["device_id"],
        onDelete = ForeignKey.CASCADE
    )]
)
data class ScanRecord(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "device_id")
    val deviceId: Int,
    val rssi: Int,
    @ColumnInfo(name = "scanned_at")
    val scannedAt: Long
)

关系映射通过 @Relation 注解在“嵌入类”中声明。Room 会自动根据注解中的 parentColumn 和 entityColumn 生成关联查询,一次调用即可拿到包含子对象的完整实体:

data class DeviceWithRecords(
    @Embedded val device: BleDevice,
    @Relation(
        parentColumn = "id",
        entityColumn = "device_id"
    )
    val records: List<ScanRecord>
)

Room 在编译期会校验所有表名、列名、外键与 @Relation 的配置是否一致,提前发现不匹配错误,避免运行时崩溃。

4.4 原理图解:数据模型对比

下面的 SVG 直观展示了 SQLite 与 Room 在“表结构定义”和“关系映射”两个层次上的核心差异与最终汇合点:

数据模型对比:SQLite vs Room SQLite 方案 表结构定义 手写 CREATE TABLE 语句 手动定义字段、类型、约束 运行时 DDL 无编译期校验 关系映射 外键约束 + 手写 JOIN 手动遍历游标组装对象 嵌套关联时代码量膨胀 Room 方案 表结构定义 @Entity 注解标记数据类 自动生成 CREATE TABLE 代码 编译期校验表名、字段、约束 关系映射 @Relation 注解声明关联 自动生成 JOIN 并填充嵌套对象 编译期校验 parentColumn 等配置 声明式 vs 手写 自动映射 vs 手动 JOIN 目标:数据持久化 💡 两种方案最终都存储为 SQLite 数据文件

从上图可以清晰看到:SQLite 在表结构和关系映射两个层次都依赖手工编码,而 Room 通过注解声明 + 编译期代码生成自动完成同样的工作,最终都落地为 SQLite 数据库文件。

4.5 类比理解:手写清单 vs 智能表单

可以把 SQLite 的数据模型比作手写购物清单:你在空白纸上自己画表格、写品名、算总价,完全自由但每次都要重复劳动,写错了也只能涂改。Room 则像是智能表单 App:你只需要在预设字段里填入内容,App 自动生成整齐的表格、计算总和,并在提交前检查有没有漏填或填错。两者的最终结果都是一份清单(数据库文件),但创建和维护过程的效率、可靠性完全不同。

4.6 常见误区

  • 误区一:Room 会改变底层数据库引擎。Room 仍然是基于 SQLite 的持久化库,最终生成的数据库文件格式与纯 SQLite 完全兼容,并不是“替换”了 SQLite。
  • 误区二:纯 SQLite 无法表达关系。SQLite 完全支持外键约束和 JOIN 查询,只是需要开发者手工编写所有 SQL 逻辑,Room 只是把这一过程自动化了。
  • 误区三:@Relation 会自动创建外键。@Relation 只影响查询时的对象组装,不会自动在子表上建立外键约束;外键仍需在 @Entity(foreignKeys = [...]) 中显式声明。
  • 误区四:表结构定义只涉及字段名和类型。约束(主键、非空、唯一、默认值、外键级联策略)同样是表结构定义的关键部分,两种方案都需要妥善处理。

4.7 练习题

  1. 选择题:在 Room 中,用于指示“某个字段是另一张表的外键”的配置位于哪个注解中?
    A. @PrimaryKey  B. @Relation  C. @Entity(foreignKeys = [...])  D. @Database
  2. 简答题:在蓝牙扫描场景中,存在 BleDevice 和 ScanRecord 两个实体,一个设备对应多条记录。请分别描述在 SQLite 方案和 Room 方案中,如何完成“查询某台设备及其所有扫描记录”这一需求的数据模型映射过程。

4.8 应用边界

选择纯 SQLite 的情况:项目只有 1–2 张简单表,且表之间无关联或关联非常固定(如仅做 KV 存储);团队对 SQL 非常熟悉且希望完全控制 DDL;或者项目无法引入 Room 依赖(如某些 C/C++ 嵌入式环境)。

选择 Room 的情况:实体数量超过 3 个,且存在复杂的嵌套关联、级联删除或多表联合查询;希望利用编译期校验减少运行时错误;需要与 Kotlin 协程 / Flow / RxJava 深度集成以简化异步数据观察。在蓝牙场景中,由于通常涉及设备表、扫描记录表、连接日志表等多实体关联,Room 的注解式关系映射能显著降低代码维护成本。

要点回顾:表结构定义决定数据如何存储,关系映射决定数据如何关联。SQLite 要求开发者手工完成全部 DDL 和 JOIN 逻辑;Room 通过 @Entity 和 @Relation 注解将大部分工作自动化,并增加编译期校验。两种方案最终都面向同一个底层——SQLite 数据库文件,但 Room 在开发效率、可维护性和正确性保障上更适用于多实体关联的蓝牙场景。