Java-05 深入浅出 MyBatis动态SQL与参数拼接完全指南

TL;DR

  • 场景:Java工程师学习MyBatis动态SQL特性的开发者
  • 结论:MyBatis通过if/where/foreach/sql标签实现灵活SQL构建,参数null值需显式判断
  • 产出:动态SQL四标签用法、参数拼接示例、foreach批量查询、SQL片段复用、错误速查

版本矩阵

数据项 数值/事实 来源 核查状态
MyBatis版本 3.5.16(2024年4月发布) MyBatis官方文档 ✅ 已核查
<if>标签 条件判断,test属性使用OGNL表达式 MyBatis官方 ✅ 已核查
<where>标签 自动处理WHERE关键字及多余AND/OR MyBatis官方 ✅ 已核查
<foreach>标签 循环拼接,支持collection/open/close/separator/item属性 MyBatis官方 ✅ 已核查
<sql>+<include> SQL片段定义与引用复用 MyBatis官方 ✅ 已核查
selectList方法 动态多条件查询,null值需显式判断 博客示例代码 ✅ 已核查
selectListByIdList 批量IN查询,@Param("idList")接收List参数 博客示例代码 ✅ 已核查
selectOneBySegment 使用sql/include片段复用 博客示例代码 ✅ 已核查

核心配置

动态 SQL

动态 SQL 是 MyBatis 的核心特性之一,它允许开发者根据不同的业务条件动态生成 SQL 语句。这一特性解决了复杂查询场景下 SQL 拼接的痛点,不仅提升了开发效率,也增强了代码的可读性与可维护性。

在实际开发中,业务逻辑往往复杂多变,导致 SQL 语句需要根据传入参数的不同而动态调整。MyBatis 的动态 SQL 机制正是为此而生。

动态 SQL 的用途

  • 灵活性:能够处理动态变化的查询条件,例如用户界面中的多条件筛选表单。
  • 避免冗余:通过动态语法将多个相似的 SQL 查询逻辑合并,减少代码重复。
  • 提高效率:仅在需要时生成对应的 SQL 片段,避免加载不必要的数据。

动态 SQL 的注意事项

null 值的处理

MyBatis 不会自动过滤 null 值,开发者需要在 <if> 等标签中显式进行非空判断。

复杂逻辑的可读性

过多的动态 SQL 逻辑会导致 XML 文件变得臃肿复杂,建议合理拆分,保持结构清晰。

性能问题

动态生成的 SQL 应尽量保持简洁,避免因过度复杂而影响数据库查询性能。

调试

建议开启 MyBatis 的日志功能,以便查看最终生成的 SQL 语句,确保其正确性。

参数拼接

在实际开发中,我们经常需要根据实体对象中不同字段的取值来动态构建查询条件。例如,当 ID 不为空时按 ID 查询,当用户名不为空时再加入用户名作为条件。这种多条件组合查询是动态 SQL 的典型应用场景。

xml 复制代码
<!-- 查询所有用户信息 -->
<select id="selectList" resultType="icu.wzk.model.UserInfo">
    SELECT
        *
    FROM
        user_info
    <where>
        <if test="username != null and username != ''">
            and username=#{username}
        </if>
        <if test="password != null and password != ''">
            and password=#{password}
        </if>
        <if test="age != null and age != ''">
            and age=#{age}
        </if>
    </where>
</select>

对应的 Mapper 接口如下图所示:

编写测试代码,复用之前的逻辑,并传入部分参数:

java 复制代码
public class WzkIcu05 {
    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
        UserInfo userInfo = UserInfo
                .builder()
                .username("wzk")
                .build();
        List<UserInfo> dataList = userInfoMapper.selectList(userInfo);
        dataList.forEach(System.out::println);
        sqlSession.close();
    }
}

执行后,控制台输出如下日志,可以看到 SQL 中只拼接了 username 条件:

shell 复制代码
24/11/11 15:59:59 DEBUG jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7f010382]
24/11/11 15:59:59 DEBUG UserInfoMapper.selectList: ==>  Preparing: SELECT * FROM user_info WHERE username=?
24/11/11 15:59:59 DEBUG UserInfoMapper.selectList: ==> Parameters: wzk(String)
24/11/11 15:59:59 DEBUG UserInfoMapper.selectList: <==      Total: 1
UserInfo(id=1, username=wzk, password=icu, age=18)

对应的运行截图如下:

循环拼接

当我们需要根据一个 ID 集合来批量查询数据时,可以使用 <foreach> 标签来实现循环拼接。

首先,在 UserInfoMapper 中添加一个新方法:

java 复制代码
List<UserInfo> selectListByIdList(@Param("idList") List<Integer> idList);

对应的接口截图如下:

接着,编写对应的 Mapper XML:

xml 复制代码
<select id="selectListByIdList" parameterType="icu.wzk.model.UserInfo" resultType="icu.wzk.model.UserInfo">
    SELECT
        *
    FROM
        user_info
    <where>
        id IN
        <foreach collection="idList" open="(" close=")" separator="," index="index" item="item">
            #{item}
        </foreach>
    </where>
