一、前文回顾
在前文中我们分析了Mybatis是如何替我们实现了我们所编写的Mapper接口(采用JDK动态代理,在获取Mapper的时候创建Mapper的实现类)。本文将继续学习Mybatis中其他的知识,今天索要研究的是Mybatis是如何处理动态SQL的。
二、动态SQL
在说动态SQL之前,我们先说下静态SQL,所谓静态SQL就是SQL在运行期间不会改变,例如要查询用户表的所有用户我们会这样写 select * from user (这里不讨select * 是否规范);与之相对的就是动态SQL,即在运行期间SQL会经常改变,例如通过用户名查询用户可能是 select * from user where name like ‘%张三%’;或者是select * from user where name like ‘%李四%’;在Mybatis之前我们通常会使用PrepareStatement配合占位符?来实现。之前也有说过Mybatis几乎屏蔽了所有的JDBC操作,我们不在需求这么麻烦的去操作动态SQL,那么Mybatis中是如何实现的呢?
三、Mybatis动态SQL分析
1、前置代码
1、实体类
@Data
public class Company {
private Long id;
private String companyName;
private String companyNo;
private String address;
}
2、Mapper接口
public interface CompanyMapper {
List<Company> list(@Param("company") Company company);
}
3、Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pax.mapper.CompanyMapper">
<select id="list" resultType="com.pax.entity.Company">
select * from company
<where>
<if test="company.companyNo != null and company.companyNo != ''">
company_no like concat('%',#{company.companyNo},'%')
</if>
<if test="company.companyName != null and company.companyName != ''">
company_name like concat('%',#{companyName},'%')
</if>
</where>
</select>
</mapper>
4、Main函数
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//创建SqlSession工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//通过SqlSessionFactory创建SqlSession (try-with-resource,会自动关闭连接)
try (SqlSession session = sqlSessionFactory.openSession()) {
CompanyMapper companyMapper = session.getMapper(CompanyMapper.class);
Company condition = new Company();
condition.setCompanyName("Test Company Name");
condition.setCompanyNo("123");
companyMapper.list(condition);
}
}
2、流程Debug
2.1、参数是如何转化的
我们直接在companyMapper.list(condition);这一行打下断点进行Debug。前面的流程不再赘述(如果不是很清楚的可以看之前的文章Mybatis流程Debug ),我们直接来到MapperMethod这个类,
可以看到当执行完 method.convertArgsToSqlCommandParam(args); 方法之后,我们传递的Company对象就转为成一个类似Map的类型的参数。从方法名上也可以看出该方法的作用就是将我们传递的参数,转化成 命令参数(即SQL参数)
(关注到key为“company”,这个值就是我们再Mapper接口中定义的 List list(@Param(“company”) Company company);)
继续Debug该方法,下面代码是转化的核心方法
源码分析:
1、首先会判断是否有参数,如果没有参数则返回null(静态SQL没有参数)
2、通过反射获取方法的参数中是否有 “@Param”注解,如果没有注解且只有一个参数直接返回传递的参数(结合到本案例中就是直接返回 company对象)
3、否则会封装为Map<String, Object>对象,如果我们自定义了key则使用我们自定义key,同时也会添加名为 param1,param2…的key,如下图
2.2、参数是如何拼接到SQL
分析完了参数是如何转化的,接下来颜探究参数是如何拼接到SQL中的,继续Debug
result = sqlSession.selectList(command.getName(), param);,一路Debug我们来到了BaseExecutor,
可以看到当执行完ms.getBoundSql(parameter);之后我们的SQL已经将我们所写的等标签转化为了对应的SQL,同时也将动态参数用占位符?来代替。(至于如何转化的放到后面将,这里讲参数是如何拼接到sql中的)。既然已经拿到了含有占位符的SQL,那么接下来就是要用实际的参数去替代占位符(?),继续Debug。
一路Debug我们停在了SimpleExecutor的prepareStatement方法,方法名中也可以看出该方法返回的是一个预编译的Statment对象,进入该方法
源码分析:
1、获取数据库连接
2、创建PrepareStatment(预编译的Statment对象)
3、处理参数!
继续Debug处理参数的方法
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//核心代码
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
源码分析:这部分源码相对来说很长,但是并不是非常的复杂。核心得逻辑就是通过之前封装的Map对象取到我们传递的参数,并且根据类型判断是否有对应的TypeHandler(类型处理器,用于自定义处理参数),接着根据不同类型的参数去调用不同类型的处理器,再根据配置文件里JDBC类型和Java类型的映射关系最终给SQL动态赋值。(了解即可)
四、总结
本文讲述了Mybatis中是如何动态的为我们的SQL添加参数,下一篇文章将会学习在Mybatis中我们所写的标签是如何被解析的,例如 等等,希望对你有所帮助。