前言
在 MyBatis-plus 自定义通用方法及其实现原理 中笔者介绍了 MyBatis-plus
添加通用方法的实现方式,但是其中还有一些细节需要澄清,下文笔者将详细分析
MyBatis-plus
对 SQL 语句脚本的构建,以及将其嵌入MappedStatement
的过程MyBatis
使用SqlSource
构建可执行的 SQL 语句
1. MyBatis-plus 对 SQL 语句脚本的处理
我们都知道 MyBatis-plus
干掉了繁琐的 XML 文件,使 MyBatis
框架的易用度、好用度大幅上升。在MyBatis-plus 源码解析 中笔者提到过,MyBatis-plus
实际是将 Mapper 方法映射为了对应的 SQL 语句脚本,这个步骤的核心就是 AbstractMethod#injectMappedStatement()
的子类实现,本文以 SelectOne#injectMappedStatement()
为例进行分析,其主要处理分为两个部分:
- SQL 语句脚本的构建
- 解析 SQL 语句脚本,将其转化为
SqlSource
封装到MappedStatement
中
1.1 SQL 语句脚本的构建
-
SelectOne#injectMappedStatement()
方法如下,SQL 语句脚本的构建实际在一个以SqlMethod.SELECT_ONE
为主体的String#format()
拼接操作中,重要调用的如下:- 通过
sqlMethod.getSql()
调用SqlMethod.SELECT_ONE#getSql()
方法获得 SQL 脚本主体字符串 - 调用
AbstractMethod#sqlFirst()
构建 SQL 脚本在正式 SQL 语句之前的部分,这里会使用字符串替换${ew.sqlFirst}
- 调用
AbstractMethod#sqlSelectColumns()
构建 SQL 语句 SELECT 查询的字段相关的脚本部分,此处会使用字符串替换${ew.sqlSelect}
- 调用
TableInfo#getTableName()
获取实际的要查询的表名 - 调用
AbstractMethod#sqlWhereEntityWrapper()
构建 SQL 语句 WHERE 条件相关的脚本部分,此处会使用字符串替换${ew.sqlSegment}
- 调用
AbstractMethod#sqlComment()
构建 SQL 语句尾部注释相关的脚本部分,此处会使用字符串替换${ew.sqlComment}
@Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_ONE; SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo), sqlComment()), modelClass); return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo); }
- 通过
-
SqlMethod.SELECT_ONE#getSql()
方法实际只是个获取操作,只要看下这个枚举的定义就知道这里拿到的是个使用%s
占位的脚本字符串public enum SqlMethod { ...... SELECT_ONE("selectOne", "查询满足条件一条数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"), private final String method; private final String desc; private final String sql; SqlMethod(String method, String desc, String sql) { this.method = method; this.desc = desc; this.sql = sql; } public String getMethod() { return method; } public String getDesc() { return desc; } public String getSql() { return sql; } }
-
AbstractMethod#sqlFirst()
的实现很简单,可以看到这里实际就是使用SqlScriptUtils#convertChoose()
工具类拼接 SQL 语句脚本 标签的过程,经过处理这里可以得到的脚本片段如下<choose> <when test="ew != null and ew.sqlFirst != null"> ${ew.sqlFirst} </when> <otherwise></otherwise> </choose>
protected String sqlFirst() { return SqlScriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_FIRST), SqlScriptUtils.unSafeParam(Q_WRAPPER_SQL_FIRST), EMPTY); }
-
SqlScriptUtils#convertChoose()
方法如下,显然就是标签字符串的构造,没有特别操作public static String convertChoose(final String whenTest, final String whenSqlScript, final String otherwise) { return "<choose>" + NEWLINE + "<when test=\"" + whenTest + QUOTE + RIGHT_CHEV + NEWLINE + whenSqlScript + NEWLINE + "</when>" + NEWLINE + "<otherwise>" + otherwise + "</otherwise>" + NEWLINE + "</choose>"; }
-
AbstractMethod#sqlSelectColumns()
的处理大同小异,其实就是指定查询表的字段,此处可以得到如下脚本片段<choose> <when test="ew != null and ew.sqlSelect != null"> ${ew.sqlSelect} </when> <otherwise>id,name,type</otherwise> </choose>
protected String sqlSelectColumns(TableInfo table, boolean queryWrapper) { /* 假设存在 resultMap 映射返回 */ String selectColumns = ASTERISK; if (table.getResultMap() == null || (table.getResultMap() != null && table.isInitResultMap())) { /* 普通查询 */ selectColumns = table.getAllSqlSelect(); } if (!queryWrapper) { return selectColumns; } return SqlScriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_SELECT), SqlScriptUtils.unSafeParam(Q_WRAPPER_SQL_SELECT), selectColumns); }
-
AbstractMethod#sqlWhereEntityWrapper()
的处理稍显复杂,不过原理和以上方法是一样的,此处可以获得如下脚本片段<if test="ew != null"> <where> <if test="ew.entity != null"> <if test="ew.entity.id != null">id=#{ew.entity.id}</if> <if test="ew.entity['name'] != null">AND name=#{ew.entity.name}</if> <if test="ew.entity['type'] != null">AND type=#{ew.entity.type}</if> </if> <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere"> <if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal">AND</if> ${ew.sqlSegment} </if> </where> <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere"> ${ew.sqlSegment} </if> </if>
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) { if (table.isLogicDelete()) { String sqlScript = table.getAllSqlWhere(true, true, WRAPPER_ENTITY_DOT); sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true); sqlScript += (NEWLINE + table.getLogicDeleteSql(true, true) + NEWLINE); String normalSqlScript = SqlScriptUtils.convertIf(String.format("AND ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_NONEMPTYOFNORMAL), true); normalSqlScript += NEWLINE; normalSqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_EMPTYOFNORMAL), true); sqlScript += normalSqlScript; sqlScript = SqlScriptUtils.convertChoose(String.format("%s != null", WRAPPER), sqlScript, table.getLogicDeleteSql(false, true)); sqlScript = SqlScriptUtils.convertWhere(sqlScript); return newLine ? NEWLINE + sqlScript : sqlScript; } else { String sqlScript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT); sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true); sqlScript += NEWLINE; sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_NONEMPTYOFWHERE), true); sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE; sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_EMPTYOFWHERE), true); sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true); return newLine ? NEWLINE + sqlScript : sqlScript; } }
-
AbstractMethod#sqlComment()
拿到的脚本片段如下,不做更多解释<choose> <when test="ew != null and ew.sqlComment != null"> ${ew.sqlComment} </when> <otherwise></otherwise> </choose>
protected String sqlComment() { return SqlScriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_COMMENT), SqlScriptUtils.unSafeParam(Q_WRAPPER_SQL_COMMENT), EMPTY); }
经过以上步骤,SQL 语句脚本各个关键的片段都已经构建完毕,最终得到的脚本如下所示。接下来的处理就是解析这个脚本,通过 LanguageDriver#createSqlSource()
方法将脚本转化为 SqlSource
对象,这个对象是决定最终执行的 SQL 语句的重中之重
<script>
<choose>
<when test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</when>
<otherwise></otherwise>
</choose>
SELECT
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,type</otherwise>
</choose>
FROM node
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null">AND name=#{ew.entity.name}</if>
<if test="ew.entity['type'] != null">AND type=#{ew.entity.type}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal">AND</if>
${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if>
<choose>
<when test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</when>
<otherwise></otherwise>
</choose>
</script>
1.2 SqlSource 的转化
-
LanguageDriver#createSqlSource()
是接口方法,MyBatis
使用 XML 来定义 SQL 语句配置的,实际调用到XMLLanguageDriver#createSqlSource()
,可以看到这里最终调用XMLScriptBuilder#parseScriptNode()
开始解析 XML 脚本public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { // issue #3 if (script.startsWith("<script>")) { XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver()); return createSqlSource(configuration, parser.evalNode("/script"), parameterType); } else { // issue #127 script = PropertyParser.parse(script, configuration.getVariables()); TextSqlNode textSqlNode = new TextSqlNode(script); if (textSqlNode.isDynamic()) { return new DynamicSqlSource(configuration, textSqlNode); } else { return new RawSqlSource(configuration, script, parameterType); } } } @Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); }
-
XMLScriptBuilder#parseScriptNode()
方法如下,可以看到核心是调用XMLScriptBuilder#parseDynamicTags()
解析 XML 脚本中的各个节点,最终返回一个SqlSource
对象此处注意,解析脚本时会判断
TextSqlNode
保存的文本内容,只要含有字符串拼接符${}
就需要进行动态拼接处理,因此返回一个DynamicSqlSource
对象public SqlSource parseScriptNode() { MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<>(); NodeList children = node.getNode().getChildNodes(); for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); 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 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); isDynamic = true; } } return new MixedSqlNode(contents); }
-
经过以上步骤,构建的
DynamicSqlSource
对象与 SQL 语句脚本的对应关系如下所示,后续就是将这个SqlSource
封装到MappedStatement
中的处理,也就是调用AbstractMethod#addSelectMappedStatementForTable()
方法
-
AbstractMethod#addSelectMappedStatementForTable()
方法其实只是入口,这里是通过一个个方法调用逐步组装适配MyBatis
API 的数据对象,最终通过Mybatis
内部的MapperBuilderAssistant#addMappedStatement()
方法构建封装了关键 SQL 信息的MappedStatement
对象,并将其添加到Configuration
中缓存起来protected MappedStatement addSelectMappedStatementForTable(Class<?> mapperClass, String id, SqlSource sqlSource, TableInfo table) { String resultMap = table.getResultMap(); if (null != resultMap) { /* 返回 resultMap 映射结果集 */ return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null, resultMap, null, new NoKeyGenerator(), null, null); } else { /* 普通查询 */ return addSelectMappedStatementForOther(mapperClass, id, sqlSource, table.getEntityType()); } } protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, String id, SqlSource sqlSource, Class<?> resultType) { return addMappedStatement(mapperClass, id, sqlSource, SqlCommandType.SELECT, null, null, resultType, new NoKeyGenerator(), null, null); } protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, SqlCommandType sqlCommandType, Class<?> parameterType, String resultMap, Class<?> resultType, KeyGenerator keyGenerator, String keyProperty, String keyColumn) { String statementName = mapperClass.getName() + DOT + id; if (hasMappedStatement(statementName)) { logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET); return null; } /* 缓存逻辑处理 */ boolean isSelect = false; if (sqlCommandType == SqlCommandType.SELECT) { isSelect = true; } return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null, parameterType, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn, configuration.getDatabaseId(), languageDriver, null); }
2. MyBatis 对 SqlSource 的使用
-
在 MyBatis 缓存实现源码分析 时序图中,笔者详细描绘了上层 Mapper 方法触发到底层数据库操作的流程,可以看到在缓存执行器执行
CachingExecutor#query()
方法时会将 SQL 语句需要的参数包装为ParamMap
对象传入,并调用MappedStatement#getBoundSql()
进行对应 SQL 语句的构建@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
-
MappedStatement#getBoundSql()
源码如下,参考上一节的内容可以知道此处的核心是调用DynamicSqlSource#getBoundSql()
方法创建封装了 SQL 执行语句全部所需信息的BoundSql
对象public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); } // check for nested result maps in parameter mappings (issue #30) 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; }
-
DynamicSqlSource#getBoundSql()
方法的处理主要分为了以下几步:rootSqlNode.apply()
调用MixedSqlNode#apply()
方法进入每个 SQL 节点的处理,这个过程会将传入参数值送入 SQL 节点中进行判断,从而决定那个节点最终的 sql 片段,最后将其拼接起来缓存到DynamicContext
内部SqlSourceBuilder#parse()
方法处理 SQL 语句中的#{}
占位符,将其替换为PreparedStatement
需要的入参形式 “?”
public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; }
-
MixedSqlNode#apply()
方法如下,可以看到就是遍历其内部保存的实际节点,调用其SqlNode#apply()
方法,此处可以对照上文解析 SQL 语句脚本的示意图理解,本文以IfSqlNode#apply()
方法为例@Override public boolean apply(DynamicContext context) { contents.forEach(node -> node.apply(context)); return true; }
-
IfSqlNode#apply()
方法的处理比较简练,可以看到重要步骤:- 使用内部的评估器
ExpressionEvaluator
对 Mapper 方法调用时的入参进行检验,此处实际用到了OGNL表达式
相关 API,有兴趣的读者可以自行了解,本文不再深入 - 如果表达式成立,则继续处理当前节点包含的其他节点,此处以处理
${}
字符串拼接符的TextSqlNode#apply()
为例
@Override public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; }
- 使用内部的评估器
-
TextSqlNode#apply()
的逻辑如下,可以看到进行的关键操作如下,有关字符串拼接符的解析及替换本文暂不展开,有兴趣的读者可从此处深入- 调用
TextSqlNode#createParser()
创建解析器,用以解析当前节点中保存的字符串拼接符${ew.sqlSegment}
等 - 调用
GenericTokenParser#parse()
方法通过OGNL 表达式
从参数值中取得对应字符串,例如拼接字符串${ew.sqlSegment}
,会通过反射调用到MyBatis-plus
中的AbstractWrapper#getSqlSegment()
方法完成 SQL 条件语句的构建,取到拼接字符串的替换值后通过DynamicContext#appendSql()
方法将其追加到 SQL 字符串尾部
@Override public boolean apply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; } private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); }
- 调用
-
经过以上步骤,SQL 语句脚本已经被处理为了如下的字符串,脚本中之前预存的
MyBatis-plus
的替换字符串${ew.xxx}
已经被替换掉,同时 WHERE 条件中已经填上了MyBatis-plus
中 Mapper 方法入参的QueryWrapper
构建的 sql 片段,可以看到 SQL 语句 WHERE 条件的实际参数位置已经被#{ew.paramNameValuePairs.xxx} 占位符
占住/*bind_db*/SELECT id,name,type FROM node WHERE (edge_id = #{ew.paramNameValuePairs.MPGENVAL1} AND status = #{ew.paramNameValuePairs.MPGENVAL2} AND deleted = #{ew.paramNameValuePairs.MPGENVAL3})
-
回到本节步骤3第二个处理阶段,
SqlSourceBuilder#parse()
方法会返回一个新的StaticSqlSource
对象,并将 SQL 语句中的#{}
占位符内部的字符串处理为获取参数值的参数映射对象ParameterMapping
,至此 SQL 语句已经更新为如下形式:/* bind_db */SELECT id,name,type FROM node WHERE (edge_id = ? AND status = ? AND deleted = ?)
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()); }
-
StaticSqlSource#getBoundSql()
方法其实就是新建一个BoundSql
对象返回,可以看到这个对象中已经包含了预处理的 sql 语句,实际参数值的缓存对象ParamMap
,以及取得参数值的映射路径ParameterMapping
列表,通过参数值的映射路径即可有方法取得对应参数值
@Override public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(configuration, sql, parameterMappings, parameterObject); }
-
实际对预处理的 sql 语句对应的
PreparedStatement
的参数填充在参数处理器ParameterHandler#setParameters()
子类实现中完成,这个处理由StatementHandler#parameterize()
触发,最终调用到DefaultParameterHandler#setParameters()
方法可以看到此处的处理是从
ParameterMapping
获取参数值的取值路径,然后使用这个路径借助MetaObject
从参数缓存对象parameterObject
中取得参数值,最后调用TypeHandler#setParameter()
将参数值注入到PreparedStatement
对象中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 | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }