【JavaEE】MyBatis 基本用法 ⑤ ( 单条件查询 | 多条件查询 | 多条件动态查询 | 单条件动态查询 )

文章目录

前言


Mybatis 的数据查询需求 由浅入深 可以分为四种 :

  • 单条件查询 ,
  • 多条件查询 ,
  • 多条件动态条件查询 ,
  • 单条件动态条件查询 ;

本文以 mybatis-demo 项目中的 BrandMapper 接口与 BrandMapper.xml 映射文件为载体 , 依次实现这四种查询 , 并把过程中涉及的 参数传递 , 参数占位符 , 特殊字符处理 , 动态 SQL 标签等核心知识一并讲透 ;

四种查询方式一览 :

  • 单条件查询 : 条件固定只有一个 , 典型场景是根据 id 查看详情 , 核心知识点是参数占位符 ;
  • 多条件查询 : 多个条件用 and 组合查询 , 核心知识点是三种参数传递方式 ;
  • 多条件动态查询 : 条件可选可缺 , 核心知识点是 if 与 where 标签 ;
  • 单条件动态查询 : 多个条件只按一个查 , 核心知识点是 choose , when , otherwise 标签 ;

一、MyBatis 参数传递与单条件查询


单条件查询的典型需求是 : 根据数据库表的 id 字段 查询对应的 Brand 数据 , SQL 里的 id 显然不能写死 , 于是引出 MyBatis 的参数传递机制 ;

1、参数占位符 : #{} 与 ${}

在 Mapper 接口中定义 Brand selectById(int id); 接口函数 , 对应的 SQL 映射文件中需要使用 占位符 接收该 接口函数 ;

MyBatis 提供两种参数占位符 : 写法相似 , 含义完全不同 ;

  • #{} 预编译占位符 : 执行 SQL 时会先把 #{} 替换为 JDBC 的 ? 号 , 参数值随后通过 setXxx() 方法安全传入 , 底层走 PreparedStatement 预编译 , 可以防止 SQL 注入 , 绝大多数场景都使用它 ;
  • ${} 拼接占位符 : 直接把参数值当作字符串拼接进 SQL 再编译执行 , 存在 SQL 注入风险 , 仅用于动态表名 , 动态列名等 SQL 结构本身不固定的场景 ;

两者对比 :

对比项 #{} ${}
底层原理 替换为 ? 号 , 预编译后通过 setXxx 设置参数值 参数值原样拼接进 SQL 字符串后编译
SQL 注入 可以防止 存在风险
传参表现 值被当作一个完整的参数 , 不参与 SQL 语法 值直接参与 SQL 语法构成
使用场景 传递参数值 ( 绝大多数场景 ) 动态表名 , 动态列名 ( 如分表 tb_brand_2024 )

直观示例 : 假设要查询 id 为 1 的品牌 , 两种占位符在 SQL 中的写法与最终执行效果完全不同 :

xml 复制代码
<!-- #{} 预编译占位符 : 最终执行的是预编译 SQL , 参数安全传入 -->
<select id="selectById" resultMap="brandResultMap">
    select * from tb_brand where id = #{id};
</select>
<!-- 实际执行 : select * from tb_brand where id = ?  ( 参数 1 通过 setInt 传入 ) -->

<!-- ${} 拼接占位符 : 参数值直接拼进 SQL 字符串 -->
<select id="selectById" resultMap="brandResultMap">
    select * from tb_brand where id = ${id};
</select>
<!-- 实际执行 : select * from tb_brand where id = 1  ( 值原样拼接 , 无预编译 ) -->

核心区别一句话总结 :

  • #{} 是 " 占个坑 , 参数后补 " , 安全防注入 ;
  • ${} 是 " 直接填值 " , 灵活但有风险 , 只在 动态表名 / 列名等 SQL 结构不固定的场景才用 ;

2、parameterType 参数类型

<select> 标签的 parameterType 属性用于声明入参类型 , 例如 parameterType="int" ; 由于 MyBatis 能 根据接口方法签名自动推断参数类型 , 该属性可以省略不写 ;

3、特殊字符处理

SQL 中出现 < , > 这类字符时会与 XML 语法冲突 , < 是标签起始符 , 直接书写会破坏 XML 结构导致解析报错 ; 解决方式有两种 :

特殊字符的两种处理方式 :

  • 转义字符 : 用 XML 实体替代 , &lt; 代替 < , &gt; 代替 > , &amp; 代替 & , 例如 where ordered &lt; 15 ;
  • CDATA 区 : 用 <![CDATA[ ... ]]> 包住含特殊字符的 SQL 片段 , 区内内容 XML 不做解析 , 原样保留 ;
xml 复制代码
<!-- 转义字符示例 : 查询排序值小于 15 的品牌 -->
<select id="selectByOrdered" resultMap="brandResultMap">
    select * from tb_brand where ordered &lt; #{ordered};
</select>

<!-- CDATA 区示例 : 效果等价 , 写法更直观 -->
<select id="selectByOrdered" resultMap="brandResultMap">
    select * from tb_brand where <![CDATA[ ordered < ]]> #{ordered};
</select>

