3. Room 数据库核心概念回顾:Entity、DAO、Database、类型转换器与关系映射
在构建蓝牙多设备数据管理应用时,Room 作为 Android 官方 ORM 层,提供了对 SQLite 的抽象与编译时校验。本章系统梳理 Room 的五大核心构件:Entity、DAO、Database、类型转换器(TypeConverter)以及关系映射(Relation)。这些概念是后续章节实现蓝牙设备发现记录、连接状态持久化、多设备数据同步的基础。
3.1 核心概念总览
下表将每个概念的核心职责、在蓝牙场景中的典型用法以及关键注解进行对比,便于快速建立整体认知。
| 构件 | 职责 | 蓝牙场景示例 | 关键注解 |
|---|---|---|---|
| Entity | 定义数据表结构,字段映射到列 | BluetoothDeviceEntity(设备地址、名称、RSSI) | @Entity、@PrimaryKey、@ColumnInfo |
| DAO | 提供数据访问操作(增删改查) | DeviceDao.insert() 保存扫描结果 | @Dao、@Insert、@Query、@Update、@Delete |
| Database | Room 数据库实例,连接 DAO 与底层 SQLite | AppDatabase.instance 供全局使用 | @Database(entities=[…], version=…) |
| TypeConverter | 将自定义类型与数据库支持类型互相转换 | 将 List<String>(已配对设备列表)转为 JSON 字符串 | @TypeConverter 注解静态方法 |
| Relation | 定义 Entity 之间的关联查询(1:1、1:N、M:N) | 设备实体与其连接历史表的一对多关系 | @Relation、@Embedded、@Transaction + @Query |
3.2 Entity:数据表模型
Entity 是 Room 的最小数据单元。每一个 Entity 类使用 @Entity 注解标记,Room 会为它生成对应的 SQLite 表。蓝牙多设备场景中,至少需要定义设备基本信息表与连接记录表。
@Entity(tableName = "bluetooth_devices")
public class BluetoothDeviceEntity {
@PrimaryKey
@NonNull
@ColumnInfo(name = "device_address")
private String address; // 蓝牙 MAC 地址(唯一)
@ColumnInfo(name = "device_name")
private String name; // 设备名称
@ColumnInfo(name = "rssi")
private int rssi; // 信号强度
@ColumnInfo(name = "last_seen_time")
private long lastSeenTime; // 最后发现时间(毫秒)
// 构造方法、Getter、Setter 省略
}
3.3 DAO:数据访问对象
DAO 接口定义了所有对 Entity 的 CRUD 操作。Room 在编译时自动生成 DAO 的实现。蓝牙多设备管理下,常见的 DAO 方法包括:
- 插入单条扫描记录(带冲突策略)
- 查询所有已发现设备(按时间降序)
- 更新设备名称或 RSSI
- 删除超过一周的历史记录
@Dao
public interface DeviceDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertDevice(BluetoothDeviceEntity device);
@Query("SELECT * FROM bluetooth_devices ORDER BY last_seen_time DESC")
LiveData<List<BluetoothDeviceEntity>> getAllDevices();
@Query("UPDATE bluetooth_devices SET rssi = :rssi WHERE device_address = :address")
void updateRssi(String address, int rssi);
@Query("DELETE FROM bluetooth_devices WHERE last_seen_time < :threshold")
void deleteOldDevices(long threshold);
}
3.4 Database:数据库实例与结构变更
Database 类是 Room 的入口,必须声明 @Database 注解,列出所有 Entity 类。同时它需要暴露一个抽象方法来获取 DAO。单例模式可以避免重复创建实例造成开销。
@Database(entities = {BluetoothDeviceEntity.class, ConnectionRecordEntity.class},
version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
public abstract DeviceDao deviceDao();
public abstract ConnectionDao connectionDao();
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"ble_device_db"
).build();
}
}
}
return INSTANCE;
}
}
version 并实现 Migration,否则应用在升级时会 Crash(fallbackToDestructiveMigration 仅限开发环境使用)。
3.5 类型转换器(TypeConverter)
Room 基本类型支持:int、long、String、byte[] 等。但蓝牙数据中经常需要存储 List<String>(如已配对设备地址列表)或 Map<String, Integer>(特征值与句柄映射)。此时需要 TypeConverter 序列化/反序列化。
public class Converters {
@TypeConverter
public static String fromDeviceAddressList(List<String> addresses) {
return addresses == null ? null : TextUtils.join(",", addresses);
}
@TypeConverter
public static List<String> toDeviceAddressList(String data) {
if (data == null || data.isEmpty()) return new ArrayList<>();
return Arrays.asList(data.split(","));
}
}
// 在 Database 类上声明 @TypeConverters
@Database(entities = {..., ConverterTestEntity.class}, version = 1)
@TypeConverters(Converters.class)
public abstract class AppDatabase extends RoomDatabase { ... }
3.6 关系映射:从实体到关联
Room 推荐通过嵌套查询实现关系,而不是直接建立外键约束。蓝牙多设备场景中典型关系:一个设备拥有多条连接记录(1:N)。使用 @Relation 与 @Embedded 组合来获取包含关联列表的 POJO。
定义连接记录 Entity:
@Entity(tableName = "connection_records")
public class ConnectionRecordEntity {
@PrimaryKey(autoGenerate = true)
private int id;
@ColumnInfo(name = "device_address")
private String deviceAddress; // 外键逻辑关联
private String eventType; // CONNECTED / DISCONNECTED
private long timestamp;
// getter/setter 省略
}
定义关联查询的 POJO:
public class DeviceWithRecords {
@Embedded
public BluetoothDeviceEntity device;
@Relation(
parentColumn = "device_address",
entityColumn = "device_address"
)
public List<ConnectionRecordEntity> records;
}
@Dao
public interface DeviceDao {
@Transaction
@Query("SELECT * FROM bluetooth_devices WHERE device_address = :address")
public DeviceWithRecords getDeviceWithRecords(String address);
}
@Transaction 注解,确保关系查询在同一个事务中执行,避免 N+1 问题。parentColumn 与 entityColumn 必须严格对应表列名。
3.7 核心概念交互图
下面这张 SVG 结构图展示了 Entity、DAO、Database、TypeConverter、Relation 之间的协作关系,以及它们在蓝牙数据持久化流程中的位置。
3.8 常见错误与规避
- 未声明 Entity 导致编译错误:在
@Database的entities数组中未包含新定义的 Entity。 - TypeConverter 方法未声明为 static:Room 要求类型的转换方法必须是静态的,否则运行时会抛出
IllegalStateException。 - 关系查询缺少 @Transaction:
@Relation自动查询多个表,如果没有@Transaction可能会触发多次查询且结果不一致。 - 主键冲突未处理:插入相同地址的设备时会抛出异常,应使用
@Insert(onConflict = OnConflictStrategy.REPLACE)。
3.9 性能与稳定性要点
- DAO 方法尽量返回 LiveData 或 Flow:蓝牙扫描频次高,使用响应式数据可自动更新 UI,避免手动轮询。
- 使用 Batch Insert 减少事务开销:扫描到多个设备时,调用
@Insert传入 List 比逐条插入效率高 10 倍以上。 - 索引优化:对
last_seen_time和device_address添加索引可加速排序与查询。 - 避免在 UI 线程操作数据库:Room 默认不允许在主线程调用 DAO 方法,必须使用异步方式或开启
allowMainThreadQueries()(仅调试)。