【MyBatis详解】——从Xml配置解析到SQL执行过程,mybatis动态更新xml文件后热部署,不重启应用的方法

整体架构与使用Demo

Mybatis的架构整体可以分为3层:
在这里插入图片描述

  1. 接口层
    也就是和数据库进行交互,核心接口为SqlSession,一个SqlSession对应着一次数据库会话,那么其生命周期不是永久的,理论上每次访问数据库时都需要创建它。
    形式分为两种,一种是使用Mapper接口,一种是基于Mybatis提供的Api;
    • Mybatis提供的Api
      需要我们提供StatementId和查询参数,传递给SqlSession对象,提供SqlSession对象实现与数据库的交互;但是这种创建sqlSession的形式不符合面向接口编程的习惯。
    • 使用Mapper接口:
      Mybatis将配置文件中的每一个Mapper节点都抽象为一个Mapper接口,根据SqlSession.getMapper(XXXMapper.class),Mybatis将通过动态代理,生成一个Mapper实例。但当我们调用Mapper接口中的方法时,Mybatis会根据方法名和参数,确定StatementId,底层还是通过SqlSession来实现对数据库的操作。
  2. 数据处理层
    • 配置解析
    • 参数、结果集映射:java数据类型与jdbc数据类型的转换,包括查询阶段和结果返回阶段
    • Sql解析:动态sql生成
    • Sql执行
  3. 框架支持层
    • 事务管理
    • 连接池管理
    • 缓存机制

使用Demo:

String resource = "mybatis-config.xml"; 
InputStream inputStream = Resources.getResourceAsStream(resource); 
// 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 
// 创建SqlSession 
SqlSession sqlSession = sqlSessionFactory.openSession(); 
// 执行SQL语句 (通过StatementId)
List list = sqlSession.selectList("com.example.mapper.personMapper.selectPersonByMap");
// 执行SQL语句(通过Mapper接口)
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
personMapper.selectPersonByMap();

Mybatis的初始化有两种形式:

  • 基于XML配置文件:通过XML配置文件,将配置信息解析为Configuration对象。
  • 基于Java API:在Java代码中手动创建Configuration对象,然后将配置参数set 进入Configuration对象中。(不推荐)

Mybatis核心流程

1. 创建SqlSessionFactory

String resource = "mybatis-config.xml"; 
InputStream inputStream = Resources.getResourceAsStream(resource); 
// 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 

在这里插入图片描述

将xml配置信息解析为Configuration对象,然后构建一个DefaultSqlSessionFactory类型的工厂:

  1. 创建XMLConfigBuilder,解析xml全局文件
  2. 创建XMLMapperBuilder,解析每个xml
  3. 最后将返回Configuration对象,保存着全部的xml配置信息;

配置解析——SQL解析(存储MappedStatement到Configuration中)

我们重点分析XMLMapperBuilder.parse方法中,对sql相关信息的解析过程:
在这里插入图片描述

  • 解析Mapper节点,获得XNode,再解析XNode中的 select|insert|update|delete 节点,保存为XNode类型的List;遍历CRUD的XNode节点:
  • 新建XMLStatementBuilder,调用parseStatementNode,用于解析每一个CRUD的节点
  • 创建LanguageDriver(默认为XMLLanguageDriver,用于处理xml中的sql部分)
  • 调用LanguageDriver.createSqlSource,获取SqlSource
  • 根据SqlSource创建MappedStatement,并且注册到Configuration中

createSqlSource获取SqlSource的方法:
在这里插入图片描述

  1. 新建XMLScriptBuilder,并调用parse解析各个节点的sql部分
  2. 递归解析,最终得到MixedSqlNode(本质为SqlNode的List集合),并且标记此Sql类型为动态还是静态
  3. 如果是动态类型,返回DynamicSqlSource
  4. 如果是静态类型,返回RawSqlSource
  5. 返回的SqlSource将会保存在MappedStatement中,然后等sql执行的时候,通过getBoundSql方法触发sql的解析