说明 : 只有 < 存在语法冲突必须处理 , > 可以直接书写 , 习惯上会一并处理 ;

二、单条件查询案例实现


有了 参数占位符 的知识铺垫 , 根据 id 查询品牌就可以按 " 接口方法 + SQL 映射 + 测试验证 " 三步落地 ;

1、第一步 : Mapper 接口定义方法

在 BrandMapper.java 接口中 , 定义 带参数的 接口函数 ;

java 复制代码
    /**
     * 查看详情:根据Id查询
     * <p>
     * 【参数传递原理】
     *   方法只有一个参数时 , SQL 中直接用 #{id} 即可取到该参数的值:
     *       MyBatis 会把参数值传入 SQL , 将 #{id} 替换为占位符 ?  , 
     *       再通过 PreparedStatement.setInt(1, id) 安全设置参数(防 SQL 注入)。
     *   说明:#{ } 里面写什么名字都可以(如 #{abc}) , 因为只有一个参数 , 
     *   MyBatis 早期版本不关心占位符名称 , 只按位置传值;
     *   但为了可读性 , 建议与方法参数名保持一致。
     * <p>
     * 【为什么不用 parameterType 属性?】
     *   <select> 标签的 parameterType 属性用于声明入参类型(如 int、String) , 
     *   MyBatis 可以根据接口方法签名自动推断出来 , 所以该属性可以省略不写。
     * <p>
     * 返回值 Brand:查询到的单个品牌对象;id 不存在时返回 null。
     */
    Brand selectById(int id);

接口方法的两个要点 :

  • 返回值 Brand : 主键查询最多命中一行 , 封装成单个 Brand 对象返回 , id 不存在时返回 null ;
  • 绑定规则 : XML 中 <mapper namespace> 必须是接口全限定名 org.example.mapper.BrandMapper , <select> 的 id 必须与方法名 selectById 完全一致 ;

2、第二步 : SQL 映射文件编写 SQL

在 BrandMapper.xml 的 SQL 映射文件中 , 定义 " Brand selectById(int id) " 接口函数 对应的 SQL 脚本 ;

xml 复制代码
    <!--
    ======================================================================
    【本条 SQL 涉及的核心知识点:参数占位符】
    ======================================================================
    MyBatis 中有两种参数占位符:

    1. #{ }(预编译占位符 ------ 推荐)
       * 底层使用 PreparedStatement , #{ } 会先被替换为 JDBC 的 ? 占位符 , 
         参数值后续通过 setXxx() 方法安全传入;
       * 传入的值会被当作「一个完整的参数值」处理 , 不参与 SQL 语法拼接 , 
         因此可以【防止 SQL 注入】(如传入 "1 or '1'='1'" 也只会被当成普通字符串)。

    2. ${ }(字符串拼接占位符)
       * 直接把参数值原样【拼接到 SQL 语句】中再编译执行;
       * 存在【SQL 注入】风险 , 除非特殊场景否则不要用。

    3. 使用时机:
       * 传递参数值的时候:用 #{ }(安全 , 本条 SQL 采用);
       * 表名或列名(SQL 结构本身)不固定的时候:只能用 ${ }
         (因为表名/列名不能作为 ? 占位符参数传入 , 如动态分表 tb_brand_2024) , 
         但要自己做好参数校验防注入。

    【参数类型 parameterType】
        <select> 标签的 parameterType 属性用于声明入参类型(如 int、String) , 
        MyBatis 能根据接口方法签名自动推断 , 所以该属性【可以省略】不写。

    【特殊字符处理】(SQL 中出现 < 、> 等特殊字符时使用)
        XML 文件中 < 是标签起始符 , 直接写 < 会破坏 XML 结构导致解析报错 , 两种解决办法:
        1. 转义字符:用 XML 实体替代 , 如 &lt;(<)、&gt;(>)、&amp;(&)
           例:where id &lt; 10  →  实际 SQL:where id < 10
        2. CDATA 区:<![CDATA[ ... ]]> 包住含特殊字符的 SQL 片段 , 
           区内内容 XML 不做解析、原样保留 , 写什么符号都可以
           例:<![CDATA[ where id < 10 ]]>
        说明:只有 < 有语法冲突必须处理;> 可以直接写 , 但习惯上一并处理。
    ======================================================================
    -->
    <!--
        查看详情:根据 Id 查询
        id            :必须与 BrandMapper 接口方法名  完全一致(绑定依据)
        parameterType :入参类型为 int , 可省略(已省略) , MyBatis 自动推断
        resultMap     :沿用上面的 brandResultMap , 解决 brand_name/company_name
                        列名与属性名不一致的封装问题
        #{id}         :取出接口方法传入的参数值 , 替换为 ? 预编译执行 , 防 SQL 注入
        执行结果      :一行数据封装成一个 BselectByIdrand 对象返回;id 不存在时返回 null
    -->
    <select id="selectById" resultMap="brandResultMap">
        select *
        from tb_brand where id = #{id};
    </select>

