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
DatabaseRoom 数据库实例,连接 DAO 与底层 SQLiteAppDatabase.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 省略
}
提示:蓝牙设备的 MAC 地址具有唯一性,适合作为主键。若使用自增主键,则每次插入重复地址的设备时会造成冗余,建议使用 REPLACE 策略或在插入前检查。

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;
    }
}
注意:如果 Entity 的结构在后续版本中发生变化(例如新增字段),必须增加 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 之间的协作关系,以及它们在蓝牙数据持久化流程中的位置。

Room 核心概念交互图 - 蓝牙多设备场景 Room 核心概念在蓝牙数据管理中的协作 AppDatabase BluetoothDeviceEntity @Entity → 表 bluetooth_devices ConnectionRecordEntity @Entity → 表 connection_records DeviceDao @Dao → CRUD 方法 ConnectionDao @Dao → 查询记录 TypeConverter List ↔ JSON 字符串 DeviceWithRecords (Relation) @Embedded + @Relation → 1:N 关联查询 提供 DAO 实例 操作 操作 转换 组合 说明:Database 管理多个 DAO;DAO 操作 Entity 表;TypeConverter 辅助数据类型转换;Relation 通过 @Embedded + @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()(仅调试)。