总结

  • XMLConfigBuilder
    解析xml整体文件
  • XMLMapperBuilder
    解析每个xml文件
  • XMLStatementBuilder
    解析xml文件中各个select,insert,update,delete节点
  • XMLScriptBuilder
    解析xml中各个节点sql部分的Builder,产生MapperStatement,保存到Configuration中

SqlNode

节点解析时的工具类,简单理解就是xml中的每个标签,如update,trim,if标签
其实现类包括
在这里插入图片描述
实现动态Sql的关键就是 各个SqlNode的 apply方法
以IfSqlNode为例,如果满足条件,则apply,并返回true

 @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

而StaticTextSqlNode类型静态sql,则直接append

@Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

MappedStatement

是Configuration中的属性,表示一个CRUD节点的信息

SqlSource

是MappedStatement的属性,实现类包括:
在这里插入图片描述

  • StaticSqlSource:最终静态SQL语句的封装,其他类型的SqlSource最终都委托给StaticSqlSource。
  • RawSqlSource:原始静态SQL语句的封装,在加载时就已经确定了SQL语句,比动态SQL语句要快,因为不需要运行时解析SQL节点。
    • 如#占位符
  • DynamicSqlSource:动态SQL语句的封装,在运行时需要根据参数处理if等标签或者${} SQL拼接之后才能生成最后要执行的静态SQL语句。
    • 如$占位符或者if等标签
  • ProviderSqlSource:当SQL语句通过指定的类和方法获取时(使用@XXXProvider注解),需要使用本类,它会通过反射调用相应的方法得到SQL语句。

其getBoundSql方法提供BoundSql对象。

BoundSql

是从SqlSource中获取得到的信息,封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数;后续在执行Sql的时候将会使用到BoundSql。
在这里插入图片描述

2. 创建SqlSession

SqlSession sqlSession = sqlSessionFactory.openSession(); 

在这里插入图片描述

  1. 创建Transaction事务:
    1. 根据Configuration配置信息获取Environment环境对象
    2. 根据environment信息获取TransactionFactory,这里获取到的事务工厂,即对应着xml文件中的transactionManager节点
    3. newTransaction创建事务tx
  2. 创建Executor执行器
    1. 调用Configuration对象的newExecutor方法,将事务对象tx传参进去;
    2. newExecutor方法,默认创建的是Simple类型的SimpleExecutor
      (类型一共三种:SIMPLE, REUSE, BATCH)
      如果开启了cacheEnabled(默认开启),那么包装为CachingExecutor:在查询数据之前先查找缓存,没有找到再从数据库查询并加入缓存。
    3. executor = (Executor) interceptorChain.pluginAll(executor);
      使用了执行器链模式,使得Executor对象可以被插件拦截;
  3. 创建DefaultSqlSession:
    1. new DefaultSqlSession(configuration, executor, autoCommit)

SqlSession

Mybatis与数据库交互的核心接口,一个SqlSession对应着一次数据库会话。

Executor

执行器,真正的sql执行并不是SqlSession直接执行的,而是通过Executor去执行。

3. 获取Mapper

PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);

在这里插入图片描述

  1. 通过MapperRegister(Map类型的Mapper注册器Map<Class<?>, MapperProxyFactory<?>>)根据接口Class信息,获取到目标MapperProxyFactory
  2. 通过MapperProxyFactory创建MapperProxy(也就是Mapper接口的代理)
    1. Proxy.newProxyInstance动态代理创建
  3. 至此,我们通过SqlSession.getMapper获取到的personMapper对象,实际上是一个代理类

4. 通过Mapper接口调用CRUD方法

personMapper.selectPersonByMap();

在这里插入图片描述

被代理对象的方法的访问,都会落实到代理者的invoke上来,所以调用的mapper.select等方法,将会经过MapperProxy中的invoke:

  1. 创建MapperMethod对象:
    根据CRUD的类型将调用分发到SqlSession的不同方法上
  2. 处理参数:
    convertArgsToSqlCommandParam:当args数量>1时,将Object[]的参数包装为Map<String, Object>
  3. SqlSession方法的执行:
    1. 根据MapperMethod内部类SqlCommand中存储的statementId,从Configuration中找对应的MappedStatement
    2. 将MappedStatement作为参数,调用Executor中对应的方法

