1. MyBatis动态SQL核心价值解析
作为Java生态中最主流的ORM框架之一,MyBatis的动态SQL功能一直是其区别于Hibernate等全自动框架的核心竞争力。我在实际项目中发现,90%以上的复杂查询场景都需要借助动态SQL来实现条件分支、循环拼接等灵活操作。与静态SQL相比,动态SQL能够根据运行时参数智能调整最终执行的SQL语句,这在多条件查询、批量操作等场景中尤为实用。
动态SQL的本质是通过XML或注解方式定义的SQL模板,MyBatis会在运行时根据参数值动态生成最终SQL。这种机制完美解决了传统JDBC中需要手动拼接SQL字符串的痛点——既避免了代码中大量if-else判断导致的维护困难,又从根本上杜绝了SQL注入风险(当然,前提是正确使用#{}占位符)。
2. 高频动态SQL标签全解
2.1 条件分支标签组
xml复制<select id="searchProducts" resultType="Product">
SELECT * FROM products
WHERE status = 1
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<!-- 更多条件... -->
</select>
重要经验:test属性使用OGNL表达式,判断null时建议用!=而非not equals,后者在某些版本可能存在解析问题。
xml复制<select id="findOrders" resultType="Order">
SELECT * FROM orders
WHERE
<choose>
<when test="status == 'paid'">
payment_status = 1 AND shipping_status = 0
</when>
<when test="status == 'shipped'">
shipping_status = 1 AND receipt_status = 0
</when>
<otherwise>
status = #{defaultStatus}
</otherwise>
</choose>
</select>
2.2 循环处理标签
xml复制<update id="batchUpdateWaybills">
UPDATE waybills
SET update_time = NOW()
WHERE id IN
<foreach item="id" collection="ids"
open="(" separator="," close=")">
#{id}
</foreach>
</update>
踩坑记录:
- collection属性对应Mapper接口中的参数名,数组用array,列表用list
- 大数据量批量操作时,建议分批处理(每批500-1000条)
- Oracle的IN语句有1000个参数限制,需要做特殊处理
2.3 动态字段与表名
xml复制<sql id="tenantFilter">
FROM ${tenantSchema}.orders
</sql>
<select id="getOrder" resultType="Order">
SELECT *
<include refid="tenantFilter"/>
WHERE id = #{id}
</select>
安全警示:表名/字段名动态化必须使用${},这会带来SQL注入风险。解决方案:
- 在业务层严格校验参数
- 使用白名单机制校验表名字段名
- 重要系统建议采用逻辑隔离而非物理隔离
3. 实战模板与避坑指南
3.1 万能查询模板
这个模板在我参与的CRM系统中服务了80%的查询需求:
xml复制<select id="selectByCondition" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM
${tableName}
<where>
<if test="condition.id != null">
AND id = #{condition.id}
</if>
<if test="condition.name != null and condition.name != ''">
AND name LIKE CONCAT('%',#{condition.name},'%')
</if>
<if test="condition.statusList != null and condition.statusList.size() > 0">
AND status IN
<foreach collection="condition.statusList" item="status"
open="(" separator="," close=")">
#{status}
</foreach>
</if>
<if test="condition.startTime != null">
AND create_time >= #{condition.startTime}
</if>
<if test="condition.endTime != null">
AND create_time <= #{condition.endTime}
</if>
</where>
ORDER BY
<choose>
<when test="orderBy != null and orderBy != ''">
${orderBy}
</when>
<otherwise>
id DESC
</otherwise>
</choose>
LIMIT #{offset}, #{pageSize}
</select>
3.2 批量插入优化方案
MyBatis官方推荐的批量插入方式在实际生产中存在性能瓶颈。经过压测对比,我总结出三种优化方案:
方案一:JDBC批处理模式
xml复制<insert id="batchInsert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users(name,email) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name},#{item.email})
</foreach>
</insert>
适用场景:MySQL且数据量<5000条
方案二:ExecutorType.BATCH模式
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(User user : userList) {
mapper.insert(user);
}
session.commit();
}
性能提升3-5倍,但要注意手动清除一级缓存
方案三:多值INSERT+rewriteBatchedStatements
在jdbc url添加参数:
code复制jdbc:mysql://...?rewriteBatchedStatements=true&allowMultiQueries=true
实测10万条数据插入从120秒降至8秒
3.3 动态SQL性能陷阱
-
过度动态化问题:一个包含15个
的查询,当所有条件都生效时,SQL解析耗时可能达到简单SQL的3倍。建议: - 将高频必选条件移出动态部分
- 对固定条件组合使用
片段 - 考虑使用MyBatis-Plus的QueryWrapper
-
${}滥用风险:某金融项目曾因直接使用${accountId}导致SQL注入漏洞。安全规范:
- 能用#{}绝不用${}
- 必须用${}时要做严格正则校验
- 敏感操作增加操作日志审计
-
N+1查询问题:在
内执行查询会导致笛卡尔积爆炸。解决方案:
xml复制<!-- 错误示范 -->
<select id="getUserWithOrders" resultMap="userWithOrders">
SELECT * FROM users WHERE id = #{id};
SELECT * FROM orders WHERE user_id = #{id};
</select>
<!-- 正确做法 -->
<select id="getUserWithOrders" resultMap="userWithOrders">
SELECT u.*, o.*
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
4. 高级技巧与源码解析
4.1 动态SQL生成原理
通过调试MyBatis源码(版本3.5.6),动态SQL的生成流程如下:
-
解析阶段:XMLConfigBuilder加载映射文件,由XMLScriptBuilder将各动态标签转换为对应的SqlNode实现类:
- IfSqlNode对应
- ChooseSqlNode对应
- ForEachSqlNode对应
- IfSqlNode对应
-
执行阶段:DynamicSqlSource.getBoundSql()方法中:
java复制// 创建动态上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 递归解析所有SqlNode
rootSqlNode.apply(context);
// 生成最终SQL
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
- 参数处理:通过MetaObject智能获取参数值,支持:
- 简单类型直接取值
- Map按key获取
- POJO对象通过getter方法获取
- 集合类型支持index/key访问
4.2 自定义动态标签
通过实现LanguageDriver接口,我们可以扩展自定义标签。例如实现一个
java复制public class SafeUpdateLanguageDriver extends XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration,
String script,
Class<?> parameterType) {
// 在原始SQL前添加安全字段
script = "UPDATE " + script + " SET update_by = #{loginUserId}, update_time = NOW()";
return super.createSqlSource(configuration, script, parameterType);
}
}
注册方式:
xml复制<update id="updateProduct" lang="com.example.SafeUpdateLanguageDriver">
products SET name=#{name} WHERE id=#{id}
</update>
4.3 与MyBatis-Plus的协同
MyBatis-Plus的Wrapper虽然强大,但在复杂动态SQL场景下,原生MyBatis仍有不可替代的优势。最佳实践是:
- 简单条件查询使用QueryWrapper:
java复制QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "张")
.between("age", 20, 30)
.orderByDesc("create_time");
- 复杂动态逻辑使用XML:
xml复制<select id="selectComplex" resultType="User">
SELECT * FROM user
<where>
<if test="wrapper != null">
${ew.customSqlSegment}
</if>
<if test="extraCondition != null">
AND ${extraCondition}
</if>
</where>
</select>
- 混合使用时注意:
- Wrapper的sqlSegment要用${}引入
- 避免同时使用Wrapper和
判断相同条件 - 分页参数建议统一通过Page对象传递
5. 生产环境问题排查手册
5.1 常见异常处理
问题一:Parameter 'xxx' not found
-
可能原因:
- 参数名与#{}占位符名称不匹配
- 使用${}时参数为null
- 嵌套参数未正确使用"."访问
-
解决方案:
xml复制<!-- 错误示例 -->
<if test="user.name != null">...</if>
<!-- 正确示例 -->
<if test="user != null and user.name != null">...</if>
问题二:Invalid bound statement (not found)
- 检查点:
- Mapper接口与XML的namespace是否一致
- 方法名与SQL ID是否匹配
- 是否被其他拦截器修改了SQL
问题三:Foreach循环集合为null
- 防御性编码:
xml复制<if test="list != null and list.size() > 0">
<foreach collection="list" item="item">...</foreach>
</if>
5.2 性能优化检查清单
-
SQL日志分析:
- 开启mybatis.configuration.log-impl=STDOUT_LOGGING
- 检查生成的SQL是否符合预期
- 关注重复执行的相同SQL
-
缓存策略优化:
- 一级缓存:SqlSession级别,建议在批量操作后手动clearCache()
- 二级缓存:namespace级别,注意实现Serializable接口
-
连接池配置:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
5.3 监控与调优
- 慢SQL监控:
java复制@Intercepts(@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class}))
public class SlowQueryInterceptor implements Interceptor {
private static final long SLOW_THRESHOLD = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long duration = System.currentTimeMillis() - start;
if(duration > SLOW_THRESHOLD) {
StatementHandler handler = (StatementHandler)invocation.getTarget();
log.warn("Slow SQL detected: {} - {}ms",
handler.getBoundSql().getSql(),
duration);
}
return result;
}
}
- 动态SQL预热:
在应用启动时预加载常用SQL:
java复制@PostConstruct
public void warmUpMyBatis() {
List<Class<?>> mappers = Arrays.asList(UserMapper.class, OrderMapper.class);
mappers.forEach(mapper -> {
sqlSession.getMapper(mapper).selectByExample(null);
});
}
- 线程池隔离:
对重要业务SQL使用独立线程池:
java复制@Bean(name = "importantSqlExecutor")
public Executor importantSqlExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("important-sql-");
return executor;
}
@Async("importantSqlExecutor")
public CompletableFuture<List<User>> findImportantUsers() {
return CompletableFuture.completedFuture(userMapper.selectImportant());
}