SQL 的取参原理 : 方法只有一个参数时 , MyBatis 把参数值直接传入 SQL , 将 #{id} 替换为 ? 后通过 PreparedStatement.setInt() 安全设置 , 此时占位符里写什么名字都能取到值 ( 例如 #{abc} ) , 但为了可读性建议与方法形参同名 ;

3、第三步 : 测试类验证

编写单元测试方法 , 调用上述 BrandMapper 接口中的 selectById 接口函数 ;

java 复制代码
    /**
     * 测试:查看详情(根据 Id 查询)
     */
    @Test
    public void testSelectById() throws IOException {

        //接收参数
        int id = 1;

        //1. 加载核心配置文件 , 获取 SqlSessionFactory
        String resource = "mybatis-config-mapper.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2. 获取 SqlSession 对象
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3. 获取 Mapper 接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4. 执行方法 , 打印结果
        Brand brand = brandMapper.selectById(id);
        System.out.println(brand);

        //5. 释放资源
        sqlSession.close();

    }

固定套路共 5 步 : 加载配置 , 获取 SqlSession , 获取代理对象 , 执行方法 , 释放资源 , 后续所有查询都复用这套流程 ;

执行结果 : 查询的 id = 1 的数据对象是 Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1} ;

dart 复制代码
[DEBUG] 20:56:39.434 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection 
[DEBUG] 20:56:39.686 [main] o.a.i.d.p.PooledDataSource - Created connection 959869407. 
[DEBUG] 20:56:39.686 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df] 
[DEBUG] 20:56:39.689 [main] o.e.m.B.selectById - ==>  Preparing: select * from tb_brand where id = ?; 
[DEBUG] 20:56:39.714 [main] o.e.m.B.selectById - ==> Parameters: 1(Integer) 
[DEBUG] 20:56:39.724 [main] o.e.m.B.selectById - <==      Total: 1 
Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}
[DEBUG] 20:56:39.725 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df] 
[DEBUG] 20:56:39.725 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df] 
[DEBUG] 20:56:39.726 [main] o.a.i.d.p.PooledDataSource - Returned connection 959869407 to pool. 

进程已结束 , 退出代码为 0

三、多条件查询


同时按照 " 当前状态 , 企业名称 , 品牌名称 " 组合筛选 Brand 数据 , 这就是多条件查询 ;

它涉及两个新问题 : 条件表达式怎么写 , 多个参数怎么传 ;

1、条件表达式的编写与连接

常用的条件表达式 :

  • 等值比较 : 用 = 精确匹配 , 例如 status = #{status} ;
  • 模糊匹配 : 用 like 配合 % 通配符 , 例如 company_name like #{companyName} , % 属于参数值的一部分 , 要在调用方拼好再传入 ;
  • 条件连接 : 多个条件用 and 连接表示 " 且 " , 用 or 连接表示 " 或 " , 例如 where status = #{status} and company_name like #{companyName} ;

2、三种参数传递方式

接口方法有多个参数时 , MyBatis 不会自动命名 , SQL 中的 #{xxx} 要能取到值 , 必须保证 xxx 与传入参数的名字对得上 , 由此产生三种传参方式 :

java 复制代码
// 方式一 : 散装参数 , 每个参数前用 @Param 绑定占位符名称
List<Brand> selectByCondition(@Param("status") int status,
                              @Param("companyName") String companyName,
                              @Param("brandName") String brandName);

// 方式二 : 对象参数 , 占位符名称与实体类属性名对应
List<Brand> selectByCondition(Brand brand);

// 方式三 : Map 集合参数 , 占位符名称与 Map 的 key 对应
List<Brand> selectByCondition(Map map);

三种方式对比 :

对比项 散装参数 对象参数 Map 集合参数
对齐要求 @Param 名称与占位符一致 属性名与占位符一致 key 与占位符一致
类型约束 有 , 编译期即可检查 有 , 编译期即可检查 无 , 写错 key 不报错只查空
优点 参数含义一目了然 结构清晰 , 符合面向对象习惯 灵活 , 不依赖实体类
缺点 参数多时方法签名冗长 条件与实体字段强绑定 键名靠手写 , 容易拼错
使用场景 参数少且固定 条件与实体属性基本对应 多表查询等无现成实体的场景

四、多条件查询案例实现


多条件查询沿用 " Mapper 接口 + SQL 映射 XML + 测试 " 三步走 , 本案例启用 Map 集合参数方式 , 另两种方式注释保留 ;

1、Mapper 接口

一个重要限制 : MyBatis 接口不支持方法重载 , SQL 定位靠 " namespace + 方法名 " , 重载方法名相同会导致 XML 中 id 冲突 , 所以 三种传参方式同一时刻只能启用一种 , 其余注释备查 ;

在 BrandMapper.java 中定义如下 接口函数 :