MapperMethod

它是一个分发者,根据CRUD的类型将调用分发到SqlSession的不同方法上。
有两个内部类同时被创建:
- SqlCommand
interfaceName + methodName作为statementId,去Configuration中去找对应的MappedStatement
存储statementId、SqlCommandType(CRUD类型)信息
- MethodSignature
记录方法的参数、返回值等信息

5. Executor执行SQL

在这里插入图片描述

  1. 我们默认为调用的是有缓存的Executor,那么先本地查询缓存,如果没找到再去调用(默认简单类型)SimpleExecutor的query方法
  2. 创建StatementHandler
    默认类型为ParparedStatementHandler,Executor将执行的任务交给它,它才是sql的具体执行者。也可以被插件所拦截(常见的物理分页插件)
    1. prepare:预编译sql,得到Statement对象
    2. 调用ParameterHandler:处理参数
    3. 调用ResultSetHandler:处理执行结果

StatementHandler

由此可见,Executor也不是真正的执行者,而是由StatementHandler完成的。在StatementHandler被创建的过程中,有一个getBoundSql的步骤,触发了动态sql的处理,下节详细分析。

【MyBatis详解】——从Xml配置解析到SQL执行过程_查看xml里写的sql语句实际是怎么执行的-CSDN博客

mybatis动态更新xml文件后热部署,不重启应用的方法_idea xml文件 热更新-CSDN博客

