使用MyBatis Dynamic SQL处理复杂的JOIN或子查询

什么是 MyBatis Dynamic SQL?

MyBatis Dynamic SQL 是 MyBatis 3 的一个核心子项目 (以前是一个独立库,现已完全整合到 MyBatis 3 中)。它的核心目标是提供一种类型安全、流畅且富有表达力的方式来在 Java 代码中构建动态 SQL 语句,彻底摒弃了传统的 XML 动态 SQL 标签或繁琐的字符串拼接。

核心概念和特点

  1. 流畅的 API (Fluent API):

    • 提供了一组链式调用的方法(如 select(), from(), where(), and(), or(), orderBy(), groupBy() 等),让 SQL 语句的构建看起来就像在用代码"写" SQL 一样自然、可读。

    • 例如:

      java 复制代码
      SelectStatementProvider selectStatement = select(id, firstName, lastName, birthDate)
              .from(person)
              .where(id, isEqualTo(1))
              .or(firstName, isLike("John%"), and(lastName, isLike("Doe%")))
              .orderBy(lastName.descending(), firstName.ascending())
              .build()
              .render(RenderingStrategies.MYBATIS3);
  2. 类型安全 (Type Safety):

    • 这是其最大的优势之一。表名、列名、条件值等都被定义为强类型的对象(通常通过注解处理器或工具自动生成)。
    • 编译器会在编译时检查你的 SQL 构建逻辑,防止出现列名拼写错误、类型不匹配(比如试图比较字符串和数字)等低级错误。这显著提高了代码的健壮性。
  3. 动态 SQL 构建 (Dynamic SQL):

    • 完美解决了根据运行时条件动态拼接 SQL 片段(如 WHERE 子句、JOIN 条件、ORDER BY 列等)的需求。
    • 提供了丰富的条件构建方法 (isEqualTo, isLike, isBetween, isIn, isNull, isNotNull, when 等),并支持复杂的逻辑组合 (and, or)。
    • 条件可以轻松地根据业务逻辑开启或关闭,避免写大量 if 判断和字符串拼接。
  4. 与 MyBatis 3 无缝集成:

    • 生成的 SQL 语句和参数映射被封装成一个 SelectStatementProvider, InsertStatementProvider, UpdateStatementProvider, 或 DeleteStatementProvider 对象。

    • 这个对象可以直接传递给 MyBatis Mapper 接口中对应的方法(这些方法的参数类型就是 XxxStatementProvider)。MyBatis 核心引擎负责执行这个准备好的语句。

    • 例如 Mapper 接口:

      java 复制代码
      @Mapper
      public interface PersonMapper {
          @SelectProvider(type = SqlProviderAdapter.class, method = "select")
          List<Person> selectMany(SelectStatementProvider selectStatement);
      }
  5. 支持 CRUD 和复杂查询:

    • 不仅支持 SELECT,也完全支持 INSERT, UPDATE, DELETE 语句的动态构建。
    • 支持 JOINs, 子查询, UNIONs, 函数调用等相对复杂的 SQL 结构。
  6. 代码生成器支持:

    • MyBatis Generator (MBG) 提供了对 Dynamic SQL 的强力支持。它可以自动生成:
      • 与数据库表结构对应的强类型 Domain 对象(Model)。
      • 包含所有列名、表名常量(或对象)的 XxxDynamicSqlSupport 类。
      • 基础的 CRUD Mapper 接口和方法(使用 Dynamic SQL 构建)。
      • 这大大减少了手写样板代码的工作量。

为什么要使用 MyBatis Dynamic SQL?

