MyBatis动态SQL与关联查询实战技巧

📅 2026/8/3 9:17:59
MyBatis动态SQL与关联查询实战技巧
1. MyBatis动态SQL与关联查询实战指南作为Java开发者最常用的ORM框架之一MyBatis在实际项目中的高级特性应用往往决定了数据访问层的优雅程度。今天我们就来深入探讨两个核心进阶特性动态SQL构建和关联关系映射这是处理复杂业务查询时的必备技能包。我在电商系统开发中曾遇到一个典型场景商品搜索需要支持多达12个可选筛选条件同时要返回商品详情及其关联的SKU列表。最初采用拼接SQL字符串的方式不仅难以维护还存在注入风险直到全面采用MyBatis动态SQL才彻底解决这个问题。而关联查询则帮助我们实现了API响应数据的一次性装配将原本需要5次数据库访问的流程优化到1次完成。本文将基于3.5.9版本通过真实案例演示如何利用if、choose、foreach等动态标签构建灵活查询resultMap实现一对一如订单-收货地址、一对多如商品-SKU关联映射嵌套查询与嵌套结果两种关联加载策略的取舍规避N1查询问题的实战技巧无论你是需要实现动态条件筛选、多表联合查询还是优化复杂对象组装性能这些方案都能直接套用到你的项目中。下面我们从一个电商系统的实际案例出发逐步拆解实现过程。2. 动态SQL深度解析2.1 基础标签应用实战假设我们需要实现一个商品分页查询接口支持以下动态条件可选商品名称模糊搜索多分类ID筛选价格区间过滤按销量或价格排序对应的Mapper XML配置如下select idsearchProducts resultMapProductResultMap SELECT * FROM products where if testname ! null and name ! AND name LIKE CONCAT(%,#{name},%) /if if testcategoryIds ! null and categoryIds.size() 0 AND category_id IN foreach collectioncategoryIds itemcid open( separator, close) #{cid} /foreach /if if testminPrice ! null AND price #{minPrice} /if if testmaxPrice ! null AND price #{maxPrice} /if /where choose when testsortBy sales ORDER BY sales_count DESC /when when testsortBy price ORDER BY price ${orderType} /when otherwise ORDER BY create_time DESC /otherwise /choose LIMIT #{offset}, #{pageSize} /select关键点说明where标签会自动处理AND前缀无需担心首条件前的AND导致语法错误foreach的collection参数支持List、Array、Map等多种集合类型${orderType}直接拼接SQL片段需注意注入风险而#{param}会预编译参数警告动态排序字段应使用choose硬编码可选值避免直接接收前端传参导致SQL注入2.2 高级动态SQL技巧场景一动态更新字段使用set标签实现只更新非空字段update idupdateProductSelective UPDATE products set if testname ! nullname#{name},/if if testprice ! nullprice#{price},/if if teststatus ! nullstatus#{status}/if /set WHERE id#{id} /update场景二批量插入优化利用foreach实现批量插入比单条插入效率提升10倍以上insert idbatchInsert INSERT INTO products(name, price) VALUES foreach collectionlist itemp separator, (#{p.name}, #{p.price}) /foreach /insert性能陷阱大批量数据如1万应分批次执行避免单个SQL过长MySQL的max_allowed_packet参数可能需要调整3. 关联查询实现方案3.1 一对一关联映射以订单与收货地址为例两种实现方式方案A嵌套结果映射推荐resultMap idOrderWithAddressMap typeOrder id propertyid columnorder_id/ result propertyamount columnorder_amount/ !-- 一对一关联 -- association propertyaddress javaTypeAddress id propertyid columnaddr_id/ result propertyprovince columnaddr_province/ result propertycity columnaddr_city/ /association /resultMap select idgetOrderWithAddress resultMapOrderWithAddressMap SELECT o.id as order_id, o.amount as order_amount, a.id as addr_id, a.province as addr_province, a.city as addr_city FROM orders o LEFT JOIN address a ON o.address_id a.id WHERE o.id #{orderId} /select方案B嵌套查询存在N1问题resultMap idOrderWithAddressMap2 typeOrder association propertyaddress columnaddress_id selectgetAddressById/ /resultMap select idgetAddressById resultTypeAddress SELECT * FROM address WHERE id #{id} /select经验优先使用JOIN方式的嵌套结果映射避免额外SQL查询。当关联对象结构复杂或使用频率低时才考虑嵌套查询。3.2 一对多关联处理商品与SKU的典型一对多关系实现resultMap idProductWithSkusMap typeProduct collection propertyskus ofTypeSku id propertyid columnsku_id/ result propertyspec columnsku_spec/ result propertyprice columnsku_price/ /collection /resultMap select idgetProductWithSkus resultMapProductWithSkusMap SELECT p.*, s.id as sku_id, s.spec as sku_spec, s.price as sku_price FROM products p LEFT JOIN skus s ON p.id s.product_id WHERE p.id #{productId} /select性能优化技巧使用LEFT JOIN而非INNER JOIN确保主对象总能返回对分页查询先获取主对象ID集合再批量获取关联对象大数据量时考虑使用Mapper注解配合Result实现延迟加载4. 实战问题排查手册4.1 动态SQL常见异常问题一参数为null时条件仍然生效!-- 错误示例 -- if testname ! !-- 当name为null时条件成立 -- AND name #{name} /if !-- 正确写法 -- if testname ! null and name ! 问题二集合判断逻辑错误!-- 错误示例 -- if testcategoryIds ! null !-- 空集合也会进入条件 -- AND category_id IN (...) /if !-- 正确写法 -- if testcategoryIds ! null and categoryIds.size() 04.2 关联查询性能陷阱N1查询问题复现查询获取N个主对象对每个主对象执行1次关联查询实际执行SQL次数 1(主查询) N(关联查询)解决方案使用JOIN嵌套结果映射一次性加载启用延迟加载需配置lazyLoadingEnabledtrue对分页场景先查主键再批量查关联4.3 MyBatis版本升级注意从3.5.x升级到3.7.x需关注默认值处理逻辑变化日志实现兼容性推荐使用SLF4J动态SQL解析器优化可能导致极端case行为差异5. 高级应用技巧5.1 动态表名与字段名使用bind标签实现安全拼接select iddynamicTableQuery bind nametableName valuecom.utils.TableHelpergetTableName(type)/ SELECT * FROM ${tableName} WHERE foreach collectioncolumns itemcol separator OR ${col} #{value} /foreach /select重要动态表名必须经过白名单校验避免SQL注入5.2 类型处理器高级应用自定义枚举类型处理public class StatusTypeHandler extends BaseTypeHandlerStatusEnum { Override public void setNonNullParameter(PreparedStatement ps, int i, StatusEnum parameter, JdbcType jdbcType) { ps.setInt(i, parameter.getCode()); } //...其他方法实现 }XML配置resultMap idorderResultMap typeOrder result columnstatus propertystatus typeHandlercom.handler.StatusTypeHandler/ /resultMap5.3 插件开发实战实现SQL执行时间监控插件Intercepts({ Signature(type Executor.class, methodquery, args{MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), Signature(type Executor.class, methodupdate, args{MappedStatement.class, Object.class}) }) public class PerformanceInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); long end System.currentTimeMillis(); System.out.println(SQL执行耗时: (end - start) ms); return result; } }在配置中注册plugins plugin interceptorcom.interceptor.PerformanceInterceptor/ /plugins6. 与MyBatis-Plus的协作策略虽然MyBatis-Plus提供了更便捷的CRUD操作但复杂场景仍需结合原生MyBatis动态SQL混合使用public interface ProductMapper extends BaseMapperProduct { Select(scriptSELECT * FROM products where.../where/script) ListProduct searchComplex(Param(param) SearchParam param); }关联查询优势互补简单CRUD使用MyBatis-Plus的Wrapper复杂关联查询使用原生resultMap分页插件整合// MyBatis-Plus分页配置 Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } }实际项目中我们通常将两者结合使用——MyBatis-Plus处理80%的基础操作原生MyBatis解决20%的复杂场景这种组合能最大化开发效率与灵活性。