mybatis应用程序,由于是半自动化的sql, 有大量的sql是在xml文件中配置的,而在开发程序的过程中,通常需要边写sql变调试应用。但在默认情况下,xml文件里配置的sql语句是被放入到缓存中去了,每次更改有sql语句的xml文件,需要重新启动应用,这样工作效率很低,于是很希望有一个动态加载xml文件的功能,自动加载新的sql语句,并重新写入到缓存中,在网上参考了很多资料,最终弄了一个简单的东西出来,直接写成了spring mvc的controller。代码如下: 
 

 
  1. package com.yihaomen.controller;
  2. import java.io.IOException;
  3. import java.lang.reflect.Field;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. import java.util.Set;
  7. import org.apache.commons.logging.Log;
  8. import org.apache.commons.logging.LogFactory;
  9. import org.apache.ibatis.builder.xml.XMLMapperBuilder;
  10. import org.apache.ibatis.session.Configuration;
  11. import org.apache.ibatis.session.SqlSessionFactory;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.context.annotation.Scope;
  14. import org.springframework.core.io.Resource;
  15. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
  16. import org.springframework.stereotype.Controller;
  17. import org.springframework.web.bind.annotation.RequestMapping;
  18. import org.springframework.web.bind.annotation.ResponseBody;
  19. @Scope("prototype")
  20. @Controller
  21. @RequestMapping("/sql")
  22. public class SQLSessionCacheController {
  23.     
  24.     private Log log = LogFactory.getLog(SQLSessionCacheController.class);
  25.     
  26.     @Autowired
  27.     private SqlSessionFactory sqlSessionFactory;
  28.     
  29.     private Resource[] mapperLocations;
  30.     private String packageSearchPath = "classpath*:**/mappers/*.xml";
  31.     private HashMap<String, Long> fileMapping = new HashMap<String, Long>();// 记录文件是否变化
  32.     
  33.     @RequestMapping("/refresh")
  34.     @ResponseBody
  35.     public String refreshMapper() {
  36.         try {
  37.             Configuration configuration = this.sqlSessionFactory.getConfiguration();
  38.             
  39.             // step.1 扫描文件
  40.             try {
  41.                 this.scanMapperXml();
  42.             } catch (IOException e) {
  43.                 log.error("packageSearchPath扫描包路径配置错误");
  44.                 return "packageSearchPath扫描包路径配置错误";
  45.             }
  46.             
  47.             System.out.println("==============刷新前mapper中的内容===============");
  48.             for (String name : configuration.getMappedStatementNames()) {
  49.                 System.out.println(name);
  50.             }
  51.             
  52.             // step.2 判断是否有文件发生了变化
  53.             if (this.isChanged()) {
  54.                 // step.2.1 清理
  55.                 this.removeConfig(configuration);
  56.                 // step.2.2 重新加载
  57.                 for (Resource configLocation : mapperLocations) {
  58.                     try {
  59.                         XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configLocation.getInputStream(), configuration, configLocation.toString(), configuration.getSqlFragments());
  60.                         xmlMapperBuilder.parse();
  61.                         log.info("mapper文件[" + configLocation.getFilename() + "]缓存加载成功");
  62.                     } catch (IOException e) {
  63.                         log.error("mapper文件[" + configLocation.getFilename() + "]不存在或内容格式不对");
  64.                         continue;
  65.                     }
  66.                 }
  67.             }
  68.             
  69.             System.out.println("==============刷新后mapper中的内容===============");
  70.             for (String name : configuration.getMappedStatementNames()) {
  71.                 System.out.println(name);
  72.             }
  73.             return "刷新mybatis xml配置语句成功";
  74.         } catch (Exception e) {
  75.             e.printStackTrace();
  76.             return "刷新mybatis xml配置语句失败";
  77.         }
  78.     }
  79.     
  80.     public void setPackageSearchPath(String packageSearchPath) {
  81.         this.packageSearchPath = packageSearchPath;
  82.     }
  83.     
  84.     public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  85.         this.sqlSessionFactory = sqlSessionFactory;
  86.     }
  87.     /**
  88.      * 扫描xml文件所在的路径
  89.      * @throws IOException
  90.      */
  91.     private void scanMapperXml() throws IOException {
  92.         this.mapperLocations = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
  93.     }
  94.     /**
  95.      * 清空Configuration中几个重要的缓存
  96.      * @param configuration
  97.      * @throws Exception
  98.      */
  99.     private void removeConfig(Configuration configuration) throws Exception {
  100.         Class<?> classConfig = configuration.getClass();
  101.         clearMap(classConfig, configuration, "mappedStatements");
  102.         clearMap(classConfig, configuration, "caches");
  103.         clearMap(classConfig, configuration, "resultMaps");
  104.         clearMap(classConfig, configuration, "parameterMaps");
  105.         clearMap(classConfig, configuration, "keyGenerators");
  106.         clearMap(classConfig, configuration, "sqlFragments");
  107.         clearSet(classConfig, configuration, "loadedResources");
  108.     }
  109.     @SuppressWarnings("rawtypes")
  110.     private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
  111.         Field field = classConfig.getDeclaredField(fieldName);
  112.         field.setAccessible(true);
  113.         Map mapConfig = (Map) field.get(configuration);
  114.         mapConfig.clear();
  115.     }
  116.     @SuppressWarnings("rawtypes")
  117.     private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
  118.         Field field = classConfig.getDeclaredField(fieldName);
  119.         field.setAccessible(true);
  120.         Set setConfig = (Set) field.get(configuration);
  121.         setConfig.clear();
  122.     }
  123.     
  124.     /**
  125.      * 判断文件是否发生了变化
  126.      * @param resource
  127.      * @return
  128.      * @throws IOException
  129.      */
  130.     private boolean isChanged() throws IOException {
  131.         boolean flag = false;
  132.         for (Resource resource : mapperLocations) {
  133.             String resourceName = resource.getFilename();
  134.             
  135.             boolean addFlag = !fileMapping.containsKey(resourceName);// 此为新增标识
  136.             
  137.             // 修改文件:判断文件内容是否有变化
  138.             Long compareFrame = fileMapping.get(resourceName);
  139.             long lastFrame = resource.contentLength() + resource.lastModified();
  140.             boolean modifyFlag = null != compareFrame && compareFrame.longValue() != lastFrame;// 此为修改标识
  141.             
  142.             // 新增或是修改时,存储文件
  143.             if(addFlag || modifyFlag) {
  144.                 fileMapping.put(resourceName, Long.valueOf(lastFrame));// 文件内容帧值
  145.                 flag = true;
  146.             }
  147.         }
  148.         return flag;
  149.     }
  150. }
 



