跨数据源查询:VD 设备列表的改造实践

前端视频巡查任务配置页需要展示”有视频设备的测站列表 + 每个测站下的设备”。但设备表在主库,测站表在 slave 库,无法一条 SQL JOIN。本文记录了使用 @DS 注解 + Java 内存组装的跨数据源查询方案。

🎧 文章导读

🎵 背景音乐

背景

视频巡查功能需要展示测站及其下的视频设备。数据分布在两个数据库中:

数据库 用途
bs_sw_cy_dh_equipmentb(设备表) 主库 (gdsw) 存储设备信息
st_stbprp_b(测站表) slave 库 (water) 存储测站信息

由于两张表在不同的数据库中,无法使用 SQL JOIN,需要在 Java 层组装数据。

方案设计

使用 dynamic-datasource@DS 注解切换数据源,两次查询 + Java 内存组装:

1
2
3
1. 查设备表(主库)→ 获取设备列表
2. 收集 stcd → 批量查测站名称(slave 库)
3. Java 内存回填 stnm

核心实现

查询流程图
图1:跨数据源查询的三步流程

Step 1: 新建测站实体和 Mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StStbprpB.java - 测站实体
@Data
@TableName("st_stbprp_b")
public class StStbprpB {
@TableId("STCD")
private String stcd;
private String stnm;
}

// StStbprpBMapper.java - 切换到 slave 数据源
@Mapper
@DS("slave")
public interface StStbprpBMapper extends BaseMapper<StStbprpB> {
}

关键点:@DS("slave") 注解让这个 Mapper 的所有查询都走 slave 数据源。

Step 2: 修改设备 DTO

VdEquipment.java 中添加非数据库字段 stnm

1
2
@TableField(exist = false)
private String stnm;

@TableField(exist = false) 告诉 MBP 这个字段不对应数据库列,避免映射错误。

Step 3: 实现跨数据源查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Resource
private StStbprpBMapper stStbprpBMapper;

@Override
public IPage<VdEquipment> getVdDevices(int current, int size, String stcd) {
// 1. 查设备表(主库)
Page<VdEquipment> page = new Page<>(current, size);
LambdaQueryWrapper<VdEquipment> wrapper = new LambdaQueryWrapper<>();
wrapper.like(VdEquipment::getSymbol, "vd");
wrapper.orderByAsc(VdEquipment::getSort);
if (stcd != null && !stcd.isEmpty()) {
wrapper.eq(VdEquipment::getStcd, stcd);
}
IPage<VdEquipment> result = this.page(page, wrapper);

if (result.getRecords().isEmpty()) {
return result;
}

// 2. 收集 stcd,批量查测站名称(slave 库)
List<String> stcds = result.getRecords().stream()
.map(VdEquipment::getStcd).distinct().collect(Collectors.toList());
List<StStbprpB> stations = stStbprpBMapper.selectList(
new QueryWrapper<StStbprpB>().in("STCD", stcds).select("STCD", "STNM"));
Map<String, String> stnmMap = stations.stream()
.collect(Collectors.toMap(StStbprpB::getStcd, StStbprpB::getStnm, (a, b) -> a));

// 3. 回填 stnm
result.getRecords().forEach(e ->
e.setStnm(stnmMap.getOrDefault(e.getStcd(), null)));

return result;
}

查询优化

几个细节值得注意:

  1. 批量查询而非逐条查询——收集所有 stcd 后一次 IN 查询,避免 N+1 问题
  2. 只查需要的字段——select("STCD", "STNM") 减少数据传输量
  3. 去重——distinct() 避免重复查询同一个测站
  4. 容错处理——getOrDefault(e.getStcd(), null) 处理测站不存在的情况

涉及文件汇总

操作 文件 说明
新建 StStbprpB.java 测站实体
新建 StStbprpBMapper.java @DS(“slave”) 切换数据源
修改 VdEquipment.java 新增 stnm 字段
修改 VdDeviceServiceImpl.java 跨数据源查询逻辑

Controller 和 Service 接口不需要改,返回类型 IPage<VdEquipment> 不变。

跨数据源查询的几种方案对比

方案对比图
图2:四种跨数据源查询方案的优缺点对比

方案 优点 缺点 适用场景
@DS 注解 + Java 组装 简单直观,MBP 原生支持 需要两次查询 数据量不大,关联逻辑简单
聚合视图/物化视图 一条 SQL 完成 需要 DBA 配置,实时性差 查询频繁,数据变化少
数据同步(ETL) 查询性能最好 架构复杂,有延迟 数据量大,分析型查询
分布式查询引擎 统一 SQL 引入新组件 多数据源场景多

本场景选择 @DS 注解方案,因为:

  • 数据量不大(设备表几千条)
  • 关联逻辑简单(只需回填测站名称)
  • 不想引入额外组件

经验总结

  1. @DS 注解要加在 Mapper 上——加在 Service 上不生效,因为 MBP 的动态数据源是基于 Mapper 代理的
  2. 批量查询避免 N+1——先收集所有 ID,一次 IN 查询,而不是逐条查询
  3. 只查需要的字段——select() 减少数据传输量,尤其是大表
  4. 容错处理——关联数据可能不存在,用 getOrDefault 处理

本文涉及代码在 digital-twin 模块中,已在 master-b_sp_20251216-delDec 分支提交。