1. MyBatis-Plus 复杂查询实战指南
在数据持久层开发中,我们经常会遇到需要编写复杂SQL的场景。MyBatis-Plus作为MyBatis的增强工具,虽然提供了强大的Wrapper条件构造器,但在处理多表关联、子查询等复杂业务时,仍然需要回归到原生SQL的灵活性上。本文将分享我在实际项目中积累的MyBatis-Plus自定义SQL实践心得。
2. 核心功能解析
2.1 条件构造器与自定义SQL的配合
MyBatis-Plus的Wrapper体系(如QueryWrapper、LambdaQueryWrapper)非常适合简单的单表CRUD操作。但当遇到以下场景时,就需要考虑自定义SQL:
- 多表关联查询(特别是需要复杂连接条件时)
- 包含聚合函数、分组统计的查询
- 需要数据库特定函数或语法的场景
- 存在复杂子查询结构的业务逻辑
java复制// 典型的多表关联示例
@Select("SELECT u.*, d.dept_name FROM user u " +
"LEFT JOIN department d ON u.dept_id = d.id " +
"${ew.customSqlSegment}")
Page<UserDTO> selectUserWithDept(Page<UserDTO> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
关键技巧:通过
${ew.customSqlSegment}可以无缝集成Wrapper的条件,实现动态WHERE子句
2.2 XML映射文件的最佳实践
对于特别复杂的SQL,建议使用XML方式维护:
xml复制<!-- 包含子查询的复杂示例 -->
<select id="selectComplexData" resultType="com.example.vo.ComplexVO">
SELECT
t1.*,
(SELECT COUNT(*) FROM sub_table WHERE main_id = t1.id) AS sub_count
FROM main_table t1
<where>
<if test="ew != null and ew.sqlSegment != null and ew.sqlSegment != ''">
AND ${ew.sqlSegment}
</if>
</where>
</select>
性能优化点:
- 避免在循环中拼接SQL片段
- 复杂查询考虑添加
@InterceptorIgnore跳过租户等拦截器 - 大数据量查询使用分页插件
3. 高级查询场景实现
3.1 动态表名查询方案
在SAAS多租户系统中,动态表名是常见需求。MyBatis-Plus提供了多种实现方式:
java复制// 方案1:使用SQL注入器
public class DynamicTableNameInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new SelectByDynamicTable());
return methodList;
}
}
// 方案2:基于参数传递
@Select("SELECT * FROM ${tableName} ${ew.customSqlSegment}")
List<Map<String, Object>> selectByDynamicTable(@Param("tableName") String tableName,
@Param(Constants.WRAPPER) Wrapper<?> wrapper);
3.2 存储过程调用示例
对于需要调用存储过程的场景:
java复制@Select("{CALL complex_procedure(#{param1,mode=IN,jdbcType=VARCHAR},"
+ "#{param2,mode=OUT,jdbcType=INTEGER})}")
@Options(statementType = StatementType.CALLABLE)
void callProcedure(Map<String, Object> params);
参数处理要点:
- 输入参数使用
mode=IN - 输出参数使用
mode=OUT - 必须指定
jdbcType - 返回值通过Map键值获取
4. 性能优化与问题排查
4.1 常见性能陷阱
-
N+1查询问题:
- 现象:主查询获取列表后,循环执行子查询
- 解决方案:使用
@Results注解或XML中的<collection>定义关联加载
-
大结果集内存溢出:
- 使用流式查询:
java复制@Select("SELECT * FROM large_table") @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE) void streamLargeData(ResultHandler<LargeData> handler); -
索引失效场景:
- 避免在WHERE条件中对字段使用函数
- 注意LIKE查询的通配符位置
- 联合索引注意最左匹配原则
4.2 典型异常处理
问题1:动态SQL注入风险
- 现象:使用
${}导致SQL注入 - 解决方案:
- 尽量使用
#{}预处理 - 必须使用
${}时严格校验输入 - 使用SafeSql工具类过滤
- 尽量使用
问题2:分页查询结果不正确
- 检查点:
- 是否配置了分页插件
- Page参数是否作为第一个参数
- 是否存在嵌套结果映射
问题3:TypeHandler不生效
- 排查步骤:
- 检查是否正确定义了TypeHandler
- 是否在@TableField或XML中指定
- 是否注册到MyBatis配置
5. 复杂查询设计模式
5.1 查询构建器模式
对于可配置的复杂查询,可以采用构建器模式:
java复制public class QueryBuilder {
private List<String> selectFields = new ArrayList<>();
private List<String> joinClauses = new ArrayList<>();
public QueryBuilder select(String... fields) {
selectFields.addAll(Arrays.asList(fields));
return this;
}
public String build() {
return "SELECT " + StringUtils.join(selectFields, ",") + " " +
"FROM main_table " +
StringUtils.join(joinClauses, " ");
}
}
5.2 装饰器模式应用
对基础查询进行功能增强:
java复制public interface QueryDecorator {
String decorate(String originSql);
}
public class OrderDecorator implements QueryDecorator {
private String orderBy;
public OrderDecorator(String orderBy) {
this.orderBy = orderBy;
}
@Override
public String decorate(String originSql) {
return originSql + " ORDER BY " + orderBy;
}
}
6. 监控与调优实践
6.1 SQL执行监控
集成P6Spy等工具监控实际执行的SQL:
properties复制# application.properties配置示例
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/db
6.2 慢查询分析
- 启用MyBatis-Plus性能分析插件:
java复制@Bean
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor interceptor = new PerformanceInterceptor();
interceptor.setMaxTime(1000); // 超过1秒记录警告
interceptor.setFormat(true);
return interceptor;
}
- 结合Explain分析执行计划:
java复制@Select("EXPLAIN ${sql}")
List<Map<String, String>> explain(@Param("sql") String sql);
7. 扩展功能集成
7.1 多数据源支持
在Spring Boot中集成多数据源:
java复制@Configuration
@MapperScan(basePackages = "com.example.mapper.db1",
sqlSessionTemplateRef = "db1SqlSessionTemplate")
public class Db1DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory db1SqlSessionFactory(
@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/*.xml"));
return factory.getObject();
}
}
7.2 分布式事务处理
使用Seata处理跨数据源事务:
java复制@GlobalTransactional
public void crossDatabaseOperation() {
db1Mapper.update(...);
db2Mapper.insert(...);
// 异常会自动回滚
}
在实际项目开发中,合理运用MyBatis-Plus的自定义SQL能力,可以兼顾开发效率和灵活性。我的经验是:80%的常规CRUD使用Wrapper,20%的复杂场景回归XML/注解SQL,两者配合才能发挥最大效益。对于特别复杂的查询,建议拆分为多个步骤处理,避免单个SQL过于庞大难以维护。