java 复制代码
    /**
     * 条件查询:按「状态 status + 公司名称 companyName + 品牌名称 brandName」多条件查询
     * <p>
     * 【多参数接收的三种方式】
     *   MyBatis 对多个参数不会自动命名 , SQL 中 #{xxx} 要能取到值 , 
     *   必须保证「xxx 与传入参数的名字对得上」 , 三种方式对号入座:
     *   1. 散装参数:方法有多个参数时 , 每个参数前必须加 @Param("xxx") 注解 , 
     *      括号里的名称与 SQL 占位符 #{xxx} 一致
     *      (需导入 org.apache.ibatis.annotations.Param);
     *   2. 对象参数:直接传 Brand 对象 , #{xxx} 的 xxx 与对象的【属性名】一致即可取值;
     *   3. Map 集合参数:传入 Map , #{xxx} 的 xxx 与 Map 的【key】一致即可取值。
     * <p>
     * 【模糊查询 like 的传参约定】
     *   % 通配符属于「参数值的一部分」 , #{} 只做安全占位、不会自动拼通配符 , 
     *   所以调用方需先把参数处理成 "%关键字%" 再传入(见测试类 testSelectByCondition)。
     * <p>
     * 【为何另外两种方式注释保留?】
     *   Mapper 接口【不支持方法重载】:SQL 定位靠「namespace + 方法名(id)」 , 
     *   重载方法名相同会导致 XML 中的 id 冲突 , 因此同一时刻只能启用一种 , 其余注释备查。
     * <p>
     * 返回值 List<Brand>:符合条件的品牌集合;查不到时返回空集合(不是 null)。
     */
    // 方式一:散装参数 ------ 多参数时必须用 @Param("占位符名称") 逐个声明映射关系
    //List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName, @Param("brandName") String brandName);

    // 方式二:对象参数 ------ 对象的属性名要和参数占位符名称一致
    //List<Brand> selectByCondition(Brand brand);

    // 方式三:Map 集合参数 ------ Map 的 key 要和参数占位符名称一致(当前采用)
    List<Brand> selectByCondition(Map map);

2、SQL 映射文件

在 SQL 映射文件 BrandMapper.xml 中 , 定义 List<Brand> selectByCondition(Map map) 接口函数对应的 SQL 脚本 ;

在 BrandMapper.java 接口中 , 定义不同的参数传入方式 , 散装参数、对象参数 、Map 参数 对应的 SQL 映射脚本是相同的 ;

xml 复制代码
    <!--
        条件查询(多条件:状态 + 公司名称 + 品牌名称)
        id        :必须与 BrandMapper 接口方法名 selectByCondition 完全一致(绑定依据)
        resultMap :沿用 brandResultMap 封装结果
        参数取值  :#{xxx} 中的 xxx 按接口的传参方式自动匹配 ------
                    ① 散装参数 → 匹配 @Param("xxx") 指定的名称
                    ② 对象参数 → 匹配对象的【属性名】xxx
                    ③ Map 参数  → 匹配 Map 的【key】xxx
                    本条 SQL 对三种传参方式通用 , 切换传参方式无需改动 SQL
        like 说明 :% 通配符由调用方拼进参数值中传入(如 "%华为%") , 
                    #{} 只负责安全占位 , 不会自动拼通配符
    -->
    <select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
        where status = #{status}
          and company_name like #{companyName}
          and brand_name like #{brandName}
    </select>

3、测试类验证

定义 单元测试类 , 测试上述 BrandMapper.java 接口中的 List<Brand> selectByCondition(Map map) 接口函数 ;

java 复制代码
@Test
public void testSelectByCondition() throws IOException {
    // 接收参数
    int status = 1;
    String companyName = "华为";
    String brandName = "华为";
    // 处理参数 : like 模糊查询需拼接 % 通配符
    companyName = "%" + companyName + "%";
    brandName = "%" + brandName + "%";
    // 封装 Map 参数 : key 必须与 SQL 占位符名称一致
    Map map = new HashMap();
    map.put("status", status);
    map.put("companyName", companyName);
    map.put("brandName", brandName);
    // 中间的加载配置 , 获取 SqlSession , 获取代理对象三步与单条件查询完全相同 , 此处省略
    // 执行方法
    List<Brand> brands = brandMapper.selectByCondition(map);
    System.out.println(brands);
}

测试中隐藏着一个坑 : 静态多条件查询的三个条件必须全部传值 , 如果 Map 中少放一个 key , 该条件按 null 处理 , 例如 status = null 恒不成立 , 整个查询将返回空列表 ; 这一问题正是动态 SQL 要解决的 ;

执行结果 :

  • 生成的 SQL 语句 : 是 select * from tb_brand where status = ? and company_name like ? and brand_name like ? ;
  • SQL 语句对应的 3 个参数 : 是 1(Integer), %华为%(String), %华为%(String) ;
  • 查询到的 Brand 集合结果 : [Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}] ;

输出内容 :

dart 复制代码
[DEBUG] 21:06:03.484 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection 
[DEBUG] 21:06:03.725 [main] o.a.i.d.p.PooledDataSource - Created connection 1449263511. 
[DEBUG] 21:06:03.725 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197] 
[DEBUG] 21:06:03.729 [main] o.e.m.B.selectByCondition - ==>  Preparing: select * from tb_brand where status = ? and company_name like ? and brand_name like ? 
[DEBUG] 21:06:03.755 [main] o.e.m.B.selectByCondition - ==> Parameters: 1(Integer), %华为%(String), %华为%(String) 
[DEBUG] 21:06:03.767 [main] o.e.m.B.selectByCondition - <==      Total: 1 
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}]
[DEBUG] 21:06:03.768 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197] 
[DEBUG] 21:06:03.768 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197] 
[DEBUG] 21:06:03.769 [main] o.a.i.d.p.PooledDataSource - Returned connection 1449263511 to pool. 

