1. MyBatis动态SQL与关联查询实战指南
作为Java开发者最常用的ORM框架之一,MyBatis在实际项目中的高级特性应用往往决定了数据访问层的优雅程度。今天我们就来深入探讨两个核心进阶特性:动态SQL构建和关联关系映射,这是处理复杂业务查询时的必备技能包。
我在电商系统开发中曾遇到一个典型场景:商品搜索需要支持多达12个可选筛选条件,同时要返回商品详情及其关联的SKU列表。最初采用拼接SQL字符串的方式,不仅难以维护还存在注入风险,直到全面采用MyBatis动态SQL才彻底解决这个问题。而关联查询则帮助我们实现了API响应数据的一次性装配,将原本需要5次数据库访问的流程优化到1次完成。
本文将基于3.5.9版本,通过真实案例演示如何利用:
<if>、<choose>、<foreach>等动态标签构建灵活查询resultMap实现一对一(如订单-收货地址)、一对多(如商品-SKU)关联映射- 嵌套查询与嵌套结果两种关联加载策略的取舍
- 规避N+1查询问题的实战技巧
无论你是需要实现动态条件筛选、多表联合查询,还是优化复杂对象组装性能,这些方案都能直接套用到你的项目中。下面我们从一个电商系统的实际案例出发,逐步拆解实现过程。
2. 动态SQL深度解析
2.1 基础标签应用实战
假设我们需要实现一个商品分页查询接口,支持以下动态条件:
- 可选商品名称模糊搜索
- 多分类ID筛选
- 价格区间过滤
- 按销量或价格排序
对应的Mapper XML配置如下:
<select id="searchProducts" resultMap="ProductResultMap"> SELECT * FROM products <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%',#{name},'%') </if> <if test="categoryIds != null and categoryIds.size() > 0"> AND category_id IN <foreach collection="categoryIds" item="cid" open="(" separator="," close=")"> #{cid} </foreach> </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> </where> <choose> <when test="sortBy == 'sales'"> ORDER BY sales_count DESC </when> <when test="sortBy == '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 id="updateProductSelective"> UPDATE products <set> <if test="name != null">name=#{name},</if> <if test="price != null">price=#{price},</if> <if test="status != null">status=#{status}</if> </set> WHERE id=#{id} </update>场景二:批量插入优化利用<foreach>实现批量插入,比单条插入效率提升10倍以上:
<insert id="batchInsert"> INSERT INTO products(name, price) VALUES <foreach collection="list" item="p" separator=","> (#{p.name}, #{p.price}) </foreach> </insert>性能陷阱:
- 大批量数据(如1万+)应分批次执行,避免单个SQL过长
- MySQL的max_allowed_packet参数可能需要调整
3. 关联查询实现方案
3.1 一对一关联映射
以订单与收货地址为例,两种实现方式:
方案A:嵌套结果映射(推荐)
<resultMap id="OrderWithAddressMap" type="Order"> <id property="id" column="order_id"/> <result property="amount" column="order_amount"/> <!-- 一对一关联 --> <association property="address" javaType="Address"> <id property="id" column="addr_id"/> <result property="province" column="addr_province"/> <result property="city" column="addr_city"/> </association> </resultMap> <select id="getOrderWithAddress" resultMap="OrderWithAddressMap"> 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:嵌套查询(存在N+1问题)
<resultMap id="OrderWithAddressMap2" type="Order"> <association property="address" column="address_id" select="getAddressById"/> </resultMap> <select id="getAddressById" resultType="Address"> SELECT * FROM address WHERE id = #{id} </select>经验:优先使用JOIN方式的嵌套结果映射,避免额外SQL查询。当关联对象结构复杂或使用频率低时,才考虑嵌套查询。
3.2 一对多关联处理
商品与SKU的典型一对多关系实现:
<resultMap id="ProductWithSkusMap" type="Product"> <collection property="skus" ofType="Sku"> <id property="id" column="sku_id"/> <result property="spec" column="sku_spec"/> <result property="price" column="sku_price"/> </collection> </resultMap> <select id="getProductWithSkus" resultMap="ProductWithSkusMap"> 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 test="name != ''"> <!-- 当name为null时条件成立 --> AND name = #{name} </if> <!-- 正确写法 --> <if test="name != null and name != ''">问题二:集合判断逻辑错误
<!-- 错误示例 --> <if test="categoryIds != null"> <!-- 空集合也会进入条件 --> AND category_id IN (...) </if> <!-- 正确写法 --> <if test="categoryIds != null and categoryIds.size() > 0">4.2 关联查询性能陷阱
N+1查询问题复现:
- 查询获取N个主对象
- 对每个主对象执行1次关联查询
- 实际执行SQL次数 = 1(主查询) + N(关联查询)
解决方案:
- 使用JOIN+嵌套结果映射一次性加载
- 启用延迟加载(需配置
lazyLoadingEnabled=true) - 对分页场景先查主键再批量查关联
4.3 MyBatis版本升级注意
从3.5.x升级到3.7.x需关注:
- 默认值处理逻辑变化
- 日志实现兼容性(推荐使用SLF4J)
- 动态SQL解析器优化可能导致极端case行为差异
5. 高级应用技巧
5.1 动态表名与字段名
使用<bind>标签实现安全拼接:
<select id="dynamicTableQuery"> <bind name="tableName" value="@com.utils.TableHelper@getTableName(type)"/> SELECT * FROM ${tableName} WHERE <foreach collection="columns" item="col" separator=" OR "> ${col} = #{value} </foreach> </select>重要:动态表名必须经过白名单校验,避免SQL注入
5.2 类型处理器高级应用
自定义枚举类型处理:
public class StatusTypeHandler extends BaseTypeHandler<StatusEnum> { @Override public void setNonNullParameter(PreparedStatement ps, int i, StatusEnum parameter, JdbcType jdbcType) { ps.setInt(i, parameter.getCode()); } //...其他方法实现 }XML配置:
<resultMap id="orderResultMap" type="Order"> <result column="status" property="status" typeHandler="com.handler.StatusTypeHandler"/> </resultMap>5.3 插件开发实战
实现SQL执行时间监控插件:
@Intercepts({ @Signature(type= Executor.class, method="query", args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type= Executor.class, method="update", 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 interceptor="com.interceptor.PerformanceInterceptor"/> </plugins>6. 与MyBatis-Plus的协作策略
虽然MyBatis-Plus提供了更便捷的CRUD操作,但复杂场景仍需结合原生MyBatis:
- 动态SQL混合使用
public interface ProductMapper extends BaseMapper<Product> { @Select("<script>SELECT * FROM products <where>...</where></script>") List<Product> 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%的复杂场景,这种组合能最大化开发效率与灵活性。