使用 MyBatis Dynamic SQL 带来了显著的优势,解决了传统 MyBatis 使用方式中的诸多痛点:

  1. 告别繁琐的 XML 和 OGNL:

    • 痛点: 传统的 MyBatis XML Mapper 文件虽然功能强大,但编写和阅读动态 SQL(使用 <if>, <choose>, <when>, <otherwise>, <foreach> 等标签)在复杂场景下会变得冗长、嵌套深、可读性下降,且需要掌握 OGNL 表达式。在 Java 和 XML 之间切换也影响开发效率。
    • 解决: Dynamic SQL 将 SQL 构建逻辑完全移回 Java 代码中,利用 Java 语言的流程控制 (if/else, 循环) 和强大的 IDE 支持(代码补全、重构、导航),开发体验更流畅、更现代。
  2. 彻底杜绝 SQL 注入风险:

    • 痛点: 手动拼接 SQL 字符串是 SQL 注入漏洞的主要来源。即使使用 StringBuilder? 占位符,拼接列名、表名或复杂条件片段时仍然容易出错或被恶意利用。
    • 解决: Dynamic SQL 的 Fluent API 和类型安全设计从根本上杜绝了 SQL 拼接 。所有的值都是通过 MyBatis 的 #{} 参数占位符机制传递的,列名和表名是框架内部定义的安全对象或常量。开发者几乎不可能写出有 SQL 注入漏洞的代码。
  3. 强大的编译时类型检查:

    • 痛点: XML 中的 SQL 错误(如拼错的列名、类型不匹配的条件)只能在运行时被发现(通过 SQL 异常),增加了调试成本和线上风险。
    • 解决: 由于列名、表名、条件操作符和值都是强类型的,编译器在编译阶段就能捕获绝大部分 SQL 结构错误 (比如对一个日期类型的列使用了字符串的 LIKE 操作,或者引用了一个不存在的列)。这极大地提高了代码质量和开发效率,减少了运行时错误。
  4. 提升代码可读性和可维护性:

    • 痛点: 复杂的 XML 动态 SQL 或冗长的 Java 字符串拼接 SQL 难以阅读和理解,尤其对于后续维护者。
    • 解决: Fluent API 让 SQL 的构建逻辑清晰明了,链式调用直观地反映了 SQL 的结构(SELECT 什么 FROM 哪里 WHERE 哪些条件 ORDER BY 什么)。逻辑组合 (and/or/when) 也更符合编程思维。代码即文档,易于理解和修改。
  5. 简化动态条件处理:

    • 痛点: 处理多变的查询条件(比如一个搜索表单有多个可选筛选条件)在传统方式下需要写大量的 if 判断来拼接 WHERE 子句,代码冗长且容易出错(比如处理第一个条件的 AND 问题)。
    • 解决: Dynamic SQL 的条件构建器 (where(...), and(...), or(...)) 自动处理了条件的动态添加移除以及 AND/OR 连接符的合理性。开发者只需关注业务条件本身,框架负责生成语法正确的 SQL。when(condition, ...) 方法提供了更声明式的条件添加方式。
  6. 更好的 IDE 支持:

    • 痛点: XML 编辑器的智能提示远不如 Java IDE 强大。字符串拼接的 SQL 完全没有代码补全和重构支持。
    • 解决: 在 Java 代码中使用 Fluent API 和强类型对象,开发者可以享受到 IDE 的全功能支持:代码自动补全(列名、方法名)、重构(重命名列、方法提取)、快速导航、错误实时提示等,极大提升开发速度和准确性。
  7. 避免 MyBatis "Example" 类的局限性:

    • 痛点: MyBatis Generator 早期生成的 XxxExample 类也用于构建动态查询条件,但它有很多限制:条件类型有限、组合逻辑不够灵活、生成的 SQL 不够直观、难以处理复杂查询(JOIN, 子查询)、生成的模型臃肿。
    • 解决: Dynamic SQL 是官方推荐的、功能更强大、更灵活、更现代的替代方案。它克服了 Example 类的所有主要缺点。
  8. 与注解 Mapper 互补:

    • 痛点: MyBatis 注解 (@Select, @Insert 等) 适合写简单固定的 SQL,但写复杂的动态 SQL 会变得非常混乱(在注解字符串里拼接 SQL 片段)。
    • 解决: Dynamic SQL 完美解决了注解方式在动态 SQL 上的短板。两者可以结合使用:简单语句用注解,复杂动态语句用 Dynamic SQL 构建并通过 @SelectProvider 等注解引用。

使用MyBatis Dynamic SQL处理复杂的JOIN或子查询

在 MyBatis Dynamic SQL 中处理复杂的 JOIN 和子查询是完全可行的,虽然相比单表操作需要更多手动配置,但依然能享受类型安全和动态 SQL 的优势。

需求描述

下面通过一个具体的需求来演示如何使用MyBatis Dynamic SQL处理复杂的JOIN或子查询。

定义两个表:rulerule_version,建表语句如下:

sql 复制代码
CREATE TABLE `rule` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `rule_id` varchar(64) NOT NULL COMMENT '规则ID',
  `domain` varchar(32) NOT NULL COMMENT '业务域',
  `name` varchar(128) NOT NULL COMMENT '规则名称',
  `description` varchar(512) DEFAULT NULL COMMENT '规则描述',
  `type` varchar(32) NOT NULL COMMENT '规则类型',
  `creator` varchar(32) DEFAULT NULL COMMENT '创建者',
  `modifier` varchar(32) DEFAULT NULL COMMENT '更新者',
  `tenant_id` varchar(32) NOT NULL COMMENT '租户ID',
  `ext_info` text DEFAULT NULL COMMENT '扩展字段',
  PRIMARY KEY(`id`),
  UNIQUE KEY `uk_uuid`(`rule_id`, `tenant_id`) GLOBAL,
  KEY `idx_rule_name`(`name`, `tenant_id`) GLOBAL
)  DEFAULT CHARSET = utf8mb4 COMMENT = '规则表';


CREATE TABLE `rule_version` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `rule_id` varchar(32) NOT NULL COMMENT '规则ID',
  `version` int(11) NOT NULL DEFAULT '1' COMMENT '版本号',
  `name` varchar(128) NOT NULL COMMENT '规则版本名称',
  `description` varchar(512) DEFAULT NULL COMMENT '规则版本描述',
  `status` varchar(32) NOT NULL DEFAULT 'DRAFT' COMMENT '版本状态',
  `creator` varchar(32) DEFAULT NULL COMMENT '创建人',
  `modifier` varchar(32) DEFAULT NULL COMMENT '更新人',
  `ext_info` text DEFAULT NULL COMMENT '扩展字段',
  `config` varchar(1024) DEFAULT NULL COMMENT '规则配置详情',
  PRIMARY KEY(`id`),
  UNIQUE KEY `uk_uuid_version`(`rule_id`, `version`) GLOBAL
)  DEFAULT CHARSET = utf8mb4 COMMENT = '规则版本表';

根据提供的建表语句,以下是两个表的ER图(使用mermaid语法表示),展示了 一个规则(rule)对应多个规则版本(rule_version) 的一对多关系:

erDiagram rule ||--o{ rule_version : "1:N" rule { bigint id PK "主键" timestamp gmt_create "创建时间" timestamp gmt_modified "修改时间" varchar(64) rule_id "规则ID" varchar(32) domain "业务域" varchar(128) name "规则名称" varchar(512) description "规则描述" varchar(32) type "规则类型" varchar(32) creator "创建者" varchar(32) modifier "更新者" varchar(32) tenant_id "租户ID" text ext_info "扩展字段" } rule_version { bigint id PK "主键" timestamp gmt_create "创建时间" timestamp gmt_modified "修改时间" varchar(32) rule_id "规则ID(FK)" int version "版本号" varchar(128) name "版本名称" varchar(512) description "版本描述" varchar(32) status "状态" varchar(32) creator "创建人" varchar(32) modifier "更新人" text ext_info "扩展字段" varchar(1024) config "规则配置" }

需求描述:根据前面的提供的两个表,分页查询规则信息和规则对应的最大版本信息并按照规则版本的更新时间倒序排列

需求分析:我们可以先获取每个规则的最新版本记录(最大版本号对应的记录),然后与规则版本表再次关联以获取最新版本的完整信息,最后再与规则表关联。

需求实现:根据需求,需要查询规则信息及其对应的最新版本信息(最大版本号),并按版本更新时间倒序排列。以下是实现该查询的SQL语句:

sql 复制代码
SELECT ruleDO.id AS id, ruleDO.rule_id AS ruleId, ruleDO.name AS name, ruleDO.domain AS domain, max_version AS latestVersion
	, ruleVersionDO.name AS versionName, ruleVersionDO.status AS versionStatus, ruleVersionDO.gmt_modified AS versionModifiedDate
FROM rule ruleDO
	JOIN rule_version ruleVersionDO ON ruleDO.rule_id = ruleVersionDO.rule_id
	JOIN (
		SELECT ruleVersionDO.rule_id AS rule_uuid, MAX(ruleVersionDO.version) AS max_version
		FROM rule_version ruleVersionDO
		WHERE ruleVersionDO.id > #{parameters.p1,jdbcType=BIGINT}
			AND ruleVersionDO.modifier LIKE #{parameters.p2,jdbcType=VARCHAR}
			AND ruleVersionDO.gmt_create >= #{parameters.p3,jdbcType=TIMESTAMP}
			AND ruleVersionDO.gmt_create <= #{parameters.p4,jdbcType=TIMESTAMP}
			AND ruleVersionDO.gmt_modified >= #{parameters.p5,jdbcType=TIMESTAMP}
			AND ruleVersionDO.gmt_modified <= #{parameters.p6,jdbcType=TIMESTAMP}
			AND ruleVersionDO.description LIKE #{parameters.p7,jdbcType=VARCHAR}
			AND ruleVersionDO.name LIKE #{parameters.p8,jdbcType=VARCHAR}
			AND ruleVersionDO.status = #{parameters.p9,jdbcType=VARCHAR}
		GROUP BY ruleVersionDO.rule_id
	) max_ver
	ON ruleVersionDO.rule_id = max_ver.rule_uuid
		AND ruleVersionDO.version = max_ver.max_version
WHERE ruleDO.id > #{parameters.p10,jdbcType=BIGINT}
	AND ruleDO.tenant_id = #{parameters.p11,jdbcType=VARCHAR}
	AND ruleDO.rule_id LIKE #{parameters.p12,jdbcType=VARCHAR}
	AND ruleDO.name LIKE #{parameters.p13,jdbcType=VARCHAR}
	AND ruleDO.creator LIKE #{parameters.p14,jdbcType=VARCHAR}
	AND ruleDO.type = #{parameters.p15,jdbcType=VARCHAR}
	AND ruleDO.domain = #{parameters.p16,jdbcType=VARCHAR}
	AND ruleDO.description LIKE #{parameters.p17,jdbcType=VARCHAR}
ORDER BY ruleVersionDO.id
LIMIT #{parameters.p19}, #{parameters.p18}

需求分析

在springboot 中使用 mybatis dynamic sql 实现上面的sql语句,我们首先需要分析这个SQL语句的结构。该SQL语句的主要部分包括:

  1. 从rule表(别名为ruleDO)和rule_version表(别名为ruleVersionDO)中选取字段。

  2. 使用一个子查询(别名为max_ver)来获取每个rule_id的最大版本号,同时子查询中有一系列的条件。

  3. 将ruleDO和ruleVersionDO表与子查询max_ver进行连接,连接条件是rule_id相等并且版本号等于最大版本。

  4. 对ruleDO表有一系列的条件。

  5. 按照ruleVersionDO.id排序,并分页。

在MyBatis Dynamic SQL中,我们可以这样构建:

步骤:

  1. 创建rule和rule_version表的动态SQL支持类(通常使用代码生成器生成,这里假设我们已经有了RuleDynamicSqlSupport和RuleVersionDynamicSqlSupport)。

  2. 将整个查询拆分为几个部分:

a. 子查询(max_ver):从rule_version表中,根据条件分组获取每个rule_id的最大版本。

b. 主查询:连接rule、rule_version和子查询max_ver,并添加条件,最后排序分页。

注意:由于子查询中使用了多个条件,且主查询也有多个条件,我们需要分别构建。

使用 mybatis dynamic sql 实现

参考前面的文章 Spring Boot中使用MyBatis Generator生成动态SQL 根据表结构自动生成的表定义类。

1. 创建表定义

示例代码如下:

java 复制代码
import jakarta.annotation.Generated;
import java.sql.JDBCType;
import java.util.Date;
import org.mybatis.dynamic.sql.AliasableSqlTable;
import org.mybatis.dynamic.sql.SqlColumn;

// 自动生成的表定义类(由MyBatis Generator创建)
public final class RuleVersionEntityDynamicSqlSupport {
    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final RuleVersionEntity ruleVersionEntity = new RuleVersionEntity();

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Long> id = ruleVersionEntity.id;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Date> gmtCreate = ruleVersionEntity.gmtCreate;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Date> gmtModified = ruleVersionEntity.gmtModified;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> ruleId = ruleVersionEntity.ruleId;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<Integer> version = ruleVersionEntity.version;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> name = ruleVersionEntity.name;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> description = ruleVersionEntity.description;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> status = ruleVersionEntity.status;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> creator = ruleVersionEntity.creator;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> modifier = ruleVersionEntity.modifier;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> config = ruleVersionEntity.config;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final SqlColumn<String> extInfo = ruleVersionEntity.extInfo;

    @Generated("org.mybatis.generator.api.MyBatisGenerator")
    public static final class RuleVersionEntity extends AliasableSqlTable<RuleVersionEntity> {
        public final SqlColumn<Long> id = column("id", JDBCType.BIGINT);

        public final SqlColumn<Date> gmtCreate = column("gmt_create", JDBCType.TIMESTAMP);

        public final SqlColumn<Date> gmtModified = column("gmt_modified", JDBCType.TIMESTAMP);

        public final SqlColumn<String> ruleId = column("rule_id", JDBCType.VARCHAR);

        public final SqlColumn<Integer> version = column("version", JDBCType.INTEGER);

        public final SqlColumn<String> name = column("name", JDBCType.VARCHAR);

        public final SqlColumn<String> description = column("description", JDBCType.VARCHAR);

        public final SqlColumn<String> status = column("status", JDBCType.VARCHAR);

        public final SqlColumn<String> creator = column("creator", JDBCType.VARCHAR);

        public final SqlColumn<String> modifier = column("modifier", JDBCType.VARCHAR);

        public final SqlColumn<String> config = column("config", JDBCType.VARCHAR);

        public final SqlColumn<String> extInfo = column("ext_info", JDBCType.LONGVARCHAR);

        public RuleVersionEntity() {
            super("rule_version", RuleVersionEntity::new);
        }
    }
}

2. 构建MyBatis Dynamic SQL复杂查询

java 复制代码
import com.example.demo.common.model.page.PageRequest;
import com.example.demo.model.query.RuleQueryCondition;
import com.example.demo.repository.generated.RuleEntityDynamicSqlSupport;
import com.example.demo.repository.generated.RuleVersionEntityDynamicSqlSupport;
import org.mybatis.dynamic.sql.SortSpecification;
import org.mybatis.dynamic.sql.SqlColumn;
import org.mybatis.dynamic.sql.SqlTable;
import org.mybatis.dynamic.sql.select.ColumnSortSpecification;
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
import org.mybatis.dynamic.sql.select.SelectModel;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.render.RenderingStrategies;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.sql.JDBCType;

import static org.mybatis.dynamic.sql.SqlBuilder.*;

@Component
public class RuleQueryBuilder {
    private final RuleVersionEntityDynamicSqlSupport.RuleVersionEntity ruleVersionDO = RuleVersionEntityDynamicSqlSupport.ruleVersionEntity;
    private final RuleEntityDynamicSqlSupport.RuleEntity ruleDO = RuleEntityDynamicSqlSupport.ruleEntity;

    // 数据查询
    public SelectStatementProvider buildDataQuery(RuleQueryCondition queryCondition, PageRequest pageRequest) {

        // 1. 创建子查询表的别名和列定义
        // 子查询的表别名
        String subQueryTable = "max_ver";
        SqlTable maxVerTable = SqlTable.of(subQueryTable);
        SqlColumn<String> maxVerRuleUuid = SqlColumn.of("rule_uuid", maxVerTable, JDBCType.VARCHAR);
        SqlColumn<Integer> maxVerMaxVersion = SqlColumn.of("max_version", maxVerTable, JDBCType.INTEGER);
        // 动态构建排序
        List<SortSpecification> sortSpecs = new ArrayList<>();
        SortSpecification sortSpecification = buildSortSpecification(pageRequest.getSort(), pageRequest.getOrder());
        if (sortSpecification != null) {
            sortSpecs.add(sortSpecification);
        }

        // 2.构建子查询
        QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);

        // 3. 主查询:关联规则表、版本表和最大版本子查询
        return select(
                ruleDO.id.as("id"),
                ruleDO.ruleId.as("ruleId"),
                ruleDO.name.as("name"),
                ruleDO.domain.as("domain"),
                maxVerMaxVersion.as("latestVersion"),
                ruleVersionDO.name.as("versionName"),
                ruleVersionDO.status.as("versionStatus"),
                ruleVersionDO.gmtModified.as("versionModifiedDate")
        )
                .from(ruleDO, "ruleDO")
                .join(ruleVersionDO, "ruleVersionDO")
                .on(ruleDO.ruleId, equalTo(ruleVersionDO.ruleId))
                .join(maxVersionSubQuery, subQueryTable)
                .on(ruleVersionDO.ruleId, equalTo(maxVerRuleUuid.qualifiedWith(subQueryTable)))
                .and(ruleVersionDO.version, equalTo(maxVerMaxVersion.qualifiedWith(subQueryTable)))
                .where(ruleDO.id, isGreaterThan(0L))
                .and(ruleDO.tenantId, isEqualToWhenPresent(queryCondition.getTenantId()))
                .and(ruleDO.ruleId, isLikeWhenPresent(wrapLike(queryCondition.getRuleId())))
                .and(ruleDO.name, isLikeWhenPresent(wrapLike(queryCondition.getName())))
                .and(ruleDO.creator, isLikeWhenPresent(wrapLike(queryCondition.getCreateBy())))
                .and(ruleDO.type, isEqualToWhenPresent(queryCondition.getType()))
                .and(ruleDO.domain, isEqualToWhenPresent(queryCondition.getDomain()))
                .and(ruleDO.description, isLikeWhenPresent(wrapLike(queryCondition.getDescription())))
                .orderBy(sortSpecs.toArray(new SortSpecification[0]))
                .limit(pageRequest.getPageSize())
                .offset(pageRequest.getOffset())
                .build()
                .render(RenderingStrategies.MYBATIS3);

    }

    // 总数查询
    public SelectStatementProvider buildCountQuery(RuleQueryCondition queryCondition) {
        // 1. 创建派生表的别名和列定义

        String subQueryTable = "max_ver";
        SqlTable maxVerTable = SqlTable.of(subQueryTable);
        SqlColumn<String> maxVerRuleUuid = SqlColumn.of("rule_uuid", maxVerTable, JDBCType.VARCHAR);
        SqlColumn<Integer> maxVerMaxVersion = SqlColumn.of("max_version", maxVerTable, JDBCType.INTEGER);
        // 2. 构建子查询
        QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);

        // 3. 主查询:关联规则表、版本表和最大版本子查询
        return select(count())
                .from(ruleDO, "ruleDO")
                .join(ruleVersionDO, "ruleVersionDO")
                .on(ruleDO.ruleId, equalTo(ruleVersionDO.ruleId))
                .join(maxVersionSubQuery, subQueryTable)
                .on(ruleVersionDO.ruleId, equalTo(maxVerRuleUuid.qualifiedWith(subQueryTable)))
                .and(ruleVersionDO.version, equalTo(maxVerMaxVersion.qualifiedWith(subQueryTable)))
                .where(ruleVersionDO.id, isGreaterThan(0L))  // 确保where条件有值
                .and(ruleDO.tenantId, isEqualToWhenPresent(queryCondition.getTenantId()))
                .and(ruleDO.ruleId, isLikeWhenPresent(wrapLike(queryCondition.getRuleId())))
                .and(ruleDO.name, isLikeWhenPresent(wrapLike(queryCondition.getName())))
                .and(ruleDO.creator, isLikeWhenPresent(wrapLike(queryCondition.getCreateBy())))
                .and(ruleDO.type, isEqualToWhenPresent(queryCondition.getType()))
                .and(ruleDO.domain, isEqualToWhenPresent(queryCondition.getDomain()))
                .and(ruleDO.description, isLikeWhenPresent(wrapLike(queryCondition.getDescription())))
                .build()
                .render(RenderingStrategies.MYBATIS3);
    }

    // 公共方法:构建最大版本子查询
    private QueryExpressionDSL<SelectModel>.GroupByFinisher buildMaxVersionSubQuery(RuleQueryCondition queryCondition) {
        return select(
                ruleVersionDO.ruleId.as("rule_uuid"),
                max(ruleVersionDO.version).as("max_version"))
                .from(ruleVersionDO)
                .where(ruleVersionDO.id, isGreaterThan(0L))
                .and(ruleVersionDO.modifier, isLikeWhenPresent(wrapLike(queryCondition.getUpdateBy())))
                .and(ruleVersionDO.gmtCreate, isGreaterThanOrEqualToWhenPresent(queryCondition.getGmtCreateFrom()))
                .and(ruleVersionDO.gmtCreate, isLessThanOrEqualToWhenPresent(queryCondition.getGmtCreateTo()))
                .and(ruleVersionDO.gmtModified, isGreaterThanOrEqualToWhenPresent(queryCondition.getGmtModifiedFrom()))
                .and(ruleVersionDO.gmtModified, isLessThanOrEqualToWhenPresent(queryCondition.getGmtModifiedTo()))
                .and(ruleVersionDO.description, isLikeWhenPresent(wrapLike(queryCondition.getRuleVersionDesc())))
                .and(ruleVersionDO.name, isLikeWhenPresent(wrapLike(queryCondition.getRuleVersionName())))
                .and(ruleVersionDO.status, isEqualToWhenPresent(queryCondition.getStatus()))

                .groupBy(ruleVersionDO.ruleId);
    }

    private SortSpecification buildSortSpecification(String field, String order) {
        if (field == null) {
            return new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.id);
        }
        ColumnSortSpecification columnSortSpecification;
        switch (field) {
            case "gmtCreate" ->
                    columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.gmtCreate);
            case "gmtModified" ->
                    columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.gmtModified);
            // 其他字段...
            // 默认排序逻辑
            default -> columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.id);
        }

        return "asc".equalsIgnoreCase(order) ? columnSortSpecification : columnSortSpecification.descending();
    }


    private String wrapLike(String value) {
        return value != null ? "%" + value + "%" : null;
    }


}

在使用MyBatis Dynamic SQL处理复杂的JOIN或子查询时,需要通过下面的方式创建子查询的表和字段定义:

java 复制代码
// 1. 创建子查询表的别名和列定义
// 子查询的表别名
String subQueryTable = "max_ver";
SqlTable maxVerTable = SqlTable.of(subQueryTable);
SqlColumn<String> maxVerRuleUuid = SqlColumn.of("rule_uuid", maxVerTable, JDBCType.VARCHAR);
SqlColumn<Integer> maxVerMaxVersion = SqlColumn.of("max_version", maxVerTable, JDBCType.INTEGER);

3. 结果映射

创建自定义的Mapper接口,

java 复制代码
import com.example.demo.model.dto.response.RuleWithLatestVersionDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper;

import java.util.List;

@Mapper
public interface RuleCustomMapper extends CommonCountMapper {


    // 使用@Result注解处理多表字段
    @SelectProvider(type = SqlProviderAdapter.class, method = "select")
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "ruleId", property = "ruleId"),
            @Result(column = "name", property = "name"),
            @Result(column = "domain", property = "domain"),
            @Result(column = "latestVersion", property = "latestVersion"),
            @Result(column = "versionName", property = "versionName"),
            @Result(column = "versionStatus", property = "versionStatus"),
            @Result(column = "versionModifiedDate", property = "versionModifiedDate"),
    })
    List<RuleWithLatestVersionDTO> findByCondition(SelectStatementProvider selectStatement);
}