进程已结束 , 退出代码为 0

五、多条件动态查询


实际业务中用户往往只填部分搜索条件 , 甚至什么都不填 ;

而 SQL 是写死的 , 条件全用 and 硬连接 , 少传一个条件就查不到数据 , 条件缺失时还会拼出 where and ... 这样的语法错误 ;

上一条 selectByCondition 用 and 硬连接三个条件 , 三个条件必须【全部传值】 , 缺任何一个(Map 中少放一个 key)都查不到数据 , 实际业务中用户往往只填部分搜索条件 → 需要「条件按需拼接」 , 用到动态 SQL 标签:

SQL 语句随用户输入的变化而变化 , 这就是动态 SQL ;

MyBatis 为此提供了 if , choose ( when , otherwise ) , trim ( where , set ) , foreach 等标签 ;

1、if 标签 : 条件判断

<if> 的 test 属性书写逻辑表达式 ( OGNL 表达式 ) , 表达式成立才把标签体内的 SQL 拼接进去 ; 判空惯例是 != null 防空指针 , 字符串再加 != '' 防空字符串 ;

2、if 的弊端与恒等式方案

if 标签存在一个经典问题 : 所有条件都以 and 开头时 , 若第一个条件未拼接 , SQL 就变成 where and company_name like ... , 直接语法报错 ; 传统解法是加恒等式 :

xml 复制代码
<!-- 恒等式方案 : 1 = 1 恒成立 , 保证 where 后面永远有内容 , 每个条件统一以 and 开头 -->
select *
from tb_brand
where 1 = 1
<if test="status != null">
    and status = #{status}
</if>
<if test="companyName != null and companyName != ''">
    and company_name like #{companyName}
</if>

恒等式方案能跑通 , 但 1 = 1 与业务无关 , 可读性差 , 属于写死的小技巧 ;

3、where 标签 : 推荐方案

MyBatis 提供 <where> 标签替换 where 关键字 , 它自动做两件事 :

where 标签的两个自动行为 :

  • 按需生成 : 内部条件全不成立时不生成 WHERE 子句 , 不会出现空 where 报错 ;
  • 剔除符号 : 自动去掉条件开头多余的 and 或 or , 所以 if 内部习惯统一写 and 开头 ;
xml 复制代码
    <!--
    1. <if>:条件判断标签
       * test 属性:写逻辑表达式(OGNL 表达式) , 为 true 才把标签体内的 SQL 拼进去;
       * 判空惯例:!= null 防空指针;字符串再加 != '' 防空字符串
         (Map 传参时 test 里的名字就是 Map 的 key)。

    2. <where>:替换 where 关键字 , 解决两个尴尬问题:
       * 所有 <if> 都不成立时 → 不生成 WHERE 子句(不会出现空的 where 报错);
       * 拼上的第一个条件自带 and → 自动【去除开头多余的 and / or】
         (所以 <if> 里习惯上都写成 and 开头 , 让 <where> 统一处理)。
    ======================================================================
    -->
    <!--
        动态条件查询(条件按需拼接)
        id        :必须与 BrandMapper 接口方法名 selectByConditionDynamic 完全一致(绑定依据)
        resultMap :沿用 brandResultMap 封装结果
        <if test> :Map 中该 key 存在且非空 , 才拼接对应的 and 条件
        <where>   :自动处理 where 关键字、去掉开头多余 and、条件全空时不生成 WHERE

        【恒等式方案】(传统写法 , 已被 <where> 标签取代 , 注释备查、暂不启用):
        不用 <where> 标签时有两个语法问题:条件全不成立 → where 后面没有内容 , SQL 报错;
        第一个条件以 and 开头 → where and status = ... 语法错误。
        传统解决办法是加恒等式 where 1 = 1(1=1 恒成立 , 保证 where 后永远有内容 , 
        后续条件全用 and 连接就不会开头报错):

            select *
            from tb_brand
            where 1 = 1
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="companyName != null and companyName != ''">
                and company_name like #{companyName}
            </if>
            <if test="brandName != null and brandName != ''">
                and brand_name like #{brandName}
            </if>

        * 缺点:1=1 与业务无关、可读性差 , 属于写死的小技巧 → 推荐用 <where> 标签。
    -->
    <select id="selectByConditionDynamic" resultMap="brandResultMap">
        select *
        from tb_brand
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="companyName != null and companyName != ''">
                and company_name like #{companyName}
            </if>
            <if test="brandName != null and brandName != ''">
                and brand_name like #{brandName}
            </if>
        </where>
    </select>

测试时只需控制 Map 中放哪些 key : 放了 key 的条件参与拼接 , 注释掉 map.put("brandName", brandName) 这一行 , 品牌名称条件就不会出现在 SQL 中 , 动态效果一目了然 ;

4、Mapper 接口