注意事项: 
在我的应用中,mybatis配置文件放在这里的:classpath*:**/mappers/*.xml, 因此我定义死了,需要修改成自己的路径. 

测试方法: 
可以通过controller提供的地址,直接在浏览器上输入url访问, 比如: http://localhost:8080/sql/refresh , 当然,你还可以通过js用ajax方式调用,都是可以的。

提取MyBatis中XML语法构造SQL的功能

 

提取MyBatis中XML语法构造SQL的功能

MyBatis能够使用 *.xml来编辑XML语法格式的SQL语句,常用的xml标签有<where><if><foreach>等。

偶然遇到一个场景,只想使用MyBatis的解析XML语法生成SQL的功能,而不需其他功能,于是在@Select打断点,跟踪代码执行,后续发现和XML有关的类主要在包路径org.apache.ibatis.scripting.xmltags

下面只用简单的例子举例如何仅使用MyBaits中XML生成SQL的功能,不做太多抽象/封装逻辑、不考虑 SQL注入 等安全问题,以演示功能为主

1. 数据库表定义

person表定义
create table person
(
    id     int auto_increment comment '主键' primary key,
    name   varchar(255) null comment '名称',
    gender tinyint(1)   null comment '性别, 0 female, 1 man',
    age    int          null comment '年龄, 0~200'
);

2. XML文件中SQL代码

假设有如下XML语法的SQL代码片段,这里`item`故意和`collection`重名,主要是方便后续解析
<script>
  select * from person 
      where name like CONCAT('%', #{name} ,'%') 
      <if test='ageList != null'>
          and age in 
          <foreach collection='ageList' open='(' close =')' item='ageList' separator=','> 
                #{ageList}
          </foreach>
      </if> 
      <if test='gender != null'> 
          and gender > #{gender} 
      </if>
</script>

3. 代码示例和输出

3.1 使用XML语法生成SQL (#{}的版本)

java代码 (#{}的版本)

Java代码
package com.example.springboottest;

import com.google.common.base.Splitter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2023/7/22 2:44 PM
 */
