SQL初始化被分为主要几个内容
SQL初始化以LanguageDriver接口为主要内容,以获得SqlSource为结束。主要涉及到的几个内容为
- LanguageDriver 语言驱动
- XMLScriptBuilder SqlSource的构造器
- LanguageDriverRegistry 注册表
- NodeHandler 节点处理
- DynamicContext 动态环境上下文
- SqlNode SQL节点
- SqlSource 的实现类
- BoundSql 可执行的SQL封装
- ParameterHandler 参数处理
- LanguageDriver 语言驱动
LanguageDriver
语言驱动目前主要有两种语言驱动,
- XMLLanguageDriver
- RawLanguageDriver
其接口代码是
/**
* 语言驱动接口
*/
public interface LanguageDriver {
/**
* 创建 ParameterHandler 对象。
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* 创建 SqlSource 对象,从 Mapper XML 配置的 Statement 标签中,即 <select /> 等。
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* 创建 SqlSource 对象,从方法注解配置,即 @Select 等。
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
XMLLanguageDriver
XML语言驱动实现类
RawLanguageDriver
RawSqlSource 语言驱动器实现类,确保创建的 SqlSource 是 RawSqlSource 类
createSqlSource 创建sql源
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 创建 XMLScriptBuilder 对象,执行解析
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
// 如果是 <script> 开头,使用 XML 配置的方式,使用动态 SQL
if (script.startsWith("<script>")) {
// 创建 XPathParser 对象,解析出 <script /> 节点
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
// 调用上面的 #createSqlSource(...) 方法,创建 SqlSource 对象
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
// 变量替换
script = PropertyParser.parse(script, configuration.getVariables());
// 创建 TextSqlNode 对象
TextSqlNode textSqlNode = new TextSqlNode(script);
// 如果是动态 SQL ,则创建 DynamicSqlSource 对象
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
// 如果非动态 SQL ,则创建 RawSqlSource 对象
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
其内部逻辑是使用XMLScriptBuilder对配置,XNode节点对象或者字符串对象和参数类型进行解析,如果是
XMLScriptBuilder
它是解析配置成SQL源的主要对象,后续对于这个类,我们还有很多东西需要学习,其构造方法和主要方法为
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
// 初始化 nodeHandlerMap 属性
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());
}
// 负责将 SQL 解析成 SqlSource 对象
public SqlSource parseScriptNode() {
// 解析SQL
MixedSqlNode rootSqlNode = parseDynamicTags(context);
// 创建 SqlSource 对象
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
- 首先在构造方法中可以看到context就是XNode对象,同时mybatis直接初始化了一些节点处理对象的映射(initNodeHandlerMap中)。
- parseScriptNode 是其主要的解析方法,其关键的几个方法是:
- parseDynamicTags 将XNode解析成MixedSqlNode,根据配置和MixedSqlNode 解析成DynamicSqlSource或者RawSqlSource
parseDynamicTags
// 解析 SQL 成 MixedSqlNode 对象
protected MixedSqlNode parseDynamicTags(XNode node) {
// 创建 SqlNode 数组
List<SqlNode> contents = new ArrayList<>();
// 遍历 SQL 节点的所有子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 当前子节点
XNode child = node.newXNode(children.item(i));
// 如果类型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 时
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获得内容
String data = child.getStringBody("");
// 创建TextSql对象
TextSqlNode textSqlNode = new TextSqlNode(data);
// 验证是否为动态SQL,主要是验证"${" "}",
if (textSqlNode.isDynamic()) {
// 添加到 contents 中
contents.add(textSqlNode);
// 标记为动态 SQ
isDynamic = true;
// 如果是非动态的 TextSqlNode 对象
} else {
// 创建 StaticTextSqlNode 添加到 contents 中
contents.add(new StaticTextSqlNode(data));
}
// 如果类型是 Node.ELEMENT_NODE
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
// 根据子节点的标签,获得对应的 NodeHandler 对象
NodeHandler handler = nodeHandlerMap.get(nodeName);
// 获得不到,说明是未知的标签,抛出 BuilderException 异常
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 执行 NodeHandler 处理
handler.handleNode(child, contents);
// 标记为动态 SQ
isDynamic = true;
}
}
// 创建 MixedSqlNode 对象
return new MixedSqlNode(contents);
}
其核心逻辑是,筛选出符合要去的节点然后通过MixedSqlNode对此节点进行一次报装,同时校验${}来判断是否动态参数
RawSqlSource指的是原始的SQL源,DynamicSqlSource指的是动态的数据源,其区别可以在getBoundSql看出来。
其继承了XMLLanguageDriver,主要是对SqlSource进行校验,保证为RawSqlSource类,其他逻辑都是使用父类方法
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// 调用父类,创建 SqlSource 对象
SqlSource source = super.createSqlSource(configuration, script, parameterType);
// 校验创建的是 RawSqlSource 对象
checkIsNotDynamic(source);
return source;
}
private void checkIsNotDynamic(SqlSource source) {
if (!RawSqlSource.class.equals(source.getClass())) {
throw new BuilderException("Dynamic content is not allowed when using RAW language");
}
}
LanguageDriverRegistry 驱动注册表
其内部维护了一个语言驱动类和语言驱动的映射表,里面逻辑都比较简单
private final Map<Class<? extends LanguageDriver>, LanguageDriver> LANGUAGE_DRIVER_MAP = new HashMap<>();
NodeHandler 节点处理接口
在上面的XMLScriptBuilder.initNodeHandlerMap中我们看到,他初始化了很多节点处理器,现在看下其主要内容
private interface NodeHandler {
// nodeToHandle 要处理的 XNode 节点
// targetContents 目标的 SqlNode 数组。
// 实际上,被处理的 XNode 节点会创建成对应的 SqlNode 对象,
// 添加到 targetContents 中
// 其实现类XXXHandler格式,其中XXX 为具体标签
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
他是XMLScriptBuilder的一个内部接口,用来对各种节点进行处理。
这里需要添加一个图片,显示其实现类
简单的查看其中一个实现
// trim标签
private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析内部SQL节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 获得 prefix、prefixOverrides、"suffix"、suffixOverrides 属性
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
// 创建 TrimSqlNode 对象 并 添加到targetContents中
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
其逻辑是将XNode使用parseDynamicTags解析成MixedSqlNode,然后从MixedSqlNode中读取需要的参数,将这些获取到的参数封装到处理方法对应的SqlNode中,然后保存到整体的SqlNode列表中。
具体实现类比较多,对应的SqlNode也比较多,但是每个handler相对独立,有兴趣的可以单独看一下,不算太麻烦。
DynamicContext 动态环境上下文
一个动态环境的上下文
而其构造函数可以看出
// 构造函数, 对传入的parameterObject对象进行“map”化处理;
public DynamicContext(Configuration configuration, Object parameterObject) {
// 当传入的参数对象不是Map类型时,Mybatis会将传入的POJO对象用MetaObject对象来封装
// 当动态计算sql过程需要获取数据时,用Map接口的get方法包装 MetaObject对象的取值过程。
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
// 添加 bindings 的默认值
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
ContextMap
ContextMap是其内部类,继承hashmap,从map中或者从其内部的MetaObject对象中取值
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
ContextAccessor
是 DynamicContext 的内部静态类,实现 ognl.PropertyAccessor 接口,上下文访问器
static {
// Mybatis中采用了Ognl来计算动态sql语句,DynamicContext类中的这个静态初始块,很好的说明了这一点
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
根据DynamicContext的静态代码块中可以制定,ContextAccessor是其处理上下文访问的工具
其取值逻辑可以看到
// ContextAccessor.java
@Override
public Object getProperty(Map context, Object target, Object name) {
Map map = (Map) target;
// 优先从 ContextMap 中,获得属性
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
// 如果没有,则从 PARAMETER_OBJECT_KEY 对应的 Map 中,获得属性
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
return null;
}
SqlNode SQL节点
每个 XML Node 会解析成对应的 SQL Node 对象
其主要作用就是讲SQLNode绑定到上下文中
/**
* SQL Node 接口,每个 XML Node 会解析成对应的 SQL Node 对象
* @author Clinton Begin
*/
public interface SqlNode {
// context 上下文
boolean apply(DynamicContext context);
}
写到这里要吐槽下有道笔记,竟然没有自动保存功能,昨天写完后直接合上电脑走了,第二天发现笔记都不见了,本来后面的内容我用2个小时写完了,现在全没了,自闭了自闭了
关于SqlNode.apply具体作用,我们可以根据调试过程来看一下,首先我们知道方法进来的才是是动态参数的上下文,那么最终实现的内容是什么呢?
// org/apache/ibatis/scripting/xmltags/IfSqlNode.java
@Override
public boolean apply(DynamicContext context) {
// 判断是否符合条件
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 符合执行contents的应用
contents.apply(context);
return true;
}
return false;
}
比如这个IfSqlNode可以看到最后使用的contents.apply(context),这个contents是什么?其实是另外一个SqlNode,那它最终会调用到哪个SqlNode呢?
根据调试的结果可以看出来
最终调用的是org/apache/ibatis/scripting/xmltags/TextSqlNode.java
而其中的逻辑是
@Override
public boolean apply(DynamicContext context) {
// 创建BindingTokenParser 和 GenericTokenParser对象
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 进行解析,并且将结果放入context中
context.appendSql(parser.parse(text));
return true;
}
可以理解,最终是将文本内容拼接到context,最终拼接成有变量占位符的SQL字符串。
而可以里面出contents.apply里面的逻辑主要是根据对应的表达式判断是否符合,假如符合就将内容拼接到SQL中。
SqlNode的代码嵌套的比较复杂,第一时间自己也没看出个端倪,只好调试一步一步的操作查看程序逻辑,所以我建议想去进一步了解mybatis对if,where,set等等标签是如何解析的,在这一步一定要通过调试,结合起来自学。
SqlSource 的实现类
根据XMLScriptBuilder.parseScriptNode的后续逻辑是开始获得SqlSource。
public interface SqlSource {
// 根据传入的参数对象,返回 BoundSql 对象
BoundSql getBoundSql(Object parameterObject);
}
SqlSource的方法只有一个就是获取BoundSql
SqlSource主要有四个实现类
- DynamicSqlSource
- ProviderSqlSource
- RawSqlSource
- StaticSqlSource
而根据逻辑这里主要学习DynamicSqlSource和RawSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
DynamicSqlSource
/**
* 实现 SqlSource 接口,动态的 SqlSource 实现类
* @author Clinton Begin
*/
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
// 根 SqlNode 对象
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 应用 rootSqlNode
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
// 创建 SqlSourceBuilder 对象
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 解析出 SqlSource 对象
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 获得 BoundSql 对象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 添加附加参数到 BoundSql 对象中
context.getBindings().forEach(boundSql::setAdditionalParameter);
// 返回 BoundSql 对象
return boundSql;
}
}
根据上面的逻辑可以看到,在传入参数数据对象的时候,
- mybatis通过配置和参数封装成DynamicContext。
- 然后获得Sql源的构建器,通过和参数的数据配合构建出SqlSource
- 最后通过参数替换获得最终的执行SQL
RawSqlSource
public class RawSqlSource implements SqlSource {
// SqlSource 对象
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
// 获得SQLsourceBuilder对象
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 获得sqlSource
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
// 创建 DynamicContext 对象
DynamicContext context = new DynamicContext(configuration, null);
// 绑定rootSqlNode
rootSqlNode.apply(context);
// 获得SQL
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 根据参数对象获得BoundSql
return sqlSource.getBoundSql(parameterObject);
}
}
可以看到和DynamicSqlSource相比,因为并不存在SQL拼接,所以逻辑要简单很多,在getBoundSql的时候只需要进行简单的参数替换就可以返回结果
BoundSql 可执行的SQL封装
public class BoundSql {
// SQL语句
private final String sql;
// 参数映射数组
private final List<ParameterMapping> parameterMappings;
// 参数对象
private final Object parameterObject;
// 附加的参数集合
private final Map<String, Object> additionalParameters;
// MetaObject 对象
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);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
一次可执行的SQL的封装,主要记录SQL,入参和结果的一些属性,都不算太复杂
ParameterHandler 参数处理
ParameterHandler主要进行参数的替换
public interface ParameterHandler {
// 获得参数对象
Object getParameterObject();
// 设置PreparedStatement的占位符
void setParameters(PreparedStatement ps)
throws SQLException;
}
其主要的实现类就是DefaultParameterHandler。
而其设置参数的主要逻辑是
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 遍历 ParameterMapping 数组
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
// 获得 ParameterMapping 对象
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
// 查询 ParameterMapping 值
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、jdbcType 属性
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);
}
}
}
}
}
整个逻辑大致可以分为几部分:
- 从boundSql中获得参数映射。
- 然后从boundSql或者传递来的参数,或者从configuration中获得和映射中符合条件的参数值。
- 然后通过值和类型一次替换掉PreparedStatement中的占位符。