多条件动态查询 与 多条件查询 的 Mapper 接口定义是相同的 , 其 区别是在 SQL 映射脚本 不同 ;

Mapper 接口函数参考 :

java 复制代码
    /**
     * 动态条件查询:条件可选,Map 里放了哪个 key 就按哪个条件查(配合 XML 中 <where>/<if> 标签)
     * <p>
     * 【与 selectByCondition 的区别】
     *   静态版 selectByCondition 的三个条件必须【全部传值】(and 连接,缺一个就查不到数据);
     *   实际业务中用户往往只填部分搜索条件,本方法通过动态 SQL 实现「条件按需拼接」:
     *   放了 key 的条件拼进 SQL,没放的 key 对应条件直接跳过。
     * <p>
     * 【XML 端配合的两个标签】(见 BrandMapper.xml 中 selectByConditionDynamic)
     *   1. <if test="...">:条件判断标签,test 里写逻辑表达式(OGNL),
     *      表达式成立才把该段 SQL 拼进去;判空惯例:!= null,字符串再加 != '';
     *   2. <where>:替代 where 关键字 ------ 内部条件全不成立时不生成 WHERE 子句;
     *      并自动去掉条件开头多余的 and / or。
     * <p>
     * 返回值 List<Brand>:符合条件的品牌集合;一个条件都没传时等价于查询全部。
     */
    List<Brand> selectByConditionDynamic(Map map);

5、测试方法

定义 单元测试方法 , 调用上面 Mapper 接口函数 ;

java 复制代码
   /**
     * 测试:动态条件查询(放哪个 key 就按哪个条件查)
     */
    @Test
    public void testSelectByConditionDynamic() throws IOException {

        //接收参数
        int status = 1;
        String companyName = "华为";
        String brandName = "华为";

        //处理参数:like 模糊查询需拼接 % 通配符
        companyName = "%" + companyName + "%";
        brandName = "%" + brandName + "%";

        //封装 Map 参数:放哪个 key 就拼哪个条件(不放的 key 对应条件不参与查询)
        Map map = new HashMap();
        map.put("status", status);
        //map.put("companyName", companyName);
        //map.put("brandName", brandName);  // 注释掉:演示「品牌名称」条件不拼接

        //1. 加载核心配置文件,获取 SqlSessionFactory
        String resource = "mybatis-config-mapper.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2. 获取 SqlSession 对象
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3. 获取 Mapper 接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4. 执行方法
        List<Brand> brands = brandMapper.selectByConditionDynamic(map);
        System.out.println(brands);

        //5. 释放资源
        sqlSession.close();

    }

执行结果 :

  • SQL 脚本 : 拼接的 SQL 语句是 select * from tb_brand WHERE status = ? ;
  • SQL 参数 : 1(Integer) ;
  • 最终的查询结果 : [Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}, Brand{id=2, brandName='小米', companyName='小米科技有限公司', ordered=50, description='小米手机,为发烧而生', status=1}] ;
dart 复制代码
[DEBUG] 22:15:55.880 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection 
[DEBUG] 22:15:56.101 [main] o.a.i.d.p.PooledDataSource - Created connection 2114444063. 
[DEBUG] 22:15:56.102 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f] 
[DEBUG] 22:15:56.106 [main] o.e.m.B.selectByConditionDynamic - ==>  Preparing: select * from tb_brand WHERE status = ? 
[DEBUG] 22:15:56.128 [main] o.e.m.B.selectByConditionDynamic - ==> Parameters: 1(Integer) 
[DEBUG] 22:15:56.142 [main] o.e.m.B.selectByConditionDynamic - <==      Total: 2 
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}, Brand{id=2, brandName='小米', companyName='小米科技有限公司', ordered=50, description='小米手机,为发烧而生', status=1}]
[DEBUG] 22:15:56.142 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f] 
[DEBUG] 22:15:56.142 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f] 
[DEBUG] 22:15:56.143 [main] o.a.i.d.p.PooledDataSource - Returned connection 2114444063 to pool. 

进程已结束,退出代码为 0

六、单条件动态查询


另一类场景是 " 从多个条件中选择一个 " : 数据的 搜索类型 是单选 , 选中状态就只按状态查 , 选中企业名称就只按企业名称查 , 什么都不选就查全部 ;

这类似 Java 的 switch 语句 , MyBatis 提供 choose 标签族 ;

1、choose , when , otherwise 标签

三个标签与 Java 语法的对应关系 :

  • choose 标签 : 相当于 switch , 内部从上到下判断 , 只命中第一个成立的 when 分支 , 多选一 ;
  • when 标签 : 相当于 case , test 属性成立则拼接该段 SQL , 并结束整个 choose ;
  • otherwise 标签 : 相当于 default , 所有 when 都不成立时的兜底分支 , 可以省略 ;

2、Mapper 接口函数

单条件 动态查询 接口方法 定义 :

