当前位置: 代码网 > it编程>编程语言>Java > MyBatis框架与SQL层面关键技术示例详解

MyBatis框架与SQL层面关键技术示例详解

2026年09月10日 Java 我要评论
一、mybatis 动态 sql动态 sql 是 mybatis 最核心的能力:根据入参动态拼接 sql,而不是写死。用于"筛选条件有就拼、没有就不拼"的场景。1.1<if&

一、mybatis 动态 sql

动态 sql 是 mybatis 最核心的能力:根据入参动态拼接 sql,而不是写死。用于"筛选条件有就拼、没有就不拼"的场景。

1.1<if>— 条件拼接

入参有值才拼这段 sql。这是"可选筛选条件"的基础:

<if test="paramdto.sourcesystem != null and paramdto.sourcesystem != ''">
  and o.source_system = #{paramdto.sourcesystem}
</if>
  • test 里写 ognl 表达式判断
  • 字符串要判 != null and != ''(空字符串也不拼)
  • 集合要判 != null and .size() > 0

1.2<foreach>— 集合展开为 in

把 list 入参展开成 in (?, ?, ?)

<if test="paramdto.warehousecodes != null and paramdto.warehousecodes.size() > 0">
  and o.warehouse_code in
  <foreach collection="paramdto.warehousecodes" item="code"
           open="(" separator="," close=")">
    #[code]
  </foreach>
</if>
  • collection = 集合参数名,item = 每个元素的别名
  • open/close/separator = 拼成 (a, b, c)

1.3<where>— 智能处理 where 和 and

自动加 where,并去掉紧跟其后的多余 and/or

<where>
  <if test="...">and a = #{a}</if>
  <if test="...">and b = #{b}</if>
</where>
<!-- 若第一个条件命中,<where> 会把开头的 and 去掉,避免 where and a=... 语法错误 -->

1.4<sql>+<include>— sql 片段复用(关键)

把公共的 from/join、where 条件抽成片段,多个查询复用。比如列表接口和合计接口就靠它保证筛选口径完全一致

<sql id="fromjoins">
  from main_table o
  left join member_base mb on mb.member_id = o.member_id
</sql>
<sql id="whereconditions">
  <where>
    <if test="...">and ...</if>
  </where>
</sql>
<!-- 列表查询 -->
<select id="listpager" resulttype="...">
  select o.*, mb.name <include refid="fromjoins"/> <include refid="whereconditions"/>
  order by o.create_time desc
</select>
<!-- 合计查询:复用同一片段,改口径不会漏改 -->
<select id="listsum" resulttype="...">
  select sum(o.qty), sum(o.amount) <include refid="fromjoins"/> <include refid="whereconditions"/>
</select>

价值:改一处 where 条件,列表和合计同时生效,避免两处口径漂移。

二、#{}vs${}(安全关键)

写法机制用途风险
#{param}预编译占位符(preparedstatement 的 ?),值作为参数传入绝大多数场景,尤其是外部入参无 sql 注入,安全
${param}字符串直接拼接动态表名/列名/排序方向等无法用占位符的场景有 sql 注入风险,禁止拼外部输入
where code = #[code]          <!-- ✅ 预编译,安全 -->
order by ${sortcolumn}        <!-- ⚠️ 拼接,sortcolumn 必须白名单校验 -->

原则:能用 #{} 就绝不用 ${},外部入参一律 #{}

三、pagehelper 物理分页

3.1 工作原理

pagehelper 是分页插件(mybatis interceptor),拦截紧跟其后执行的第一条查询,自动:

  1. 生成并执行一条 select count(*) ...(算总数)
  2. 在原 sql 尾部拼 limit ?, ?(取当页数据)
pagehelper.startpage(pagenum, pagesize);      // 声明下一条查询要分页
list<xxx> list = mapper.listxxx(param);       // 这条被拦截,自动分页
pageinfo<xxx> page = new pageinfo<>(list);     // 拿到 total/pages/list

3.2 物理分页 vs 逻辑分页

  • 物理分页:sql 层 limit 只取当页数据(pagehelper 用的这种),数据量大也不吃内存。
  • 逻辑分页:查全部再在内存 sublist(mybatis 的 rowbounds 默认行为),大数据会 oom,禁用。

3.3 count 语句的坑

pagehelper 生成 count 时会"智能优化"(去 order by、简化 select),但当 select 里有标量子查询、sql 太复杂无法安全解析时,它会退化select count(*) from (原完整sql) tmp,把子查询也带进 count 执行——导致 count 也慢。这是"sql 直接跑快、但接口慢"的隐藏原因。解决:把大表访问移出主查询(见第六节)。

