概述
- 我们通常在mapper.xml中定义增删查改select|insert|update|delete的相关SQL,在对应的select|insert|update|delete节点中支持通过if,(choose, when, otherwise),(trim, where, set),foreach等内嵌节点来实现动态SQL定义,动态SQL的使用方法可以参考:动态 SQL
- 对于增删查改,在对应的select|insert|update|delete节点中,通过id属性来映射mapper接口的对应方法,其中mapper接口是通过父节点mapper的namespace来定义映射的。
- 由上篇文章:Mybatis源码分析(三):mapper.xml的解析及namespace与Mapper接口的映射
的分析可知,mapper.xml主要是通过builder包的xml子包的XMLMapperBuilder来解析,对应增删查改select|insert|update|delete节点的解析主要是在buildStatementFromContext方法中定义的。
增删查改节点解析:XMLStatementBuilder
-
buildStatementFromContext方法的定义如下:其中参数list为select|insert|update|delete对应的类型为XNode的节点集合。
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { // list为mapper.xml文件中SQL操作(select|insert|update|delete)节点列表 for (XNode context : list) { // 增删查改节点的解析器 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { // 解析mapper.xml文件中的相关SQL操作标签, // 并存放到configuration的mappedStatements集合中 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { // 如果存在解析异常,则放到incompleteStatements中 configuration.addIncompleteStatement(statementParser); } } }
-
在内部增删查改节点的解析主要是通过builder包的xml子包的XMLStatementBuilder来完成的,具体在XMLStatementBuilder的parseStatementNode方法中定义解析逻辑:
public void parseStatementNode() { // mapper接口的方法名 String id = context.getStringAttribute("id"); // fetchSize,parameterMap,parameterType,resultType,resultMap相关属性的解析 ... // flushCache,useCache缓存相关的属性的解析 ... // include节点解析,主要用于include相关的SQL片段,如需要select的列 ... // 解析SQL,使用SqlSource存放 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); // keyGenerator相关 ... // 构造当前增删查改节点对应的MappedStatement对象 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }
-
在parseStatementNode中,在解析好select|insert|update|delete节点相关的属性之后,最后通过builder包的MapperBuilderAssistant来创建对应的MappedStatement对象,然后保存该MappedStatement对象到Configuration的mappedStatements集合中。其中SqlSource是用来保存SQL节点的类,包括动态SQL节点。
public MappedStatement addMappedStatement(...参数列表) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; // MappedStatement构建器,组装select|insert|update|delete节点的属性值,构造SQL对应的MappedStatement对象 // 然后将该mappedStatement对象实例保存到Configuration的mappedStatements集合 MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); // 添加到configuration的mappedStatements集合中缓存起来 configuration.addMappedStatement(statement); return statement; }
-
Configuration的addMappedStatement方法实现:
public void addMappedStatement(MappedStatement ms) { // id对应mapper接口的方法名 mappedStatements.put(ms.getId(), ms); }
动态SQL实现
-
由以上分析可知,增删查改select|insert|update|delete对应的每个SQL节点对应一个mapping包的SqlSource类对象实例。SqlSource接口的定义如下:在应用代码调用mapper节点的方法时,在内部通过对应的MappedStatement调用其所关联的SqlSource的getBoundSql方法,获取需要执行的SQL语句。
// 保存在mapper.xml或者使用注解定义的mybatis风格的sql语句 public interface SqlSource { BoundSql getBoundSql(Object parameterObject); }
-
BoundSql的定义如下:其中sql属性就是实际执行的SQL语句。
// SQL语句对象类,对于动态参数已经替换为了SQL标准的问号?,并已经记录好了参数的顺序信息 public class BoundSql { private final String sql; // SQL语句,带问号?的SQL语句 private final List<ParameterMapping> parameterMappings; // SQL参数名 private final Object parameterObject; // 参数名和参数值映射集合 private final Map<String, Object> additionalParameters; private final MetaObject metaParameters; ... }
-
在XMLStatementBuilder的parseStatementNode方法中,通过scripting包的xmltags子包的LanguageDriver的createSqlSource方法来创建对应的SqlSource对象,对应xml的实现类为XMLLanguageDriver。XMLLanguageDriver的createSqlSource方法实现如下:通过scripting包的xmltags子包的XMLScriptBuilder的parseScriptNode方法来解析该增删查改对应的xml节点。
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); }
-
XMLScriptBuilder的构造函数实现如下:
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) { super(configuration); this.context = context; this.parameterType = parameterType; // 初始化子节点处理器 initNodeHandlerMap(); } // 初始化各子节点的处理器,并保存在nodeHandlerMap private void initNodeHandlerMap() { nodeHandlerMap.put("trim", new TrimHandler()); nodeHandlerMap.put("where", new WhereHandler()); nodeHandlerMap.put("set", new SetHandler()); nodeHandlerMap.put("foreach", new ForEachHandler()); nodeHandlerMap.put("if", new IfHandler()); nodeHandlerMap.put("choose", new ChooseHandler()); nodeHandlerMap.put("when", new IfHandler()); nodeHandlerMap.put("otherwise", new OtherwiseHandler()); nodeHandlerMap.put("bind", new BindHandler()); }
-
XMLScriptBuilder的parseScriptNode方法,定义解析增删改查节点,同时判断是否为动态SQL,如果是则使用
DynamicSqlSource来封装,否则使用RawSqlSource。public SqlSource parseScriptNode() { // 解析获取一个SqlNode节点结合,通过MixedSqlNode来封装 // 在parseDynamicTags方法中,会根据节点信息,判断是否为动态SQL,为isDynamic设值 MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource = null; if (isDynamic) { // 动态SQL,即包含if,foreach等节点的 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { // 普通静态SQL,不包含if,foreach等节点的,但可以存在使用#{}设置的动态参数 sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
-
动态SQL对应的DynamicSqlSource的定义如下:由parseScriptNode方法可知,rootSqlNode的类型为MixedSqlNode,MixedSqlNode包含了if,foreach等节点的集合,如if节点对应scripting包的xmltags子包的IfSqlNode。
// 动态SQL,即包含动态参数的SQL public class DynamicSqlSource implements SqlSource { private final Configuration configuration; private final SqlNode rootSqlNode; ... } public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; // 包含sql节点集合,如trim,where,if等 public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } ... } public class IfSqlNode implements SqlNode { private final ExpressionEvaluator evaluator; private final String test; private final SqlNode contents; ... }
-
其中是否为动态SQL的判断:对于纯文本,则根据是否存在使用${}来定义的动态SQL语句参数;对于SQL内部存在xml节点,如if, foreach等,则默认属于动态SQL;对于纯文本中存在#{}定义的字符串参数,不使用动态SQL。具体在parseDynamicTags方法中定义。
protected MixedSqlNode parseDynamicTags(XNode node) { List<SqlNode> contents = new ArrayList<SqlNode>(); NodeList children = node.getNode().getChildNodes(); for (int i = 0; i < children.getLength(); i++) { XNode child = node.newXNode(children.item(i)); // 纯文本SQL,则判断是否存在"${"和"}"对,则说明为动态SQL if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { String data = child.getStringBody(""); TextSqlNode textSqlNode = new TextSqlNode(data); // 如果是纯文本,即select|insert|update|delete节点内部,不存在if, foreach等子节点时, // 存在"${"和"}"对,则说明为动态SQL if (textSqlNode.isDynamic()) { contents.add(textSqlNode); isDynamic = true; } else { contents.add(new StaticTextSqlNode(data)); } // SQL内部包含xml节点,即if, foreach, choose等节点,则是动态SQL } 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); // 设置为true isDynamic = true; } } return new MixedSqlNode(contents); }
-
$ 与 # 的区别为:$是将传入的数据直接显示生成sql语句,#是将传入的值当做字符串的形式。如下:
select id,name,age from student where id =#{id}
#:select id,name,age from student where id ='1' $:select id,name,age from student where id = 1