五、对比传统MyBatis实现

假设实现多表动态查询:"查询纽约部门下薪资大于X的员工"

传统XML方式:

xml 复制代码
<select id="findEmployees" resultMap="employeeResult">
  SELECT * FROM employee e
  JOIN department d ON e.dept_id = d.id
  <where>
    <if test="location != null">
      d.location = #{location}
    </if>
    <if test="minSalary != null">
      AND e.salary > #{minSalary}
    </if>
  </where>
</select>

Dynamic SQL实现:

java 复制代码
public SelectStatementProvider buildQuery(String location, BigDecimal minSalary) {
    return select(emp.columns())
        .from(emp)
        .join(dept).on(emp.deptId, equalTo(dept.id))
        .where(dept.location, isEqualTo(location).when(Objects::nonNull))
        .and(emp.salary, isGreaterThan(minSalary).when(Objects::nonNull))
        .build()
        .render(RenderingStrategies.MYBATIS3);
}

优势对比:

特性 传统XML Dynamic SQL
类型安全 ❌ 运行时错误 ✅ 编译时检查
JOIN条件动态性 有限 ✅ 任意组合
子查询支持 复杂 ✅ 流畅API
重构友好度 ❌ (字符串引用) ✅ (强类型引用)
多表字段冲突处理 手动别名 ✅ 自动别名管理