java 复制代码
    /**
     * 单条件(单选)动态查询:多个条件【只按一个】查 ------ 从上到下命中第一个非空条件就不再往下看
     * <p>
     * 【与 selectByConditionDynamic 的区别】
     *   动态多条件版:满足 test 的条件【全部拼接】(多个 and 组合查询);
     *   本方法:只拼一个条件,类似 Java 的 switch(多选一)。
     * <p>
     * 【XML 端核心标签】(见 BrandMapper.xml 中 selectByConditionSingleDynamic)
     *   1. <choose>:相当于 switch,内部从上到下【只命中一个】分支;
     *   2. <when test="...">:相当于 case ------ test 成立则拼接该段 SQL 并结束整个 choose;
     *   3. <otherwise>:相当于 default ------ 所有 when 都不成立时的兜底分支(可选)。
     *   外层套 <where>:条件以列名开头(无 and),<where> 在有内容时才生成 where 关键字。
     * <p>
     * 【参数 Brand:对象参数方式】
     *   #{xxx} 按属性名取值;未赋值的属性为 null,对应 <when> 的 test 不成立、条件被跳过,
     *   因此「给哪个属性赋值,就按哪个条件查」。
     * <p>
     * 返回值 List<Brand>:符合条件的品牌集合;什么都没赋值时查询全部。
     */
    List<Brand> selectByConditionSingleDynamic(Brand brand);

3、SQL 映射文件

SQL 映射脚本 配置 , 主要是使用 <choose> , <when> , <otherwise> 标签 根据条件 在多个 SQL 选择语句中筛选出第一个符合条件的语句 ;

xml 复制代码
    <!--
    ======================================================================
    【choose/when/otherwise 标签:单选分支】
    ======================================================================
    * <choose>   :相当于 switch ------ 内部从上到下按顺序判断,【只命中第一个】
                  成立的 <when> 分支,拼完即结束整个 choose(多选一);
                  与 <if> 的区别:<if> 是「满足就拼、可拼多个」,<choose> 是「只拼一个」。
    * <when>     :相当于 case ------ test 为 true 则拼接该段 SQL。
    * <otherwise>:相当于 default ------ 所有 <when> 都不成立时的兜底分支(可省略)。
    ======================================================================
    -->
    <!--
        单条件(单选)动态查询 ------ 手动 where + otherwise 兜底 1=1 写法(未启用,仅供对比学习)
        手动写 where 关键字时,若所有 when 都不成立,where 后面就空了 → SQL 语法错误,
        所以用 <otherwise> 拼 1 = 1 兜底,保证 where 后面永远有内容:

        <select id="selectByConditionSingleDynamic" resultMap="brandResultMap">
            select *
            from tb_brand
            where
            <choose>(相当于 switch)
                <when test="status != null">(相当于 case)
                    status = #{status}
                </when>
                <when test="companyName != null and companyName != ''">(相当于 case)
                    company_name like #{companyName}
                </when>
                <when test="brandName != null and brandName != ''">(相当于 case)
                    brand_name like #{brandName}
                </when>
                <otherwise>
                    1 = 1
                </otherwise>
            </choose>
        </select>

        * 缺点:where 与 1=1 的语法正确性要自己保证,可读性差
          → 推荐下面用 <where> 标签(有内容才生成 where,无需 otherwise 兜底)。
    -->
    <!--
        单条件(单选)动态查询(启用)
        id        :必须与 BrandMapper 接口方法名 selectByConditionSingleDynamic 完全一致(绑定依据)
        resultMap :沿用 brandResultMap 封装结果
        参数      :Brand 对象参数,#{xxx} 按属性名取值,未赋值的属性为 null
        <choose>  :只命中第一个 test 成立的 <when>,实现「多选一」
        <where>   :有内容才生成 where 关键字(条件以列名开头、无 and,直接正常拼出)
    -->
    <select id="selectByConditionSingleDynamic" resultMap="brandResultMap">
        select *
        from tb_brand
        <where>
            <choose><!--相当于switch-->
                <when test="status != null"><!--相当于case-->
                    status = #{status}
                </when>
                <when test="companyName != null and companyName != ''"><!--相当于case-->
                    company_name like #{companyName}
                </when>
                <when test="brandName != null and brandName != ''"><!--相当于case-->
                    brand_name like #{brandName}
                </when>
            </choose>
        </where>
    </select>

不套 <where> 标签的写法需要手动写 where 关键字 , 此时若所有 when 都不成立 , where 后面就空了 , 必须靠 <otherwise> 拼 1 = 1 兜底 ; 套上 <where> 标签后有内容才生成 where 关键字 , 无需兜底 , 写法更简洁 ;

4、测试类验证

编写 单元测试类 , 调用 Mapper 接口方法 List<Brand> selectByConditionSingleDynamic(Brand brand); ;

java 复制代码
    /**
     * 测试:单条件动态查询(多选一,只按第一个非空属性查)
     */
    @Test
    public void testSelectByConditionSingleDynamic() throws IOException {

        //接收参数
        int status = 1;
        String companyName = "华为";
        String brandName = "华为";

        //处理参数:like 模糊查询需拼接 % 通配符
        companyName = "%" + companyName + "%";
        brandName = "%" + brandName + "%";

        //封装对象:只给 companyName 赋值,status/brandName 为 null → choose 只命中公司名称分支
        Brand brand = new Brand();
        //brand.setStatus(status);
        brand.setCompanyName(companyName);
        //brand.setBrandName(brandName);

        //1. 加载核心配置文件,获取 SqlSessionFactory
        String resource = "mybatis-config-mapper.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        //2. 获取 SqlSession 对象
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //3. 获取 Mapper 接口的代理对象
        BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);

        //4. 执行方法
        List<Brand> brands = brandMapper.selectByConditionSingleDynamic(brand);
        System.out.println(brands);

        //5. 释放资源
        sqlSession.close();

    }

