JdbcTemplate vs MyBatis-Plus:复杂查询场景的选择指南

在数据归集系统的 TreeSelectorServiceImpl 中,大部分查询使用 MyBatis-Plus,但有 6 处使用了原生 JdbcTemplate。本文分析这 6 种场景,总结何时应该”退回”原生 SQL,以及如何在两者之间做出合理选择。

🎧 文章导读

🎵 背景音乐

前言

MyBatis-Plus(以下简称 MBP)是 Spring Boot 项目中最常用的 ORM 框架之一。它的 QueryWrapperLambdaQueryWrapper 让单表查询变得非常简洁,但在某些复杂场景下,原生 JdbcTemplate 反而是更好的选择。

本文以数据归集系统 TreeSelectorServiceImpl 中的 6 处 JdbcTemplate 使用为例,分析三类核心原因,并给出优化方向。

场景清单

先看全景——6 处 JdbcTemplate 使用的原因和涉及的 SQL 特性:

# 方法 原因 涉及的 SQL 特性
1 batchElementValue(无时间范围) GROUP BY 取最新记录 MAX(TM) + GROUP BY + 子查询
2 getEquipmentSymbolsWithNames 跨表 LEFT JOIN equipmentr LEFT JOIN elementb
3 latestElementValue GROUP BY + 可选 LEFT JOIN MAX(TM) + GROUP BY + 关联查询
4 getElementData 动态表名 + 动态列名 表名/列名来自枚举运行时拼接
5 batchElementData 同上,批量版 动态表名 + 动态列名
6 getSymbolChnMap 全量查中文名映射 被 1、3 调用的辅助方法

三类核心原因

三种场景对比图
图1:聚合查询、跨表关联、动态表名三种场景对比

原因一:聚合 + 分组

MBP 的 QueryWrapper 不支持 GROUP BY + 聚合函数 + 子查询的组合。

典型场景:取每个设备的最新一条记录。这需要子查询先按设备分组取最大时间戳,再关联回主表取完整记录:

1
2
3
4
5
6
7
-- 典型模式:取每个设备的最新一条记录
SELECT t1.* FROM table t1
INNER JOIN (
SELECT EQID, MAX(TM) AS MAX_TM
FROM table WHERE EQID IN (?, ?, ?)
GROUP BY EQID
) t2 ON t1.EQID = t2.EQID AND t1.TM = t2.MAX_TM

这种”分组取极值”的模式在数据查询中非常常见,但 MBP 的 QueryWrapper 无法表达子查询和聚合函数的组合。

原因二:跨表关联查询

MBP 的 QueryWrapper 只支持单表查询,无法表达 LEFT JOIN

1
2
3
4
-- 典型模式:关联要素中文名
SELECT e.SYMBOL, eb.CHNM
FROM bs_sw_cy_dh_equipmentr e
LEFT JOIN bs_sw_cy_dh_elementb eb ON e.SYMBOL = eb.SYMBOL

当需要从多张表中组合数据时,MBP 的单表限制就成了瓶颈。

原因三:动态表名/列名

MBP 的实体类映射是静态的,表名和列名在编译期确定。但有些场景需要根据运行时参数动态选择表名和列名:

1
2
3
4
// 动态拼接表名和列名
String tableName = tableTypeConfig.getTableName(); // 如 PPTN → ST_PPTN_R
String columns = String.join(",", tableTypeConfig.getColumns());
String sql = "SELECT " + columns + " FROM " + tableName + " WHERE ...";

这种”配置驱动的查询”在水文监测系统中很常见——不同类型的监测数据存储在不同的表中,表名由配置枚举决定。

分页实现

latestElementValue 接口基础上新增分页能力,沿用 batchElementData 已有的手动分页模式:

1
2
3
4
5
6
7
8
// 两个分支都加了 COUNT + LIMIT OFFSET
// 无时间范围分支
String countSql = "SELECT COUNT(*) FROM (" + dataSql + ") cnt";
String pagedSql = dataSql + " LIMIT ? OFFSET ?";

// 有时间范围分支
String countSql = "SELECT COUNT(*) FROM table WHERE ...";
String pagedSql = dataSql + " LIMIT ? OFFSET ?";

返回格式:

1
2
3
4
5
6
{
"total": 54,
"pageNum": 1,
"pageSize": 10,
"list": [...]
}

优化方向

当 JdbcTemplate 的字符串拼接变得难以维护时,可以考虑以下方案:

方向 说明 适用场景
MyBatis XML Mapper 用 XML 定义复杂 SQL,替代 JdbcTemplate 字符串拼接 场景 1、2、3
动态表名插件 MBP 的 DynamicTableNameInnerInterceptor 场景 4、5
@Select 注解 在 Mapper 接口上直接写 SQL 简单的 JOIN 查询

当前方案功能正确,优化优先级不高。若后续维护成本增加(如 SQL 拼接出错难排查),可考虑迁移到 XML Mapper。

选择指南

选择指南决策树
图2:何时用 MyBatis-Plus、何时用 JdbcTemplate 的决策树

总结一下何时用 MBP、何时用 JdbcTemplate:

用 MyBatis-Plus 的场景

  • 单表 CRUD
  • 简单条件查询(等值、范围、LIKE)
  • 分页查询(内置分页插件)
  • 需要类型安全的 Lambda 查询

用 JdbcTemplate 的场景

  • 聚合查询 + 子查询(GROUP BY + 聚合函数)
  • 跨表 JOIN(LEFT JOIN、INNER JOIN)
  • 动态表名/列名(运行时确定)
  • 复杂 SQL 需要精确控制

折中方案

  • MyBatis XML Mapper:保留 MBP 的便利性,同时支持复杂 SQL
  • @Select 注解:在 Mapper 接口上直接写 SQL,适合简单的 JOIN

经验总结

  1. 不要为了”统一技术栈”而强行用 MBP——当 QueryWrapper 无法表达你的查询时,JdbcTemplate 是更务实的选择
  2. 字符串拼接 SQL 要注意注入风险——虽然 JdbcTemplate 的参数化查询已经防注入,但表名和列名的动态拼接仍需谨慎
  3. 分页查询要先 COUNT 再查数据——避免在 Java 层做分页,浪费数据库资源
  4. 复杂 SQL 要有注释——尤其是子查询和 JOIN 的逻辑,方便后续维护

本文涉及代码在 TreeSelectorServiceImpl.java 中,共 6 处 JdbcTemplate 使用。