问题记录

NonRenderingWhereClauseException

问题描述: 使用 mybatis dynamic sql 生成sql语句报错如下:

java 复制代码
org.mybatis.dynamic.sql.exception.NonRenderingWhereClauseException: A where clause was specified, but failed to render

这个错误 NonRenderingWhereClauseException 通常发生在使用 MyBatis Dynamic SQL 时,你指定了一个 WHERE 子句,但在生成 SQL 时,该 WHERE 子句中的条件没有一个被包含在最终的 SQL 语句中。这通常是因为条件中的值没有被设置或者条件本身被跳过了(比如在可选的条件下,所有条件都不满足)。

错误原因分析:

  1. 在构建 WHERE 条件时,可能使用了 andor 方法连接条件,但是这些条件在运行时都没有被激活(例如,使用了 when 判断,但条件都不成立)。

  2. 可能没有正确使用条件构建器(例如,SqlBuilder 的静态方法)来构建条件。

解决方案:

  1. 检查你的 WHERE 条件构建代码,确保至少有一个条件会被激活(即条件中的 test 方法返回 true)。

  2. 如果你使用了多个条件,确保它们之间逻辑正确,并且至少有一个条件会被包含。

具体到代码中,你可以这样调整:

原来的错误代码可能类似于:

java 复制代码
public SelectStatementProvider selectWithCondition(Integer id, String name) {

    return select(id, name, age)
    .from(person)
    .where(id, isEqualTo(id)) // 如果id为null,这个条件不会激活
    .and(name, isLike(name)) // 如果name为null,这个条件也不会激活
    .build()
    .render(RenderingStrategies.MYBATIS3);

}