public class MyBatisSqlTest2 {
    public static void main(String[] args) {

        //1.  这里用 Map 存放查询参数(实际项目中可以用POJO类或其他形式)
        Map<String, Object> paramMap = new HashMap<>();
        List<Integer> ageList = new ArrayList<>();
        ageList.add(18);
        ageList.add(19);
        paramMap.put("ageList", ageList);
        paramMap.put("name", "person");
        paramMap.put("gender", -1);

        // 2. 打印 SQL 中使用到的查询参数
        System.out.println("==== SQL中使用到的参数: ==== start ==");
        paramMap.entrySet().forEach(System.out::println);
        System.out.println("==== SQL中使用到的参数: ==== end ==" + System.lineSeparator());

        // 3. 构造XML语法的SQL (实际项目中可以通过注解等形式封装SQL字符串)
        String anotherSql = "<script>" +
                "select * from person " +
                "<where> " +
                "name like CONCAT('%', #{name} ,'%') " +
                "<if test='ageList != null'>" +
                "and age in " +
                "<foreach collection='ageList' open='(' close =')' item='ageList' separator=','>" +
                "#{ageList}" +
                "</foreach>" +
                "</if>" +
                "<if test='gender != null'> " +
                "and gender > #{gender} " +
                "</if> " +
                "</where>"
                + "</script>";
        Configuration configuration = new Configuration();
        XMLLanguageDriver xmlLanguageDriver = new XMLLanguageDriver();
        SqlSource sqlSource = xmlLanguageDriver.createSqlSource(configuration, anotherSql, Map.class);
        BoundSql boundSql = sqlSource.getBoundSql(paramMap);
        String preparedSQL = boundSql.getSql();

        // 4. 输出 预编译SQL (?表示需要填充传入的参数的位置)
        System.out.println("==== 预编译SQL : ==== start ==");
        System.out.println(preparedSQL);
        System.out.println("==== 预编译SQL : ==== end ==" + System.lineSeparator());

        // 5. 输出 预编译SQL的 参数列表 (之后替代 ? 位置)
        System.out.println("==== 预编译SQL的参数列表 : ==== start ==");
        boundSql.getParameterMappings().forEach(System.out::println);
        System.out.println("==== 预编译SQL的参数列表 : ==== end ==" + System.lineSeparator());

        // 6. 替换预编译SQL的 ?, 传递参数 (或者XML的SQL中直接使用 ${} 则preparedSQL直接是最终的SQL, 下面这边再做替换其实也没啥意义)
        Splitter splitter = Splitter.on("?");
        Iterable<String> splitIterable = splitter.split(preparedSQL);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        StringBuilder stringBuilder = new StringBuilder();
        int i = 0;
        for (String str : splitIterable) {
            if (StringUtils.isBlank(str)) {
                continue;
            }
            stringBuilder.append(str);
            ParameterMapping paramObj = parameterMappings.get(i);
            String fieldName = paramObj.getProperty();
            Object value;
            // 这里 ITEM_PREFIX 见处理<foreach>的 org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX
            if (fieldName.startsWith(ITEM_PREFIX)) {
                fieldName = fieldName.substring(7);
                int indexIndex = fieldName.lastIndexOf('_');
                int valueIndex = NumberUtils.toInt(fieldName.substring(indexIndex + 1));
                fieldName = fieldName.substring(0, indexIndex);
                List listValue = (List) paramMap.get(fieldName);
                value = listValue.get(valueIndex);
            } else {
                value = paramMap.get(fieldName);
            }
            stringBuilder.append(value);
            i++;
        }
        String finalSqlWithParam = stringBuilder.toString();

        // 7. 输出最后传入参数后的 SQL (实际用 ${} 即可免去自己再处理一遍参数的情况)
        System.out.println("==== 最终完整的SQL : ==== start ==");
        System.out.println(finalSqlWithParam);
        System.out.println("==== 最终完整的SQL : ==== end ==");
    }
}

