在上一章的 Executor
的 query
方法中,我们看到了 SQL 的获取是借助了 MappedStatement
去生成 SQL :
复制 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
下面我们就进入这个方法,看一下动态 SQL 的构造逻辑。
1. 动态SQL的构造
这个 getBoundSql
方法,会传入当前调用它的参数对象,用于动态 SQL 的生成,下面我们先来看看它的方法实现:
复制 public BoundSql getBoundSql(Object parameterObject) {
// 使用SqlSource,根据传入的参数,构造出BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 此处是处理<parameterMap>标签,由于MyBatis已将其废弃,我们忽略
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
从源码中,我们可以得知,MappedStatement
获取 BoundSql
,实际还是调用内部组合的那个 SqlSource
去生成和获取。SqlSource
这个东西我们在第 22 章已经讲解过了,它是封装 SQL 的一个 “定义 ” (可以联想到 Bean 与 BeanDefinition
)。而 SqlSource
接口本身就一个方法,就是传入参数,生成 BoundSql
对象。
说到这里,小伙伴们会比较蒙圈:BoundSql
又是个啥呢?
1.1 BoundSql
我们可以先看一下 BoundSql
的构造:
复制 public class BoundSql {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
注意看这个 sql
参数,它是 String
类型,说明是可以直接拿来生成 PreparedStatement
的。而直接生成 PreparedStatement
的 SQL 语句,占位符都是 **?**
吧!再回想一下 SqlSource
中存储的,都是 xml 或者注解中声明的 SQL 吧,那里面如果有需要传入参数的地方,是通过 **#{}**
传入的,所以由此我们可以得知一个非常重要的推断:**SqlSource**
中传入参数,返回 **BoundSql**
的过程,会将动态 SQL 解析转化为可以执行的带占位符的 SQL 语句 。
另外思考一下,它里面存了 SQL 和参数对象,这意味着什么呢?是不是只要有一个 BoundSql
的对象,就可以执行一次 SQL 操作了呢?即便是我们自己用原生的 jdbc 操作,也可以进行操作。
1.2 创建BoundSql的逻辑
简单看了一下 BoundSql
的结构,重要的还是几种 SqlSource
的实现类,它们都是如何制造出 BoundSql
的。下面我们一个一个来看。
1.2.1 StaticSqlSource
StaticSqlSource
本身就是一个静态的 SqlSource
,它本身没有任何动态 SQL 的标签 (if,foreach等等)${}
表达式,所以它转换为 BoundSql 的时候,只需要把那些 #{}
替换为 ?
即可。它的源码实现那是相当简单:
复制 public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
诶?它直接把 SQL 封装进去就完事了?那替换 #{}
占位符的逻辑呢?上面 BoundSql
的构造方法中也没有替换的逻辑呀,肯定是创建 StaticSqlSource
的时候就已经转换完毕了,那到底在哪里呢?
诶,别着急,我们往上翻一下 StaticSqlSource
的构造方法调用位置不就知道了嘛:
复制 public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
看,上面它会利用一个 GenericTokenParser
配合 ParameterMappingTokenHandler
去处理 #{}
占位符,所以这里就可以把 #{}
转换为 ?
了。具体的内容小伙伴们暂且可以不着急深入,现在只是知道有这回事就行。
1.2.2 DynamicSqlSource
DynamicSqlSource
构造 BoundSql
的逻辑是最复杂的,所以这部分小伙伴们在看的时候要静下心来。
我们先来看一下源码的实现:
复制 private final SqlNode rootSqlNode;
public BoundSql getBoundSql(Object parameterObject) {
// 使用DynamicContext辅助解析
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
// 使用SqlSourceBuilder解析SqlNode处理后的SQL
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// SqlSourceBuilder创建完成后,生成的是StaticSqlSource
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 由StaticSqlSource可以导出可以使用的SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// BoundSql中存入额外的参数
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
虽然源码不长,但还是比较难理解的,下面我们逐句拆解。
1.2.2.1 DynamicContext的作用
这个 DynamicContext
小册前面没有提到过,它可以理解为一个构造 SQL 的容器,从它的成员属性中就可以看到端倪:
复制 private final StringJoiner sqlBuilder = new StringJoiner(" ");
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
bindings = new ContextMap(null, false);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
注意它用的是 StringJoiner
而不是 StringBuilder
,其实之前的版本中 MyBatis 还真的是用 StringBuilder
来拼接 SQL 的,只是新版本的 jdk 提供了更好用的 StringJoiner
,MyBatis 就选择了它而已。StringJoiner
可以在构造时传入一个分隔符,这样每次拼接字符串时,StringJoiner
都会自动拼接一个分隔符,正好 MyBatis 在解析动态 SQL 时,每截取出来一段拼接时,都要先拼接一个空格,所以上面我们可以看到 StringJoiner
的构造时,就传入了一个空格作为分隔符。
了解了 DynamicContext
本身的构造,关键的问题是 DynamicSqlSource
的 getBoundSql
方法中的下一句:rootSqlNode.apply(context);
这句代码是调用的 rootSqlNode
,可关键是,这个 SqlNode
又是个啥呢?
1.2.2.2 SqlNode的设计
我们可以看到 MyBatis 解析 mapper.xml 的逻辑中,会根据 SQL 定义中是否包含动态语句标签,而决定如何封装子节点。
复制 protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 提取出statement中所有的子节点(除了子标签之外,SQL明文也算)
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 当前子节点是普通文本,或者xml中的CDATA类,则认定为SQL文本
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 普通文本也有可能是动态的
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 如果是动态SQL标签,则解析标签
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
// 并标注当前statement是动态SQL
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
注意一下,它中间会根据一个一个的 SQL 子节点,封装为一个一个的 SqlNode
(普通文本会封装为 TextSqlNode
,而动态 SQL 会让 NodeHandler
处理,也就是那些对应着动态 SQL 标签的一个一个的 Handler ,例如 IfHandler
、WhereHandler
等,这些 Handler 内部的处理也是会封装 SQL 为 SqlNode
对象)。这些封装好的 SqlNode
对象,最终会被组合进一个 List<SqlNode>
的集合 contents
中,并在方法的最后封装为一个 MixedSqlSource
。所以我们在实际 Debug 的时候,进入到 DynamicSqlSource
中,拿到的一定是一个 MixedSqlSource
类型的对象。
下面我们可以 Debug 走一个看看,还是选用第 24 章中测试的那句 sqlSession.selectList("dynamic.findAllDepartment");
代码,不过这里面没有传入查询参数,所以我们改一下代码:
复制 Department example = new Department();
example.setName("部");
sqlSession.selectList("dynamic.findAllDepartment", example);
之后,将断点打在 org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
方法的第二行,Debug 运行后等待断点停下来,我们可以看到此时此刻的 rootSqlNode
还真就是 MixedSqlNode
:
1.2.2.3 SqlNode解析为SQL
那下面的工作就是 MixedSqlNode
解析为 SQL 语句的逻辑了,进入 apply
方法,可以发现它就是一个简单的循环:
复制 public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
好吧,那我们还是跟着 Debug 的脚步,一个一个的跟进。
对于普通的 StaticTextSqlNode
而言,它要做的,就是把内部的 SQL 语句原封不动的追加到 SQL 语句的组合容器 DynamicContext
中:
复制 public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
而动态 SQL 标签封装而来的 WhereSqlNode
就不一样了:
复制 public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
filteredDynamicContext.applyAll();
return result;
}
注意它进入到了 TrimSqlNode
中,而这里面它会给原本的 DynamicContext
包一层装饰者 FilteredDynamicContext
,这个家伙是配合 <trim>
标签来的,它可以在内部的 SQL 组合完成后,截掉最前面或者最后面的特定字符串(好比 where 子句中的第一个 and ),这个处理的逻辑在 FilteredDynamicContext
的 applyAll
方法(上面的 apply
方法中有调用):
复制 public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
以此法处理完成后,<where>
标签就全部处理完成了。
所有的 SqlNode 处理完成后,DynamicContext
中也就组合了所有的 SQL ,这样动态 SQL 的构建也就完成了。
1.2.2.4 if标签的处理
上面只是大面上的处理,至于 <where>
标签中会涉及到的 <if>
等标签的处理我们还没有看,下面我们再拿一个简单的例子看一下。
上面我们测试的那个 dynamic.findAllDepartment
中有一个根据 id 的判断:
复制 <select id="findAllDepartment" parameterType="Department" resultType="Department">
select * from tbl_department
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="name != null">
and name like concat('%', #{name}, '%')
</if>
</where>
</select>
而这个 if 标签对应的 SQL 是否拼接,就要来到处理 if 标签的 SqlNode
实现类 IfSqlNode
中了:
复制 public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
这个方法的意图很明显,拿出判断的表达式,看看是否成立,如果成立,则将 SQL 字符串片段拼接进去。
先不进入 evaluateBoolean
方法,我们先看看此时此刻这个 IfSqlNode
中都封装了什么:
呦,if 标签的 test
表达式,以及内部的 SQL 片段都在这里面封装好了,那万事俱备,就差判断了,我们继续往下走吧。
复制 public boolean evaluateBoolean(String expression, Object parameterObject) {
// 此处使用到了OGNL
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
注意看方法的实现,它会使用 OGNL 的语法去解析这个 test 判断表达式,得到结果后根据不同类型处理一下,之后就返回了。一般情况下我们写的表达式都是返回 boolean ,所以它直接强转完就返回了。
当然,从源码中我们也可以看得出来,我们可以在判断表达式中写一些支持 Comparable
接口的对象比较,MyBatis 也会帮我们处理判断结果。
因为我们测试的时候没有传入参数,所以此处判断表达式不成立,返回 false ,回到 IfSqlNode
的 apply
方法中,也是由于返回 false ,所以不会拼接 SQL 片段了。
其余的动态 SQL 标签,感兴趣的小伙伴们可以自行翻看,并配合着编写一些动态 SQL 来测试一下,体会其中的处理逻辑,小册就不全部展开了。
1.2.2.5 处理#{}占位符
所有的动态 SQL 节点都处理完毕后,MixSqlNode
的处理工作也就结束了,所有解析出来的 SQL 片段最终都放在 DynamicContext
中了。我们可以通过 Debug 看出:
不过请各位注意,此时的 SQL 语句还是带 #{}
占位符的,那下一步就是该处理这些占位符了吧。接下来的这三行,就是处理它们的。
复制 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
这个 SqlSourceBuilder
要做的工作,就是将 #{}
转换为 ?
。。。等一下!这个 SqlSourceBuilder
我们上面不是刚见过吗?(忘记的可以返回 1.2.1 节)合着就是那个逻辑呗!上面小册特意卖了个关子,就是想着让小伙伴在这里再次遇到之后,加深一下印象。
OK ,下面我们来具体看解析的逻辑。
复制 public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
// 3.5.5 新增特性:是否删除SQL中的多余空格
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
先简单说下原理,这里面涉及到的两个组件,GenericTokenParser
负责找 #{}
,ParameterMappingTokenHandler
负责替换成 ?
占位符,并记录占位符对应的参数。Debug 走到这里,会发现进入了 if-else 结构的 else 分支,那我们就往下进行。
注意这个 shrinkWhitespacesInSql
配置,这是 MyBatis 3.5.5 刚加的,目的是去除 SQL 语句中的多余长空格(就像上面 Debug 中我们看到的那个 SQL 一样,因为有换行,以及我们编写 SQL 时的可读性,所以封装出来的 SQL 里面有好多好多的空格,MyBatis 觉得可以处理一下它们,于是就加了这样的一个特性)。不过这个特性我们一般不会用,一来比较新,二来如果遇到我们编写 SQL 的时候确实就是有长空格,那 MyBatis 也会 “误伤” 它们。所以综合来看这个配置我们忽略就好。
接下来的这个 parse
方法就有点不友好了,它很长,而且关键信息提取不是那么容易,所以小册不打算贴这段源码了,而是换用一组图来解释:
这个 GenericTokenParser
的内部结构,各位可以理解为上图中下面部分的结构,它内部有一个 StringBuilder
用来存储解析后的 SQL 语句,还有一个存储参数列表的容器,记录每个占位符对应的参数应该放什么。
当开始处理 SQL 语句时,它会初始化一个 SQL 字符串的扫描光标,去扫描 SQL 字符串中的 #{
结构,只要发现了,它就会记录下它的位置,并将这之前的内容全部放入容器中:
之后就是处理 #{}
占位符了,既然找到了 #{
,那就一定要找到剩下半拉括号,于是它就会从左半拉括号开始往右找,找到之后也记录下来,并把这个占位符中的参数摘出来,放到参数列表的容器中:
这个时候就要根据这个参数名去找对应的参数配置了,注意这里只是记录要找的参数名,还没有具体到参数值。
找出来之后,放到参数列表的容器中,继续下一轮寻找。
所有占位符都处理完毕后,即说明占位符都已经替换完毕,可以返回。返回的是一个 StaticSqlSource
:
1.2.2.6 存入查询参数
SqlSource
拿到后,还要生成 BoundSql
才能被下一步利用,所以接下来的动作就很简单了:
复制 public BoundSql getBoundSql(Object parameterObject) {
// ......
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 由StaticSqlSource可以导出可以使用的SQL
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// BoundSql中存入额外的参数
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
这一步就很简单了,它把我们传入的查询参数,以及 databaseId
等额外参数,直接塞到 BoundSql
中,完事。
这么一长串逻辑处理完毕后,SqlSource
也就构造出 BoundSql
了,结束。
1.2.3 ProviderSqlSource
ProviderSqlSource
生成 BoundSql
的逻辑,我想不用小册讲,各位也都能想象出逻辑吧!因为我们声明注解 statement 的时候,要指明要调用生成 SQL 的方法:
复制 public interface UserAnnotationMapper {
@SelectProvider(type = UserMapperProvider.class, method = "findAll")
List<User> findAll();
}
那自然,生成 BoundSql
的方式就是反射调用方法呗,确实,MyBatis 就是帮我们反射指定的方法,并拿到返回的 SQL ,直接完事。里面具体的逻辑也非常简单,小伙伴们可以自行翻看一下源码,小册就不展开了(本来我们平时用的就很少甚至不用)。
至此,SqlSource
生成 BoundSql
的逻辑,我们就算全部走完了,但是目前还有一个问题:参数的值啥时候取出来,利用到 PreparedStatement
呢?下面我们继续研究这个问题。
2. 参数绑定的应用
在 Executor
执行数据库查询时,肯定是要先准备出 PreparedStatement
的,而这个准备的逻辑,我们上一章已经看到过了:
复制 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
核心的动作是 handler.prepare
,而准备出来后,下面还有一个 handler.parameterize
的动作,很明显这个动作是设置参数的,我们进入这个方法。
2.1 parameterize
复制 protected final ParameterHandler parameterHandler;
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
这里又是直接调用了 ParameterHandler
的方法,这个 ParameterHandler
又是个啥呢?
2.2 ParameterHandler
顾名思义,它就是参数的处理器罢了。这个接口本身简单的很:
复制 public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
从接口方法上看,它的实现类一定能通过某个方法,把本次查询的参数传进去,这样才可以 get ,以及给 PreparedStatement
设置参数值。
我们的想法是必然的,按照 MyBatis 的编码风格,这个方法被设计为了构造方法:
复制 public class DefaultParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
这里面有存储 parameterObject
。
2.3 setParameters
下面才是重点,DefaultParameterHandler
如何将参数设置到 PreparedStatement
中呢?那我们就得看 setParameters
方法了:
复制 public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// BoundSql中存放了所有的参数列表和顺序
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 (......) {
// ......
} 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 ......
}
}
}
}
纵观整段源码的逻辑,可以提取出来的步骤就三个:
借助 TypeHandler
设置到 PreparedStatement
中
前两点都还 OK ,关键是最后一点,为什么要利用 TypeHandler
呢?
如果小伙伴不理解的话,可以回想一下 PreparedStatement 的方法,里面有这样一组方法(未截全):
能理解了吧,要给 PreparedStatement
设置什么类型的参数,是需要我们自己指定的!但是用了 MyBatis 后不由我们控制了,那 MyBatis 帮我们接了这个活,肯定要有它自己的处理逻辑,而它决定使用哪个方法设置参数的办法,就是选择不同的 TypeHandler
。
2.4 TypeHandler
TypeHandler
设置参数的逻辑,并不是直接无脑设置,而是会先判断一下参数有没有实际的值,没有的话就直接设置 null 了:
复制 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} // catch throw ex ......
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} // catch throw ex ......
}
}
只有参数有值的时候,才会往下走,执行 setNonNullParameter
方法,而这个方法本身是一个模板方法,需要各个子类实现。这个时候就是不同类型的参数,用不同的 TypeHandler
了。
比方说设置字符串值的 StringTypeHandler
:
复制 public class StringTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
再比方说设置 int 值的 IntegerTypeHandler
:
复制 public class IntegerTypeHandler extends BaseTypeHandler<Integer> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter);
}
经过 TypeHandler
的设置后,PreparedStatement
中的参数值也就都设置好了,参数绑定过程完毕。