Mapper
映射文件解析的最后一步是解析所有 statement
元素,即 select
搭盾、insert
、update
婉支、delete
元素鸯隅,這些元素中可能會包含動態(tài) SQL
,即使用 ${}
占位符或 if
向挖、choose
蝌以、where
等元素動態(tài)組成的 SQL
。動態(tài) SQL
功能正是 MyBatis
強(qiáng)大的所在何之,其解析過程也是十分復(fù)雜的跟畅。
解析工具
為了方便 statement
的解析,MyBatis
提供了一些解析工具溶推。
Token 解析
MyBatis
支持使用 ${}
或 #{}
類型的 token
作為動態(tài)參數(shù)徊件,不僅文本中可以使用 token
,xml
元素中的屬性等也可以使用悼潭。
GenericTokenParser
GenericTokenParser
是 MyBatis
提供的通用 token
解析器庇忌,其解析邏輯是根據(jù)指定的 token
前綴和后綴搜索 token
舞箍,并使用傳入的 TokenHandler
對文本進(jìn)行處理舰褪。
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token 搜索 token 前綴
int start = text.indexOf(openToken);
if (start == -1) {
// 沒有 token 前綴,返回原文本
return text;
}
char[] src = text.toCharArray();
// 當(dāng)前解析偏移量
int offset = 0;
// 已解析文本
final StringBuilder builder = new StringBuilder();
// 當(dāng)前占位符內(nèi)的表達(dá)式
StringBuilder expression = null;
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// 如果待解析屬性前綴被轉(zhuǎn)義疏橄,則去掉轉(zhuǎn)義字符占拍,加入已解析文本
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
// 更新解析偏移量
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
// 前綴前面的部分加入已解析文本
builder.append(src, offset, start - offset);
// 更新解析偏移量
offset = start + openToken.length();
// 獲取對應(yīng)的后綴索引
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// 后綴被轉(zhuǎn)義略就,加入已解析文本
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
// 尋找下一個后綴
end = text.indexOf(closeToken, offset);
} else {
// 找到后綴,獲取占位符內(nèi)的表達(dá)式
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// 找不到后綴晃酒,前綴之后的部分全部加入已解析文本
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 能夠找到后綴表牢,追加 token 處理器處理后的文本
builder.append(handler.handleToken(expression.toString()));
// 更新解析偏移量
offset = end + closeToken.length();
}
}
// 尋找下一個前綴,重復(fù)解析表達(dá)式
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
// 將最后的部分加入已解析文本
builder.append(src, offset, src.length - offset);
}
// 返回解析后的文本
return builder.toString();
}
由于 GenericTokenParser
的 token
前后綴和具體解析邏輯都是可指定的贝次,因此基于 GenericTokenParser
可以實現(xiàn)對不同 token
的定制化解析崔兴。
TokenHandler
TokenHandler
是 token
處理器抽象接口。實現(xiàn)此接口可以定義 token
以何種方式被解析蛔翅。
public interface TokenHandler {
/**
* 對 token 進(jìn)行解析
*
* @param content 待解析 token
* @return
*/
String handleToken(String content);
}
PropertyParser
PropertyParser
是 token
解析的一種具體實現(xiàn)敲茄,其指定對 ${}
類型 token
進(jìn)行解析,具體解析邏輯由其內(nèi)部類 VariableTokenHandler
實現(xiàn):
/**
* 對 ${} 類型 token 進(jìn)行解析
*
* @param string
* @param variables
* @return
*/
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
/**
* 根據(jù)配置屬性對 ${} token 進(jìn)行解析
*/
private static class VariableTokenHandler implements TokenHandler {
/**
* 預(yù)先設(shè)置的屬性
*/
private final Properties variables;
/**
* 是否運(yùn)行使用默認(rèn)值山析,默認(rèn)為 false
*/
private final boolean enableDefaultValue;
/**
* 默認(rèn)值分隔符號堰燎,即如待解析屬性 ${key:default},key 的默認(rèn)值為 default
*/
private final String defaultValueSeparator;
private VariableTokenHandler(Properties variables) {
this.variables = variables;
this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
}
private String getPropertyValue(String key, String defaultValue) {
return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
}
@Override
public String handleToken(String content) {
if (variables != null) {
String key = content;
if (enableDefaultValue) {
// 如待解析屬性 ${key:default}笋轨,key 的默認(rèn)值為 default
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
// 使用默認(rèn)值
return variables.getProperty(key, defaultValue);
}
}
if (variables.containsKey(key)) {
// 不使用默認(rèn)值
return variables.getProperty(key);
}
}
// 返回原文本
return "${" + content + "}";
}
}
VariableTokenHandler
實現(xiàn)了 TokenHandler
接口秆剪,其構(gòu)造方法允許傳入一組 Properties
用于獲取 token
表達(dá)式的值。如果開啟了使用默認(rèn)值,則表達(dá)式 ${key:default}
會在 key
沒有映射值的時候使用 default
作為默認(rèn)值薇芝。
特殊容器
StrictMap
Configuration
中的 StrictMap
繼承了 HashMap
焕阿,相對于 HashMap
,其存取鍵值的要求更為嚴(yán)格何什。put
方法不允許添加相同的 key
,并獲取最后一個 .
后的部分作為 shortKey
等龙,如果 shortKey
也重復(fù)了处渣,其會向容器中添加一個 Ambiguity
對象,當(dāng)使用 get
方法獲取這個 shortKey
對應(yīng)的值時蛛砰,就會拋出異常罐栈。get
方法對于不存在的 key
也會拋出異常。
public V put(String key, V value) {
if (containsKey(key)) {
// 重復(fù) key 異常
throw new IllegalArgumentException(name + " already contains value for " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
// 獲取最后一個 . 后的部分作為 shortKey
final String shortKey = getShortName(key);
// shortKey 不允許重復(fù)泥畅,否則在獲取時異常
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
public V get(Object key) {
V value = super.get(key);
if (value == null) {
// key 不存在拋異常
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
// 重復(fù)的 key 拋異常
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
ContextMap
ContextMap
是 DynamicContext
的靜態(tài)內(nèi)部類荠诬,用于保存 sql
上下文中的綁定參數(shù)。
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
/**
* 參數(shù)對象
*/
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
// 先根據(jù) key 查找原始容器
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
// 再進(jìn)入?yún)?shù)對象查找
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
OGNL 工具
OgnlCache
OGNL
工具支持通過字符串表達(dá)式調(diào)用 Java
方法位仁,但是其實現(xiàn)需要對 OGNL
表達(dá)式進(jìn)行編譯柑贞,為了提高性能,MyBatis
提供 OgnlCache
工具類用于對 OGNL
表達(dá)式編譯結(jié)果進(jìn)行緩存聂抢。
/**
* 根據(jù) ognl 表達(dá)式和參數(shù)計算值
*
* @param expression
* @param root
* @return
*/
public static Object getValue(String expression, Object root) {
try {
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
/**
* 編譯 ognl 表達(dá)式并放入緩存
*
* @param expression
* @return
* @throws OgnlException
*/
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
// 編譯 ognl 表達(dá)式
node = Ognl.parseExpression(expression);
// 放入緩存
expressionCache.put(expression, node);
}
return node;
}
ExpressionEvaluator
ExpressionEvaluator
是 OGNL
表達(dá)式計算工具钧嘶,evaluateBoolean
和 evaluateIterable
方法分別根據(jù)傳入的表達(dá)式和參數(shù)計算出一個 boolean
值或一個可迭代對象。
/**
* 計算 ognl 表達(dá)式 true / false
*
* @param expression
* @param parameterObject
* @return
*/
public boolean evaluateBoolean(String expression, Object parameterObject) {
// 根據(jù) ognl 表達(dá)式和參數(shù)計算值
Object value = OgnlCache.getValue(expression, parameterObject);
// true / false
if (value instanceof Boolean) {
return (Boolean) value;
}
// 不為 0
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
// 不為 null
return value != null;
}
/**
* 計算獲得一個可迭代的對象
*
* @param expression
* @param parameterObject
* @return
*/
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) {
// 已實現(xiàn) Iterable 接口
return (Iterable<?>) value;
}
if (value.getClass().isArray()) {
// 數(shù)組轉(zhuǎn)集合
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) {
// Map 獲取 entry
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
解析邏輯
MyBastis
中調(diào)用 XMLStatementBuilder#parseStatementNode
方法解析單個 statement
元素琳疏。此方法中除了逐個獲取元素屬性有决,還對 include
元素闸拿、selectKey
元素進(jìn)行解析,創(chuàng)建了 sql
生成對象 SqlSource
书幕,并將 statement
的全部信息聚合到 MappedStatement
對象中新荤。
public void parseStatementNode() {
// 獲取 id
String id = context.getStringAttribute("id");
// 自定義數(shù)據(jù)庫廠商信息
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
// 不符合當(dāng)前數(shù)據(jù)源對應(yīng)的數(shù)據(jù)廠商信息的語句不加載
return;
}
// 獲取元素名
String nodeName = context.getNode().getNodeName();
// 元素名轉(zhuǎn)為對應(yīng)的 SqlCommandType 枚舉
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 是否為查詢
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 獲取 flushCache 屬性,查詢默認(rèn)為 false台汇,其它默認(rèn)為 true
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 獲取 useCache 屬性苛骨,查詢默認(rèn)為 true,其它默認(rèn)為 false
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 獲取 resultOrdered 屬性苟呐,默認(rèn)為 false
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 解析 include 屬性
includeParser.applyIncludes(context.getNode());
// 參數(shù)類型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 獲取 Mapper 語法類型
String lang = context.getStringAttribute("lang");
// 默認(rèn)使用 XMLLanguageDriver
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
// 解析 selectKey 元素
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 獲取 KeyGenerator
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
// 獲取解析完成的 KeyGenerator 對象
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 如果開啟了 useGeneratedKeys 屬性智袭,并且為插入類型的 sql 語句、配置了 keyProperty 屬性掠抬,則可以批量自動設(shè)置屬性
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 生成有效 sql 語句和參數(shù)綁定對象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// sql 類型
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 分批獲取數(shù)據(jù)的數(shù)量
Integer fetchSize = context.getIntAttribute("fetchSize");
// 執(zhí)行超時時間
Integer timeout = context.getIntAttribute("timeout");
// 參數(shù)映射
String parameterMap = context.getStringAttribute("parameterMap");
// 返回值類型
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
// 返回值映射 map
String resultMap = context.getStringAttribute("resultMap");
// 結(jié)果集類型
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 插入吼野、更新生成鍵值的字段
String keyProperty = context.getStringAttribute("keyProperty");
// 插入、更新生成鍵值的列
String keyColumn = context.getStringAttribute("keyColumn");
// 指定多結(jié)果集名稱
String resultSets = context.getStringAttribute("resultSets");
// 新增 MappedStatement
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
語法驅(qū)動
LanguageDriver
是 statement
創(chuàng)建語法驅(qū)動两波,默認(rèn)實現(xiàn)為 XMLLanguageDriver
瞳步,其提供 createSqlSource
方法用于使用 XMLScriptBuilder
創(chuàng)建 sql
生成對象。
遞歸解析 include
include
元素是 statement
元素的子元素腰奋,通過 refid
屬性可以指向在別處定義的 sql fragments
单起。
public void applyIncludes(Node source) {
Properties variablesContext = new Properties();
Properties configurationVariables = configuration.getVariables();
// 拷貝全局配置中設(shè)置的額外配置屬性
Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
applyIncludes(source, variablesContext, false);
}
/**
* 遞歸解析 statement 元素中的 include 元素
*
* Recursively apply includes through all SQL fragments.
* @param source Include node in DOM tree
* @param variablesContext Current context for static variables with values
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
// include 元素,從全局配置中找對應(yīng)的 sql 節(jié)點(diǎn)并 clone
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
// 讀取 include 子元素中的 property 元素劣坊,獲取全部屬性
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
// include 指向的 sql clone 節(jié)點(diǎn)嘀倒,逐個對屬性進(jìn)行解析
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
// statement 元素中可能包含 include 子元素
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// replace variables in text node
// 替換元素值,如果使用了 ${} 占位符局冰,會對 token 進(jìn)行解析
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
/**
* 從全局配置中找對應(yīng)的 sql fragment
*
* @param refid
* @param variables
* @return
*/
private Node findSqlFragment(String refid, Properties variables) {
// 解析 refid
refid = PropertyParser.parse(refid, variables);
// namespace.refid
refid = builderAssistant.applyCurrentNamespace(refid, true);
try {
// 從全局配置中找對應(yīng)的 sql fragment
XNode nodeToInclude = configuration.getSqlFragments().get(refid);
return nodeToInclude.getNode().cloneNode(true);
} catch (IllegalArgumentException e) {
// sql fragments 定義在全局配置中的 StrictMap 中测蘑,獲取不到會拋出異常
throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
}
}
private String getStringAttribute(Node node, String name) {
return node.getAttributes().getNamedItem(name).getNodeValue();
}
/**
* Read placeholders and their values from include node definition.
*
* 讀取 include 子元素中的 property 元素
* @param node Include node instance
* @param inheritedVariablesContext Current context used for replace variables in new variables values
* @return variables context from include instance (no inherited values)
*/
private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
Map<String, String> declaredProperties = null;
// 解析 include 元素中的 property 子元素
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node n = children.item(i);
if (n.getNodeType() == Node.ELEMENT_NODE) {
// include 運(yùn)行包含 property 元素
String name = getStringAttribute(n, "name");
// Replace variables inside
String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
if (declaredProperties == null) {
declaredProperties = new HashMap<>();
}
if (declaredProperties.put(name, value) != null) {
// 不允許添加同名屬性
throw new BuilderException("Variable " + name + " defined twice in the same include definition");
}
}
}
if (declaredProperties == null) {
return inheritedVariablesContext;
} else {
// 聚合屬性配置
Properties newProperties = new Properties();
newProperties.putAll(inheritedVariablesContext);
newProperties.putAll(declaredProperties);
return newProperties;
}
}
在開始解析前,從全局配置中獲取全部的屬性配置康二,如果 include
元素中有 property
元素碳胳,解析并獲取鍵值,放入 variablesContext
中沫勿,在后續(xù)處理中針對可能出現(xiàn)的 ${}
類型 token
使用 PropertyParser
進(jìn)行解析挨约。
因為解析 statement
元素前已經(jīng)加載過 sql
元素,因此會根據(jù) include
元素的 refid
屬性查找對應(yīng)的 sql fragments
产雹,如果全局配置中無法找到就會拋出異常诫惭;如果能夠找到則克隆 sql
元素并插入到當(dāng)前 xml
文檔中。
解析 selectKey
selectKey
用于指定 sql
在 insert
或 update
語句執(zhí)行前或執(zhí)行后生成或獲取列值蔓挖,在 MyBatis
中 selectKey
也被當(dāng)做 statement
語句進(jìn)行解析并設(shè)置到全局配置中夕土。單個 selectKey
元素會以SelectKeyGenerator
對象的形式進(jìn)行保存用于后續(xù)調(diào)用。
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// 返回值類型
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 對應(yīng)字段名
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
// 對應(yīng)列名
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
// 是否在父sql執(zhí)行前執(zhí)行
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
//defaults
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
// 創(chuàng)建 sql 生成對象
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
// 將 KeyGenerator 生成 sql 作為 MappedStatement 加入全局對象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
// 包裝為 SelectKeyGenerator 對象
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
在 selectKey
解析完成后时甚,按指定的 namespace
規(guī)則從全局配置中獲取 SelectKeyGenerator
對象隘弊,等待創(chuàng)建 MappedStatement
對象。如果未指定 selectKey
元素荒适,但是全局配置中開啟了 useGeneratedKeys
梨熙,并且指定 insert
元素的 useGeneratedKeys
屬性為 true
,則 MyBatis
會指定 Jdbc3KeyGenerator
作為 useGeneratedKeys
的默認(rèn)實現(xiàn)刀诬。
創(chuàng)建 sql 生成對象
SqlSource
SqlSource
是 sql
生成抽象接口咽扇,其提供 getBoundSql
方法用于根據(jù)參數(shù)生成有效 sql
語句和參數(shù)綁定對象 BoundSql
。在生成 statement
元素的解析結(jié)果 MappedStatement
對象前陕壹,需要先創(chuàng)建 sql
生成對象质欲,即 SqlSource
對象。
public interface SqlSource {
/**
* 根據(jù)參數(shù)生成有效 sql 語句和參數(shù)綁定對象
*
* @param parameterObject
* @return
*/
BoundSql getBoundSql(Object parameterObject);
}
SqlNode
SqlNode
是 sql
節(jié)點(diǎn)抽象接口糠馆。sql
節(jié)點(diǎn)指的是 statement
中的組成部分嘶伟,如果簡單文本、if
元素又碌、where
元素等九昧。SqlNode
提供 apply
方法用于判斷當(dāng)前 sql
節(jié)點(diǎn)是否可以加入到生效的 sql
語句中。
public interface SqlNode {
/**
* 根據(jù)條件判斷當(dāng)前 sql 節(jié)點(diǎn)是否可以加入到生效的 sql 語句中
*
* @param context
* @return
*/
boolean apply(DynamicContext context);
}
DynamicContext
DynamicContext
是動態(tài) sql
上下文毕匀,用于保存綁定參數(shù)和生效 sql
節(jié)點(diǎn)铸鹰。DynamicContext
使用 ContextMap
作為參數(shù)綁定容器。由于動態(tài) sql
是根據(jù)參數(shù)條件組合生成 sql
皂岔,DynamicContext
還提供了對 sqlBuilder
修改和訪問方法蹋笼,用于添加有效 sql
節(jié)點(diǎn)和生成 sql
文本。
/**
* 生效的 sql 部分躁垛,以空格相連
*/
private final StringJoiner sqlBuilder = new StringJoiner(" ");
public void appendSql(String sql) {
sqlBuilder.add(sql);
}
public String getSql() {
return sqlBuilder.toString().trim();
}
節(jié)點(diǎn)解析
將 statement
元素轉(zhuǎn)為 sql
生成對象依賴于 LanguageDriver
的 createSqlSource
方法剖毯,此方法中創(chuàng)建 XMLScriptBuilder
對象,并調(diào)用 parseScriptNode
方法對 sql
組成節(jié)點(diǎn)逐個解析并進(jìn)行組合教馆。
public SqlSource parseScriptNode() {
// 遞歸解析各 sql 節(jié)點(diǎn)
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 動態(tài) sql
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 原始文本 sql
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
/**
* 處理 statement 各 SQL 組成部分速兔,并進(jìn)行組合
*/
protected MixedSqlNode parseDynamicTags(XNode node) {
// SQL 各組成部分
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) {
// 解析 sql 文本
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
// 判斷是否為動態(tài) sql,包含 ${} 占位符即為動態(tài) sql
contents.add(textSqlNode);
isDynamic = true;
} else {
// 靜態(tài) sql 元素
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.");
}
// 根據(jù)子元素標(biāo)簽類型使用對應(yīng)的處理器處理子元素
handler.handleNode(child, contents);
// 包含標(biāo)簽元素活玲,認(rèn)定為動態(tài) SQL
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
parseDynamicTags
方法會對 sql
各組成部分進(jìn)行分解涣狗,如果 statement
元素包含 ${}
類型 token
或含有標(biāo)簽子元素,則認(rèn)為當(dāng)前 statement
是動態(tài) sql
舒憾,隨后 isDynamic
屬性會被設(shè)置為 true
镀钓。對于文本節(jié)點(diǎn),如 sql
純文本和僅含 ${}
類型 token
的文本镀迂,會被包裝為 StaticTextSqlNode
或 TextSqlNode
加入到 sql
節(jié)點(diǎn)容器中丁溅,而其它元素類型的 sql
節(jié)點(diǎn)會經(jīng)過 NodeHandler
的 handleNode
方法處理過之后才能加入到節(jié)點(diǎn)容器中。nodeHandlerMap
定義了不同動態(tài) sql
元素節(jié)點(diǎn)與 NodeHandler
的關(guān)系:
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());
}
MixedSqlNode
MixedSqlNode
中定義了一個 SqlNode
集合探遵,用于保存 statement
中包含的全部 sql
節(jié)點(diǎn)窟赏。其生成有效 sql
的邏輯為逐個判斷節(jié)點(diǎn)是否有效妓柜。
/**
* 組合 SQL 各組成部分
*
* @author Clinton Begin
*/
public class MixedSqlNode implements SqlNode {
/**
* SQL 各組裝成部分
*/
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
// 逐個判斷各個 sql 節(jié)點(diǎn)是否能生效
contents.forEach(node -> node.apply(context));
return true;
}
}
StaticTextSqlNode
StaticTextSqlNode
中僅包含靜態(tài) sql
文本,在組裝時會直接追加到 sql
上下文的有效 sql
中:
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
TextSqlNode
TextSqlNode
中的 sql
文本包含 ${}
類型 token
涯穷,使用 GenericTokenParser
搜索到 token
后會使用 BindingTokenParser
對 token
進(jìn)行解析棍掐,解析后的文本會被追加到生效 sql
中。
@Override
public boolean apply(DynamicContext context) {
// 搜索 ${} 類型 token 節(jié)點(diǎn)
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 解析 token 并追加解析后的文本到生效 sql 中
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
// 獲取綁定參數(shù)
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 計算 ognl 表達(dá)式的值
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
}
IfSqlNode
if
標(biāo)簽用于在 test
條件生效時才追加標(biāo)簽內(nèi)的文本拷况。
...
<if test="userId > 0">
AND user_id = #{userId}
</if>
IfSqlNode
保存了 if
元素下的節(jié)點(diǎn)內(nèi)容和 test
表達(dá)式作煌,在生成有效 sql
時會根據(jù) OGNL
工具計算 test
表達(dá)式是否生效。
@Override
public boolean apply(DynamicContext context) {
// 根據(jù) test 表達(dá)式判斷當(dāng)前節(jié)點(diǎn)是否生效
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
TrimSqlNode
trim
標(biāo)簽用于解決動態(tài) sql
中由于條件不同不能拼接正確語法的問題赚瘦。
SELECT * FROM test
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="a > 0">
a = #{a}
</if>
<if test="b > 0">
OR b = #粟誓
</if>
<if test="c > 0">
AND c = #{c}
</if>
</trim>
如果沒有 trim
標(biāo)簽,這個 statement
的有效 sql
最終可能會是這樣的:
SELECT * FROM test OR b = #起意
但是加上 trim
標(biāo)簽鹰服,生成的 sql
語法是正確的:
SELECT * FROM test WHERE b = #
prefix
屬性用于指定 trim
節(jié)點(diǎn)生成的 sql
語句的前綴揽咕,prefixOverrides
則會指定生成的 sql
語句的前綴需要去除的部分获诈,多個需要去除的前綴可以使用 |
隔開。suffix
與 suffixOverrides
的功能類似心褐,但是作用于后綴舔涎。
TrimSqlNode
首先調(diào)用 parseOverrides
對 prefixOverrides
和 suffixOverrides
進(jìn)行解析,通過 |
分隔逗爹,分別加入字符串集合亡嫌。
private static List<String> parseOverrides(String overrides) {
if (overrides != null) {
// 解析 token,按 | 分隔
final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
final List<String> list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
// 保存為字符串集合
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}
在調(diào)用包含的 SqlNode
的 apply
方法后還會調(diào)用 FilteredDynamicContext
的 applyAll
方法處理前綴和后綴掘而。
@Override
public boolean apply(DynamicContext context) {
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
// 加上前綴和和后綴挟冠,并去除多余字段
filteredDynamicContext.applyAll();
return result;
}
對于已經(jīng)生成的 sql
文本,分別根據(jù)規(guī)則加上和去除指定前綴和后綴袍睡。
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());
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
// 文本最前去除多余字段
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// 在文本最前插入前綴和空格
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
// 文本最后去除多余字段
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
// 文本最后插入空格和后綴
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
WhereSqlNode
where
元素與 trim
元素的功能類似,區(qū)別在于 where
元素不提供屬性配置可以處理的前綴和后綴斑胜。
...
<where>
...
</where>
WhereSqlNode
繼承了 TrimSqlNode
控淡,并指定了需要添加和刪除的前綴。
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
// 默認(rèn)添加 WHERE 前綴止潘,去除 AND掺炭、OR 等前綴
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
因此,生成的 sql
語句會自動在最前加上 WHERE
凭戴,并去除前綴中包含的 AND
涧狮、OR
等字符串。
SetSqlNode
set
標(biāo)簽用于 update
語句中。
UPDATE test
<set>
<if test="a > 0">
a = #{a},
</if>
<if test="b > 0">
b = #者冤
</if>
</set>
SetSqlNode
同樣繼承自 TrimSqlNode
肤视,并指定默認(rèn)添加 SET
前綴,去除 ,
前綴和后綴涉枫。
public class SetSqlNode extends TrimSqlNode {
private static final List<String> COMMA = Collections.singletonList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
// 默認(rèn)添加 SET 前綴邢滑,去除 , 前綴和后綴
super(configuration, contents, "SET", COMMA, null, COMMA);
}
}
ForEachSqlNode
foreach
元素用于指定對集合循環(huán)添加 sql
語句。
...
<foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
AND itm = #{item} AND idx #{index}
</foreach>
ForEachSqlNode
解析生成有效 sql
的邏輯如下拜银,除了計算 collection
表達(dá)式的值殊鞭、添加前綴遭垛、后綴外尼桶,還將參數(shù)與索引進(jìn)行了綁定。
@Override
public boolean apply(DynamicContext context) {
// 獲取綁定參數(shù)
Map<String, Object> bindings = context.getBindings();
// 計算 ognl 表達(dá)式獲取可迭代對象
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
// 添加動態(tài)語句前綴
applyOpen(context);
// 迭代索引
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
// 首個元素
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
// entry 集合項索引為 key锯仪,集合項為 value
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
// 綁定集合項索引關(guān)系
applyIndex(context, i, uniqueNumber);
// 綁定集合項關(guān)系
applyItem(context, o, uniqueNumber);
}
// 對解析的表達(dá)式進(jìn)行替換泵督,如 idx = #{index} AND itm = #{item} 替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
// 添加動態(tài)語句后綴
applyClose(context);
// 移除原始的表達(dá)式
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
/**
* 綁定集合項索引關(guān)系
*
* @param context
* @param o
* @param i
*/
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
/**
* 綁定集合項關(guān)系
*
* @param context
* @param o
* @param i
*/
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
context.bind(item, o);
context.bind(itemizeItem(item, i), o);
}
}
對于循環(huán)中的 #{}
類型 token
,ForEachSqlNode
在內(nèi)部類 FilteredDynamicContext
中定義了解析規(guī)則:
@Override
public void appendSql(String sql) {
GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
// 對解析的表達(dá)式進(jìn)行替換庶喜,如 idx = #{index} AND itm = #{item} 替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return "#{" + newContent + "}";
});
delegate.appendSql(parser.parse(sql));
}
類似 idx = #{index} AND itm = #{item}
會被替換為 idx = #{__frch_index_1} AND itm = #{__frch_item_1}
小腊,而 ForEachSqlNode
也做了參數(shù)與索引的綁定,因此在替換時可以快速綁定參數(shù)久窟。
ChooseSqlNode
choose
元素用于生成帶默認(rèn) sql
文本的語句秩冈,當(dāng) when
元素中的條件都不生效,就可以使用 otherwise
元素的默認(rèn)文本斥扛。
...
<choose>
<when test="a > 0">
AND a = #{a}
</when>
<otherwise>
AND b = #入问
</otherwise>
</choose>
ChooseSqlNode
是由 choose
節(jié)點(diǎn)和 otherwise
節(jié)點(diǎn)組合而成的,在生成有效 sql
于語句時會逐個計算 when
節(jié)點(diǎn)的 test
表達(dá)式稀颁,如果返回 true
則生效當(dāng)前 when
語句中的 sql
芬失。如果均不生效則使用 otherwise
語句對應(yīng)的默認(rèn) sql
文本。
@Override
public boolean apply(DynamicContext context) {
// when 節(jié)點(diǎn)根據(jù) test 表達(dá)式判斷是否生效
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// when 節(jié)點(diǎn)如果都未生效匾灶,且存在 otherwise 節(jié)點(diǎn)棱烂,則使用 otherwise 節(jié)點(diǎn)
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
VarDeclSqlNode
bind
元素用于綁定一個 OGNL
表達(dá)式到一個動態(tài) sql
變量中。
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
VarDeclSqlNode
會計算表達(dá)式的值并將參數(shù)名和值綁定到參數(shù)容器中阶女。
@Override
public boolean apply(DynamicContext context) {
// 解析 ognl 表達(dá)式
final Object value = OgnlCache.getValue(expression, context.getBindings());
// 綁定參數(shù)
context.bind(name, value);
return true;
}
創(chuàng)建解析對象與生成可執(zhí)行 sql
statemen
解析完畢后會創(chuàng)建 MappedStatement
對象颊糜,statement
的相關(guān)屬性以及生成的 sql
創(chuàng)建對象都會被保存到該對象中。MappedStatement
還提供了 getBoundSql
方法用于獲取可執(zhí)行 sql 和參數(shù)綁定對象秃踩,即 BoundSql
對象芭析。
public BoundSql getBoundSql(Object parameterObject) {
// 生成可執(zhí)行 sql 和參數(shù)綁定對象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 獲取參數(shù)映射
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)
// 檢查是否有嵌套的 resultMap
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;
}
BoundSql
對象由 DynamicSqlSource
的 getBoundSql
方法生成,在驗證各個 sql
節(jié)點(diǎn)吞瞪,生成了有效 sql
后會繼續(xù)調(diào)用 SqlSourceBuilder
將 sql
解析為 StaticSqlSource
馁启,即可執(zhí)行 sql
。
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 驗證各 sql 節(jié)點(diǎn),生成有效 sql
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 將生成的 sql 文本解析為 StaticSqlSource
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
此時的 sql
文本中仍包含 #{}
類型 token
惯疙,需要通過 ParameterMappingTokenHandler
進(jìn)行解析翠勉。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 創(chuàng)建 #{} 類型 token 搜索對象
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析 token
String sql = parser.parse(originalSql);
// 創(chuàng)建靜態(tài) sql 生成對象,并綁定參數(shù)
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
token
的具體解析邏輯為根據(jù)表達(dá)式的參數(shù)名生成對應(yīng)的參數(shù)映射對象霉颠,并將表達(dá)式轉(zhuǎn)為預(yù)編譯 sql
的占位符 ?
对碌。
@Override
public String handleToken(String content) {
// 創(chuàng)建參數(shù)映射對象
parameterMappings.add(buildParameterMapping(content));
// 將表達(dá)式轉(zhuǎn)為預(yù)編譯 sql 占位符
return "?";
}
最終解析完成的 sql
與參數(shù)映射關(guān)系集合包裝為 StaticSqlSource
對象,該對象在隨后的邏輯中通過構(gòu)造方法創(chuàng)建了 BoundSql
對象蒿偎。
接口解析
除了使用 xml
方式配置 statement
朽们,MyBatis
同樣支持使用 Java
注解配置。但是相對于 xml
的映射方式诉位,將動態(tài) sql
寫在 Java
代碼中是不合適的骑脱。如果在配置文件中指定了需要注冊 Mapper
接口的類或包,MyBatis
會掃描相關(guān)類進(jìn)行注冊苍糠;在 Mapper
文件解析完成后也會嘗試加載 namespace
的同名類叁丧,如果存在,則注冊為 Mapper
接口岳瞭。
無論是綁定還是直接注冊 Mapper
接口拥娄,都是調(diào)用 MapperAnnotationBuilder#parse
方法來解析的。此方法中的解析方式與上述 xml
解析方式大致相同瞳筏,區(qū)別只在于相關(guān)配置參數(shù)是從注解中獲取而不是從 xml
元素屬性中獲取稚瘾。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// 不允許相同接口重復(fù)注冊
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
小結(jié)
statement
解析的最終目的是為每個 statement
創(chuàng)建一個 MappedStatement
對象保存相關(guān)定義,在 sql
執(zhí)行時根據(jù)傳入?yún)?shù)動態(tài)獲取可執(zhí)行 sql
和參數(shù)綁定對象姚炕。
-
org.apache.ibatis.builder.xml.XMLStatementBuilder
:解析Mapper
文件中的select|insert|update|delete
元素摊欠。 -
org.apache.ibatis.parsing.GenericTokenParser.GenericTokenParser
:搜索指定格式token
并進(jìn)行解析。 -
org.apache.ibatis.parsing.TokenHandler
:token
處理器抽象接口钻心。定義token
以何種方式被解析凄硼。 -
org.apache.ibatis.parsing.PropertyParser
:${}
類型token
解析器。 -
org.apache.ibatis.session.Configuration.StrictMap
:封裝HashMap
捷沸,對鍵值存取有嚴(yán)格要求摊沉。 -
org.apache.ibatis.builder.xml.XMLIncludeTransformer
:include
元素解析器。 -
org.apache.ibatis.mapping.SqlSource
:sql
生成抽象接口痒给。根據(jù)傳入?yún)?shù)生成有效sql
語句和參數(shù)綁定對象说墨。 -
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
:解析statement
各個sql
節(jié)點(diǎn)并進(jìn)行組合。 -
org.apache.ibatis.scripting.xmltags.SqlNode
:sql
節(jié)點(diǎn)抽象接口苍柏。用于判斷當(dāng)前sql
節(jié)點(diǎn)是否可以加入到生效的 sql 語句中尼斧。 -
org.apache.ibatis.scripting.xmltags.DynamicContext
:動態(tài)sql
上下文。用于保存綁定參數(shù)和生效sql
節(jié)點(diǎn)试吁。 -
org.apache.ibatis.scripting.xmltags.OgnlCache
:ognl
緩存工具棺棵,緩存表達(dá)式編譯結(jié)果楼咳。 -
org.apache.ibatis.scripting.xmltags.ExpressionEvaluator
:ognl
表達(dá)式計算工具。 -
org.apache.ibatis.scripting.xmltags.MixedSqlNode
:sql
節(jié)點(diǎn)組合對象烛恤。 -
org.apache.ibatis.scripting.xmltags.StaticTextSqlNode
:靜態(tài)sql
節(jié)點(diǎn)對象母怜。 -
org.apache.ibatis.scripting.xmltags.TextSqlNode
:${}
類型sql
節(jié)點(diǎn)對象。 -
org.apache.ibatis.scripting.xmltags.IfSqlNode
:if
元素sql
節(jié)點(diǎn)對象缚柏。 -
org.apache.ibatis.scripting.xmltags.TrimSqlNode
:trim
元素sql
節(jié)點(diǎn)對象苹熏。 -
org.apache.ibatis.scripting.xmltags.WhereSqlNode
:where
元素sql
節(jié)點(diǎn)對象。 -
org.apache.ibatis.scripting.xmltags.SetSqlNode
:set
元素sql
節(jié)點(diǎn)對象币喧。 -
org.apache.ibatis.scripting.xmltags.ForEachSqlNode
:foreach
元素sql
節(jié)點(diǎn)對象轨域。 -
org.apache.ibatis.scripting.xmltags.ChooseSqlNode
:choose
元素sql
節(jié)點(diǎn)對象。 -
org.apache.ibatis.scripting.xmltags.VarDeclSqlNode
:bind
元素sql
節(jié)點(diǎn)對象杀餐。 -
org.apache.ibatis.mapping.MappedStatement
:statement
解析對象干发。 -
org.apache.ibatis.mapping.BoundSql
:可執(zhí)行sql
和參數(shù)綁定對象。 -
org.apache.ibatis.scripting.xmltags.DynamicSqlSource
:根據(jù)參數(shù)動態(tài)生成有效sql
和綁定參數(shù)怜浅。 -
org.apache.ibatis.builder.SqlSourceBuilder
:解析#{}
類型token
并綁定參數(shù)對象铐然。