运行输出结果 (#{}的版本)

运行输出结果
==== SQL中使用到的参数: ==== start ==
gender=-1
name=person
ageList=[18, 19]
==== SQL中使用到的参数: ==== end ==

==== 预编译SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', ? ,'%') and age in (?,?) and gender > ?
==== 预编译SQL : ==== end ==

==== 预编译SQL的参数列表 : ==== start ==
ParameterMapping{property='name', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='__frch_ageList_0', mode=IN, javaType=class java.lang.Integer, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='__frch_ageList_1', mode=IN, javaType=class java.lang.Integer, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
ParameterMapping{property='gender', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
==== 预编译SQL的参数列表 : ==== end ==

==== 最终完整的SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 最终完整的SQL : ==== end ==

3.2 使用XML语法生成SQL (${}的版本)

java代码 (${}的版本)

Java代码
package com.example.springboottest;

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author : Ashiamd email: ashiamd@foxmail.com
 * @date : 2023/7/22 2:44 PM
 */
public class MyBatisSqlTest2 {
    public static void main(String[] args) {

        //1.  这里用 Map 存放查询参数(实际项目中可以用POJO类或其他形式)
        Map<String, Object> paramMap = new HashMap<>();
        List<Integer> ageList = new ArrayList<>();
        ageList.add(18);
        ageList.add(19);
        paramMap.put("ageList", ageList);
        paramMap.put("name", "person");
        paramMap.put("gender", -1);

        // 2. 打印 SQL 中使用到的查询参数
        System.out.println("==== SQL中使用到的参数: ==== start ==");
        paramMap.entrySet().forEach(System.out::println);
        System.out.println("==== SQL中使用到的参数: ==== end ==" + System.lineSeparator());

        // 3. 构造XML语法的SQL (实际项目中可以通过注解等形式封装SQL字符串)
        String anotherSql = "<script>" +
                "select * from person " +
                "<where> " +
                "name like CONCAT('%', ${name} ,'%') " +
                "<if test='ageList != null'>" +
                "and age in " +
                "<foreach collection='ageList' open='(' close =')' item='ageList' separator=','>" +
                "${ageList}" +
                "</foreach>" +
                "</if>" +
                "<if test='gender != null'> " +
                "and gender > ${gender} " +
                "</if> " +
                "</where>"
                + "</script>";
        Configuration configuration = new Configuration();
        XMLLanguageDriver xmlLanguageDriver = new XMLLanguageDriver();
        SqlSource sqlSource = xmlLanguageDriver.createSqlSource(configuration, anotherSql, Map.class);
        BoundSql boundSql = sqlSource.getBoundSql(paramMap);
        String preparedSQL = boundSql.getSql();

        // 4. 输出 预编译SQL (?表示需要填充传入的参数的位置)
        System.out.println("==== 预编译SQL : ==== start ==");
        System.out.println(preparedSQL);
        System.out.println("==== 预编译SQL : ==== end ==" + System.lineSeparator());

        // 5. 输出 预编译SQL的 参数列表 (之后替代 ? 位置)
        System.out.println("==== 预编译SQL的参数列表 : ==== start ==");
        boundSql.getParameterMappings().forEach(System.out::println);
        System.out.println("==== 预编译SQL的参数列表 : ==== end ==" + System.lineSeparator());

        // 6. 替换预编译SQL的 ?, 传递参数 (或者XML的SQL中直接使用 ${} 则preparedSQL直接是最终的SQL, 下面这边再做替换其实也没啥意义)
//        Splitter splitter = Splitter.on("?");
//        Iterable<String> splitIterable = splitter.split(preparedSQL);
//        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//        StringBuilder stringBuilder = new StringBuilder();
//        int i = 0;
//        for (String str : splitIterable) {
//            if (StringUtils.isBlank(str)) {
//                continue;
//            }
//            stringBuilder.append(str);
//            ParameterMapping paramObj = parameterMappings.get(i);
//            String fieldName = paramObj.getProperty();
//            Object value;
//            // 这里 ITEM_PREFIX 见处理<foreach>的 org.apache.ibatis.scripting.xmltags.ForEachSqlNode.ITEM_PREFIX
//            if (fieldName.startsWith(ITEM_PREFIX)) {
//                fieldName = fieldName.substring(7);
//                int indexIndex = fieldName.lastIndexOf('_');
//                int valueIndex = NumberUtils.toInt(fieldName.substring(indexIndex + 1));
//                fieldName = fieldName.substring(0, indexIndex);
//                List listValue = (List) paramMap.get(fieldName);
//                value = listValue.get(valueIndex);
//            } else {
//                value = paramMap.get(fieldName);
//            }
//            stringBuilder.append(value);
//            i++;
//        }
//        String finalSqlWithParam = stringBuilder.toString();

        // 7. 输出最后传入参数后的 SQL (实际用 ${} 即可免去自己再处理一遍参数的情况)
        System.out.println("==== 最终完整的SQL : ==== start ==");
//        System.out.println(finalSqlWithParam);
        System.out.println(preparedSQL);
        System.out.println("==== 最终完整的SQL : ==== end ==");
    }
}

运行输出结果 (${}的版本)

运行输出结果
==== SQL中使用到的参数: ==== start ==
gender=-1
name=person
ageList=[18, 19]
==== SQL中使用到的参数: ==== end ==

==== 预编译SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 预编译SQL : ==== end ==

==== 预编译SQL的参数列表 : ==== start ==
==== 预编译SQL的参数列表 : ==== end ==

==== 最终完整的SQL : ==== start ==
select * from person  WHERE name like CONCAT('%', person ,'%') and age in (18,19) and gender > -1
==== 最终完整的SQL : ==== end ==
posted @ 2025-02-05 15:14  CharyGao  阅读(403)  评论(0)    收藏  举报