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

0 阅读6分钟

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/ORMyBatis官方✅ 已核查
<foreach>标签循环拼接,支持collection/open/close/separator/item属性MyBatis官方✅ 已核查
<sql>+<include>SQL片段定义与引用复用MyBatis官方✅ 已核查
selectList方法动态多条件查询,null值需显式判断博客示例代码✅ 已核查
selectListByIdList批量IN查询,@Param("idList")接收List参数博客示例代码✅ 已核查
selectOneBySegment使用sql/include片段复用博客示例代码✅ 已核查

核心配置

MyBatis 3.5.16官方文档配置XML结构图 - configuration元素层级顺序

动态 SQL

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

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

动态 SQL 的用途

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

MyBatis动态SQL核心标签:if、choose、trim、foreach介绍

动态 SQL 的注意事项

null 值的处理

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

复杂逻辑的可读性

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

性能问题

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

调试

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

参数拼接

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

<!-- 查询所有用户信息 -->
<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 接口如下图所示: MyBatis selectList查询XML配置示例 - where与if标签动态拼接

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

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 条件:

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)

对应的运行截图如下: MyBatis selectList查询日志输出 - SQL预处理与参数绑定结果

循环拼接

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

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

List<UserInfo> selectListByIdList(@Param("idList") List<Integer> idList);

对应的接口截图如下: MyBatis selectListByIdList接口方法定义

接着,编写对应的 Mapper 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 截图如下: MyBatis foreach标签实现IN查询XML配置

编写 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();
    }
}

当前数据库中的数据如下: MySQL user_info表数据 - 四行示例数据id/username/password/age

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

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)

对应的运行截图如下: MyBatis foreach批量查询日志 - IN条件三个参数绑定与结果

foreach 标签属性说明

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

片段抽取

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

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

UserInfo selectOneBySegment(UserInfo userInfo);

对应的接口截图如下: MyBatis selectOneBySegment接口方法定义

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

<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 截图如下: MyBatis sql和include标签实现SQL片段复用

编写测试代码:

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();
    }
}

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

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)

对应的运行截图如下: MyBatis selectOneBySegment片段查询日志 - SQL片段复用执行结果


错误速查卡

症状根因定位修复
SQL中多了多余的AND/ORwhere子句不以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导致无查询条件检查传入对象各字段是否为nullMyBatis不会自动过滤null,需在if中显式判断

作者:武子康的个人博客