四、子查询的三种形态与性能

4.1 派生表(from 子查询)— 慎用

from (select code, max(time) from big_table group by code) t

group by/聚合的派生表会被物化(先全表算完存临时表),大表场景是性能灾难。

4.2 相关标量子查询(select 子查询)— 带值走索引

select a.id,
       (select max(b.time) from big_table b where b.code = a.code) as lasttime
from main a

带着外层 a.code 的具体值去查,能走索引。但分页 count 会连它一起执行,仍有隐患。

4.3 条件子查询(where 子查询)

where id in (select id from other where ...)

五、join 与行膨胀

多表 join 时,若一个主记录关联到多条子记录,结果集会出现重复行(行膨胀),导致:

  • 列表同一条数据显示多行
  • 分页 total 虚高、翻页错乱

场景:一个 a单可能有多条调拨快照、一个调拨单号可能有多条出入库单。解决办法是用子查询把"多条"收敛成"一条":

-- 取每个a单最新的一条快照(用 max(id) 兜底,比 max(create_time) 更稳,防同时间戳膨胀)
left join (
  select t.pledge_order_id, t.transfer_order_code
  from snapshot t
  inner join (select pledge_order_id, max(id) as max_id from snapshot group by pledge_order_id) m
    on m.pledge_order_id = t.pledge_order_id and m.max_id = t.id
) ts on ts.pledge_order_id = o.id

六、大表取"最后一次"的优化演进

主表小、要展示关联大表的"最后一次时间",且要分页。三种写法:

  • 派生表全表聚合(最差):对千万级大表全表 group by 物化 → 十几秒
  • 相关标量子查询(改善):带 code 走索引,但分页 count 仍带子查询
  • 应用层二次批量查询
  • (最优):
    • 主查询和 count 完全不碰大表(只查主表)
    • 分页拿到当前页的 code 集合
    • 用一条 in 批量查大表的 max(time),走索引
    • java 层按 code 回填
list<row> list = mapper.listpager(param);              // 主查询不含大表
list<string> codes = list.stream().map(row::getcode)
    .filter(objects::nonnull).distinct().collect(tolist());
map<string, date> timemap = tomap(mapper.listlasttimebycodes(codes)); // 一条 in 查
list.foreach(r -> r.setlasttime(timemap.get(r.getcode())));

核心思想:把对大表的访问次数与结果集行数解耦

七、索引相关

7.1 索引失效的常见原因

  • 对索引列用函数where date(create_time) = '2026-09-01' 用不上 create_time 索引 → 改成范围查询
  • 左闭右开时间区间
  • (这次用的):把"某月"换算成
  • [月初, 下月初)
  • ,避免对时间列套函数:
  • and inbound_time >= '2026-09-01 00:00:00'
    and inbound_time <  '2026-10-01 00:00:00'
  • 隐式类型转换、like '%xx' 前导通配、or 连接非索引列等

7.2 覆盖索引

(code, time) 复合索引,select max(time) where code=? 可直接从索引取值不回表,进一步加速。

八、explain 执行计划分析

排查慢 sql 的核心工具,重点看:

关注点
typeall=全表扫描(差);ref/range/eq_ref=走索引(好)
key实际用了哪个索引,null=没走索引
rows预估扫描行数,越大越慢
extrausing temporary=用临时表(派生表物化信号);using filesort=额外排序;using index=覆盖索引(好)
select_typederived=派生表;subquery=子查询
table<derivedn>=派生表的物化结果

看到 derived + big_tabletype=all + 大 rows + using temporary,基本锁定派生表物化拖慢。

九、结果映射

  • resulttype:查询列的别名(as xxx)直接映射到 dto 的同名驼峰字段(user_name as username)。
  • 展示串拼接放 java 层:像 (编码)名称 这种拼接,放 service 层而非 sql —— 便于单测、格式调整,也避免 sql 里 concat/left 出边界问题。sql 只返回编码和名称原值。

十、一句话总结

mybatis 层靠动态 sql(<if>/<foreach>/<sql>+<include>)**灵活拼条件并复用片段保证列表与合计口径一致;用 #{} 预编译防注入;用 **pagehelper 物理分页**(注意 count 会带上 select 子查询的坑)。sql 层要警惕**派生表物化和 join 行膨胀两大性能陷阱,用子查询收敛(max id)**去重、用**应用层批量二次查询把大表访问与结果行数解耦、用左闭右开区间保索引不失效,最终靠 explain 验证执行计划。

到此这篇关于mybatis框架与 sql 层面关键技术详解的文章就介绍到这了,更多相关mybatis动态sql内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com