</select>

对应的 XML 截图如下:

编写 Java 代码进行测试:

java 复制代码
public class WzkIcu06 {
    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
        List<Integer> idList = Arrays.asList(1, 2, 3);
        List<UserInfo> dataList = userInfoMapper.selectListByIdList(idList);
        dataList.forEach(System.out::println);
        sqlSession.close();
    }
}

当前数据库中的数据如下:

运行代码,控制台输出日志如下:

shell 复制代码
24/11/11 16:15:16 DEBUG jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6d2a209c]
24/11/11 16:15:16 DEBUG UserInfoMapper.selectListByIdList: ==>  Preparing: SELECT * FROM user_info WHERE id IN ( ? , ? , ? )
24/11/11 16:15:16 DEBUG UserInfoMapper.selectListByIdList: ==> Parameters: 1(Integer), 2(Integer), 3(Integer)
24/11/11 16:15:16 DEBUG UserInfoMapper.selectListByIdList: <==      Total: 3
UserInfo(id=1, username=wzk, password=icu, age=18)

对应的运行截图如下:

foreach 标签属性说明

  • collection:要遍历的集合元素,注意不要加 #{}
  • open:语句的开始部分。
  • close:语句的结束部分。
  • item:集合遍历时的每个元素,即生成的变量名。
  • separator:元素之间的分隔符。

片段抽取

在编写 SQL 时,经常会遇到重复的 SQL 片段(例如 SELECT * FROM user_info)。MyBatis 提供了 <sql> 标签来定义可复用的 SQL 片段,并通过 <include> 标签进行引用,从而实现 SQL 代码的复用。

首先,在 UserInfoMapper 接口中添加一个新方法:

java 复制代码
UserInfo selectOneBySegment(UserInfo userInfo);

对应的接口截图如下:

编写对应的 XML,使用 <sql> 定义公共片段,并用 <include> 引用:

xml 复制代码
<sql id="SELECT_USER_INFO">
    SELECT * FROM user_info
</sql>

<select id="selectOneBySegment" parameterType="icu.wzk.model.UserInfo" resultType="icu.wzk.model.UserInfo">
    <include refid="SELECT_USER_INFO"></include>
    <where>
        id=#{id}
    </where>
</select>

对应的 XML 截图如下:

编写测试代码:

java 复制代码
public class WzkIcu07 {
    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
        UserInfo userInfo = UserInfo
                .builder()
                .id(1L)
                .build();
        userInfo = userInfoMapper.selectOneBySegment(userInfo);
        System.out.println(userInfo);
        sqlSession.close();
    }
}

执行后,控制台输出结果如下:

shell 复制代码
24/11/11 17:04:08 DEBUG UserInfoMapper.selectOneBySegment: ==>  Preparing: SELECT * FROM user_info WHERE id=?
24/11/11 17:04:08 DEBUG UserInfoMapper.selectOneBySegment: ==> Parameters: 1(Long)
24/11/11 17:04:08 DEBUG UserInfoMapper.selectOneBySegment: <==      Total: 1
UserInfo(id=1, username=wzk, password=icu, age=18)

对应的运行截图如下:


错误速查卡

症状 根因 定位 修复
SQL中多了多余的AND/OR where子句不以AND开头或结尾 查看标签包裹的if语句 使用标签而非手动写WHERE,它会自动处理
foreach生成的SQL有语法错误 collection属性写成#{idList}而非idList 检查foreach标签collection属性 collection不加#{},直接写参数名
null值条件被拼接 if标签中未判断null 检查条件 显式判断:test="username != null and username != ''"
SQL片段引用失败 refid与sql id不匹配 检查和 确保两边id完全一致
foreach生成的IN语句多逗号 separator设置错误 检查 确保separator为逗号而非其他
selectList返回空列表 参数全部为null导致无查询条件 检查传入对象各字段是否为null MyBatis不会自动过滤null,需在if中显式判断

作者:武子康的个人博客

相关推荐
Kir1to2 小时前
RabbitMQ消息可靠性三板斧
后端
过期动态2 小时前
【LeetCode 热题 100】字母异位分组
java·算法·leetcode·职场和发展·哈希算法
辰海Coding2 小时前
MiniSpring框架学习-为什么一个请求访问 /helloworld,最后能调用到某个 Controller 方法?原始 MVC实现
java·学习·程序人生·spring·mvc
ServBay2 小时前
Google I/O 2026 Antigravity 更新与 SDK
后端·ai编程·google io
驭渊的小故事2 小时前
多线程01(线程状态和线程的sleep,线程终止(Interrupt)的小关联)
java·jvm·算法
山甫aa3 小时前
Java的包和import
java·开发语言
cpp_learner3 小时前
QT 窗体遮罩
后端
星轨zb3 小时前
JUC 到 Redis 分布式锁:一次关于高并发的性能压测实验
java·redis·分布式·jmeter
深蓝轨迹3 小时前
Java 集合框架超全解 · 底层源码|集合对比|HashMap 扩容原理
java·hashmap·集合框架·arraylist·linkedlist