如果id和name都为null,那么WHERE子句就没有条件,从而抛出NonRenderingWhereClauseException异常。

解决方法:确保至少有一个条件不为null,或者使用一个始终成立的条件(比如1=1)作为基础。

  • 创建自定义恒真条件: 如果有非空字段(如主键),可以创建恒真条件 where(id, isGreaterThan(0L)) (主键总是大于 0)

修改后的代码如下,

java 复制代码
public SelectStatementProvider selectWithCondition(Integer id, String name) {

    return select(id, name, age)
    .from(person)
    .where(id, isGreaterThan(0L))  // 创建恒真条件,主键总是大于 0
    .and(id, isEqualTo(id)) // 如果id为null,这个条件不会激活
    .and(name, isLike(name)) // 如果name为null,这个条件也不会激活
    .build()
    .render(RenderingStrategies.MYBATIS3);

}

总结

虽然 MyBatis Dynamic SQL 在处理复杂 JOIN 和子查询时需要更多手动配置,但它提供了:

  1. 类型安全的复杂查询构建
  2. 动态 SQL 的组合能力
  3. 优于 XML 的可维护性
  4. 编译时错误检测

对于超复杂场景的建议:

  1. 将大查询分解为多个子查询构建器
  2. 为常用 JOIN 模式创建可重用组件
  3. 对派生表使用专门的 DTO 接收结果 结合 MyBatis 的 @ResultMap 处理多层嵌套结果

MyBatis Dynamic SQL 的核心价值在于:它通过提供一套类型安全、流畅的 Java API,革命性地改善了在 MyBatis 中构建动态 SQL 语句的体验。

  • 为什么用它? 因为它解决了传统 XML 动态 SQL 和字符串拼接的主要痛点:消除了 SQL 注入风险、提供了强大的编译时检查、显著提升了代码的可读性、可维护性和开发效率(借助 IDE 支持)、简化了复杂动态条件的处理。
  • 它带来什么? 更安全、更健壮、更易读、更易维护、更高效的数据库访问层代码。对于需要构建复杂动态查询的 MyBatis 项目来说,它是一个非常值得采用的强大工具,代表了 MyBatis 动态 SQL 处理的现代最佳实践。尤其是结合 MyBatis Generator 使用,能大幅提升开发数据库交互代码的生产力和质量。

通过合理设计,Dynamic SQL 能优雅地处理 90% 以上的复杂 SQL 场景,同时保持代码的健壮性和可读性。

参考文档

  1. MyBaits Dynamic SQL官方文档:mybatis.org/mybatis-dyn...
  2. MyBatis Generator官方文档:mybatis.org/generator/
相关推荐
鸡窝头on19 分钟前
Spring Boot 多 Profile 配置详解
spring boot·后端
风之旅人21 分钟前
开发必备"节假日接口"
java·后端·开源
2201_753169471 小时前
implement用法
java·开发语言
不会编程的阿成1 小时前
spring aop的概念与实战以及面试项目题
java·spring·面试
天上掉下来个程小白1 小时前
Apache ECharts-01.介绍
前端·javascript·spring boot·apache·苍穹外卖
李强57627822 小时前
语法制导的语义计算(包含python源码)
java·数据库·python
鼠鼠我捏,要死了捏2 小时前
Java开发企业微信会话存档功能笔记小结(企业内部开发角度)
java·企业微信·会话存档
wx_ywyy67982 小时前
“微信短剧小程序开发指南:从架构设计到上线“
java·python·短剧·短剧系统·海外短剧·推客小程序·短剧系统开发
缘友一世2 小时前
设计模式之五大设计原则(SOLID原则)浅谈
java·算法·设计模式
Mazeltov&&Iliua2 小时前
JAVA 基础知识(一)
java·开发语言