1. MyBatis-Plus 自定义 SQL 与复杂查询实战指南
在持久层框架选型中,MyBatis-Plus 因其对 MyBatis 的增强特性而广受欢迎。但当业务需求超出常规 CRUD 范围时,开发者常面临两个核心问题:如何优雅地实现自定义 SQL 语句?怎样高效处理多表关联、动态条件等复杂查询场景?本文将基于真实项目经验,拆解 MyBatis-Plus 应对复杂查询的完整解决方案。
1.1 为什么需要自定义 SQL?
尽管 MyBatis-Plus 的 Wrapper 条件构造器能覆盖 80% 的查询场景,但在以下情况仍需手动编写 SQL:
- 多表联合查询时关联字段优化
- 需要特定数据库函数(如 Oracle 的 LISTAGG)
- 复杂统计报表中的窗口函数应用
- 子查询嵌套等特殊语法需求
提示:MyBatis-Plus 3.5.0+ 版本对自定义 SQL 的支持有显著增强,特别是 XML 与注解方式的混用场景。
2. 自定义 SQL 实现方案对比
2.1 XML 映射文件方式
传统 MyBatis 的 XML 配置依然是最稳定的方案。新建YourMapper.xml文件:
<!-- 示例:动态条件分页查询 --> <select id="selectComplexPage" resultType="com.example.vo.UserDeptVO"> SELECT u.*, d.dept_name FROM user u LEFT JOIN department d ON u.dept_id = d.id <where> <if test="param.deptName != null and param.deptName != ''"> AND d.dept_name LIKE CONCAT('%', #{param.deptName}, '%') </if> <if test="param.status != null"> AND u.status = #{param.status} </if> </where> ORDER BY u.create_time DESC </select>配套的 Mapper 接口需定义对应方法:
@Mapper public interface UserMapper extends BaseMapper<User> { IPage<UserDeptVO> selectComplexPage(IPage<?> page, @Param("param") QueryParam param); }优势:
- 支持完整的 MyBatis 动态 SQL 语法
- 便于管理复杂 SQL 语句
- 与 MyBatis-Plus 的分页插件天然兼容
2.2 注解方式实现
对于简单SQL,可使用@Select等注解:
@Select("SELECT * FROM user WHERE id = #{id} AND deleted = 0") User selectActiveUser(@Param("id") Long id);注解方式限制:
- 不支持动态 SQL 标签(如
<if>) - 复杂 SQL 可读性差
- 字符串拼接容易引发 SQL 注入风险
关键技巧:使用
@SelectProvider实现动态 SQL:
public String buildComplexQuery(Map<String, Object> params) { return new SQL() {{ SELECT("*"); FROM("user"); if (params.get("name") != null) { WHERE("name = #{name}"); } }}.toString(); }3. 复杂查询实战方案
3.1 多表关联查询优化
场景:查询用户信息及所属部门名称
方案一:结果映射(ResultMap)
<resultMap id="userDeptMap" type="com.example.vo.UserDeptVO"> <id property="id" column="user_id"/> <result property="username" column="username"/> <!-- 部门字段映射 --> <association property="dept" javaType="Dept"> <id property="id" column="dept_id"/> <result property="name" column="dept_name"/> </association> </resultMap>方案二:DTO 投影(推荐)
@Select("SELECT u.id, u.name, d.name as deptName " + "FROM user u LEFT JOIN department d ON u.dept_id = d.id") List<UserDTO> listUsersWithDept();性能对比:
| 方案 | 执行效率 | 内存占用 | 适用场景 |
|---|---|---|---|
| ResultMap | 中等 | 较高 | 需要完整实体 |
| DTO投影 | 高 | 低 | 只需部分字段 |
3.2 动态条件构造
结合 Wrapper 和自定义 SQL 实现动态查询:
// 构建查询条件 LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(StringUtils.isNotBlank(name), User::getName, name) .gt(age != null, User::getAge, age); // XML中引用Wrapper <select id="selectByWrapper" resultType="User"> SELECT * FROM user ${ew.customSqlSegment} </select>注意事项:
customSqlSegment会自动带上 WHERE 关键字- 参数前缀固定为
ew.paramNameValuePairs - 复杂条件建议配合
<script>标签使用
4. 高级特性应用
4.1 批量操作优化
原生批量插入:
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO user(name, age) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.age}) </foreach> </insert>与 MP 批量方法对比:
| 方法 | 数据量 | 事务控制 | 性能 |
|---|---|---|---|
| saveBatch | <1000 | 自动分片 | 中等 |
| XML批量 | >1000 | 需手动 | 高 |
| 游标查询 | 大数据 | 流式处理 | 最高 |
4.2 子查询处理
示例:查询部门人数大于平均值的部门
@Select("SELECT * FROM department WHERE id IN " + "(SELECT dept_id FROM user GROUP BY dept_id " + "HAVING COUNT(*) > (SELECT AVG(cnt) FROM " + "(SELECT COUNT(*) as cnt FROM user GROUP BY dept_id) t))") List<Department> findPopularDepts();5. 性能调优与常见问题
5.1 SQL 注入防护
危险写法:
@Select("SELECT * FROM user WHERE name = '${name}'") // 直接拼接 User findByName(@Param("name") String name);安全写法:
@Select("SELECT * FROM user WHERE name = #{name}") // 预编译 User findByName(@Param("name") String name);5.2 慢查询优化方案
- 索引检查:
EXPLAIN SELECT * FROM user WHERE name = 'test';- N+1 问题解决:
<!-- 错误方式:循环查询 --> <select id="getUsers" resultType="User"> SELECT * FROM user </select> <!-- 正确方式:一次加载 --> <select id="getUsersWithDepts" resultMap="userDeptMap"> SELECT u.*, d.* FROM user u LEFT JOIN department d ON u.dept_id = d.id </select>5.3 事务管理要点
@Service @Transactional(rollbackFor = Exception.class) public class UserService { @Transactional(propagation = Propagation.NOT_SUPPORTED) // 只读操作 public List<User> queryComplexUsers() { // 查询方法 } }事务传播行为选择:
| 行为 | 适用场景 |
|---|---|
| REQUIRED(默认) | 多数写操作 |
| NOT_SUPPORTED | 复杂查询 |
| REQUIRES_NEW | 独立事务操作 |
6. 最新版本特性应用
MyBatis-Plus 3.5.0+ 新增功能:
- 动态表名支持:
public String dynamicTableName(String sql, String tableName) { return sql.replaceAll("user", tableName); } // 配置 mybatis-plus: table-name-parser: com.example.config.DynamicTableNameParser- SQL 注入器扩展:
public class CustomSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> methods = super.getMethodList(mapperClass); methods.add(new BatchInsertMethod()); // 自定义方法 return methods; } }- 多租户实现:
public class TenantHandler implements TenantLineHandler { @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public Expression getTenantId() { return new StringValue("当前租户ID"); } }7. 开发实践建议
代码组织规范:
- 简单查询:使用 MP 条件构造器
- 中等复杂度:注解方式
- 复杂场景:XML 映射文件
监控配置:
# 开启性能分析插件(开发环境) mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl- 分页优化技巧:
// 禁用 count 查询 Page<User> page = new Page<>(1, 10, false);- 类型处理器扩展:
@MappedTypes(Enum.class) public class CustomEnumHandler extends BaseTypeHandler<Enum> { // 实现枚举自定义存储逻辑 }8. 复杂查询综合案例
8.1 动态多条件分页查询
查询参数:
@Data public class UserQuery { private String name; private Integer minAge; private Integer maxAge; private List<Integer> deptIds; private LocalDateTime createTimeStart; private LocalDateTime createTimeEnd; }Mapper XML:
<select id="searchUsers" resultType="UserVO"> SELECT u.*, d.name AS deptName FROM user u LEFT JOIN department d ON u.dept_id = d.id <where> <if test="query.name != null and query.name != ''"> AND u.name LIKE CONCAT('%', #{query.name}, '%') </if> <if test="query.minAge != null"> AND u.age >= #{query.minAge} </if> <if test="query.maxAge != null"> AND u.age <= #{query.maxAge} </if> <if test="query.deptIds != null and !query.deptIds.isEmpty()"> AND u.dept_id IN <foreach collection="query.deptIds" item="id" open="(" separator="," close=")"> #{id} </foreach> </if> <if test="query.createTimeStart != null"> AND u.create_time >= #{query.createTimeStart} </if> <if test="query.createTimeEnd != null"> AND u.create_time <= #{query.createTimeEnd} </if> </where> ORDER BY u.create_time DESC </select>8.2 统计报表查询
场景:按部门统计用户年龄分布
<select id="userAgeReport" resultType="map"> SELECT d.name AS deptName, COUNT(*) AS total, SUM(CASE WHEN u.age < 20 THEN 1 ELSE 0 END) AS "age<20", SUM(CASE WHEN u.age BETWEEN 20 AND 30 THEN 1 ELSE 0 END) AS "age20-30", SUM(CASE WHEN u.age > 30 THEN 1 ELSE 0 END) AS "age>30" FROM user u JOIN department d ON u.dept_id = d.id GROUP BY d.name </select>9. 调试与问题排查
9.1 SQL 日志输出配置
logging: level: com.example.mapper: debug9.2 常见异常处理
BindingException:
- 检查 XML 中的 id 与 Mapper 接口是否一致
- 确认 parameterType/resultType 路径正确
SQLSyntaxErrorException:
- 验证 SQL 语法是否符合当前数据库类型
- 检查保留字是否使用反引号包裹
PaginationException:
- 确保分页参数正确传递
- 检查是否配置分页插件
9.3 性能分析工具
- Arthas 监控 SQL:
watch com.example.mapper.*Mapper '*'{params,returnObj} -x 2- Druid 监控:
spring: datasource: druid: stat-view-servlet: enabled: true10. 扩展思考方向
多数据源支持:
- 动态切换数据源注解
@DS("slave") - 读写分离配置
- 动态切换数据源注解
存储过程调用:
<select id="callProcedure" statementType="CALLABLE"> {call user_procedure(#{param1,mode=IN},#{param2,mode=OUT})} </select>自定义 TypeHandler:
- 处理 JSON 字段与 Java 对象转换
- 实现加密字段自动加解密
逻辑删除优化:
// 自定义删除语句 @Delete("UPDATE user SET deleted = 1 WHERE id = #{id}") int logicDeleteById(@Param("id") Long id);- 二级缓存整合:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>