对象参数方式下 , " 给哪个属性赋值就按哪个条件查 " , 未赋值的属性为 null , 对应 when 的 test 不成立被跳过 ; 什么属性都不赋值时等价于查询全部 ;

执行结果 :

  • SQL 脚本 : select * from tb_brand WHERE company_name like ?
  • SQL 参数 : %华为%(String)
  • 查询结果 : [Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}]
dart 复制代码
[DEBUG] 22:34:51.519 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection 
[DEBUG] 22:34:51.727 [main] o.a.i.d.p.PooledDataSource - Created connection 371800738. 
[DEBUG] 22:34:51.728 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2] 
[DEBUG] 22:34:51.731 [main] o.e.m.B.selectByConditionSingleDynamic - ==>  Preparing: select * from tb_brand WHERE company_name like ? 
[DEBUG] 22:34:51.754 [main] o.e.m.B.selectByConditionSingleDynamic - ==> Parameters: %华为%(String) 
[DEBUG] 22:34:51.771 [main] o.e.m.B.selectByConditionSingleDynamic - <==      Total: 1 
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}]
[DEBUG] 22:34:51.772 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2] 
[DEBUG] 22:34:51.772 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2] 
[DEBUG] 22:34:51.772 [main] o.a.i.d.p.PooledDataSource - Returned connection 371800738 to pool. 

进程已结束,退出代码为 0

七、注意事项与避坑


实现四种查询过程中容易踩的坑 , 集中整理如下 :

  • 名称必须严格对齐 : @Param 名称 , 对象属性名 , Map 的 key 三者 必须与 SQL 占位符 #{xxx} 的 xxx 完全一致 , 写错不会报错 , 只会查到 null 或空结果 ;
  • Map 少放 key 的隐性 bug : 静态多条件查询少放一个 key , 对应条件按 null 参与比较 , 例如 status = null 恒不成立 , 整个查询返回空列表 , 且无任何异常提示 ;
  • like 的 % 通配符 : % 属于参数值的一部分 , #{} 不会自动拼接 , 需在调用方写成 "%" + name + "%" , 或在 SQL 中用 concat 函数处理 ;
  • 接口不支持重载 : 同一个 Mapper 接口中同名方法只能保留一个 , 否则 XML 的 id 冲突 , 运行时报 Invalid bound statement ;
  • 字符串判空双条件 : <if test> 中字符串类型要同时写 != null and != '' , 只判 null 时空字符串仍会参与拼接 ;
  • XML 注释的限制 : XML 注释内部不能出现连续两个短横线 , 也不能嵌套注释 , 给注释中的标签做说明时改用普通文字 ;
  • 配置文件的选择 : Mapper 代理方式的测试统一加载 mybatis-config-mapper.xml , 它以包扫描方式加载 org.example.mapper 包下的接口与映射文件 , 二者必须同包同名 ;

总结


本文围绕品牌案例完整实现了四种条件查询 :

  • 单条件查询 掌握了 #{} 与 ${} 两种参数占位符 以及 parameterType , 特殊字符处理等基础知识 ;
  • 多条件查询 掌握了散装参数 , 对象参数 , Map 集合三种传参方式及其取舍 ;
  • 多条件动态查询 用 if 与 where 标签解决了 " 条件可选 " 的问题 ;
  • 单条件动态查询 用 choose , when , otherwise 标签解决了 " 多选一 " 的问题 ;

动态 SQL 的本质是让 SQL 语句 跟随参数变化 按需拼接 , 掌握了这四种姿势 , 就能覆盖绝大多数列表页与详情页的查询开发场景 ;

相关推荐
开开心心就好1 小时前
二维码批量生成导出工具,离线可用完全免费
java·前端·人工智能·智能手机·github·excel·visual studio
wangbing11251 小时前
开发指南147-WebSocket-前后关联关系
java
Wx-bishekaifayuan1 小时前
springboot陨石鉴收系统96265-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·spring·课程设计
code_slave(码畜)2 小时前
微服务架构落地:基础服务 —— 报表服务(上篇:定位、边界与整体架构)
java·spring boot·spring cloud·微服务·架构
不灭的黄金瞳1232 小时前
Java实现简易图书管理系统
java·intellij-idea
重生之小比特2 小时前
【C++进阶】02 Linux基本指令
java·数据库·c++
Wang's Blog2 小时前
Java 项目实战: 外卖平台优化-Nginx常用命令与环境变量配置
java·网络·nginx
sp42a2 小时前
Java 加解密组件再设计
java
FYKJ_20102 小时前
springboot家政服务平台66766-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·mysql·spark·课程设计