本篇文章重點闡述一些動態sql的技術細節,#{name}和${name}的區別,將在本篇文章中揭曉,也許讀者早已了解它們之間的區別,但是,作為技術內幕,我們不僅要了解它們的區別,還要介紹它們的作業原理,
我在這里分享一個,有很多干貨,包含jvm,netty,spring,執行緒,spring cloud等詳細講解,也有詳細的學習規劃圖,面試題整理等,我感覺在面試這塊講的非常清楚:獲取面試資料只需:點擊這里領取!!! 暗號:CSDN
1. #{name}和${name}的區別,
#{name}:表示這是一個引數(ParameterMapping)占位符,值來自于運行時傳遞給sql的引數,也就是XXXMapper.xml里的parameterType,其值通過PreparedStatement的setObject()等方法賦值,
動態sql中的標簽系結的值,也是使用#{name}來使用的,
#{name}用在sql文本中,
n a m e : 表 示 這 是 一 個 屬 性 配 置 占 位 符 , 值 來 自 于 屬 性 配 置 文 件 , 比 如 j d b c . p r o p e r t i e s , 其 值 通 過 類 似 r e p l a c e 方 法 進 行 靜 態 替 換 , 比 如 {name}:表示這是一個屬性配置占位符,值來自于屬性組態檔,比如jdbc.properties,其值通過類似replace方法進行靜態替換,比如 name:表示這是一個屬性配置占位符,值來自于屬性配置文件,比如jdbc.properties,其值通過類似replace方法進行靜態替換,比如{driver},將被靜態替換為com.mysql.jdbc.Driver,
${name}則可以用在xml的Attribute屬性,還可以用在sql文本當中,
<select id="countAll" resultType="${driver}">
select count(1) from (
select
stud_id as studId
, name, email
, dob
, phone
from students #{offset}, ${driver}
) tmp
</select>
2. ${name}的作業原理
org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分原始碼,
public void parseStatementNode() {
//...
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// ...
}
org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)部分原始碼,
private void applyIncludes(Node source, final Properties variablesContext) {
if (source.getNodeName().equals("include")) {
// new full context for included SQL - contains inherited context and new variables from current include node
Properties fullContext;
String refid = getStringAttribute(source, "refid");
// replace variables in include refid value
refid = PropertyParser.parse(refid, variablesContext);
Node toInclude = findSqlFragment(refid);
Properties newVariablesContext = getVariablesContext(source, variablesContext);
if (!newVariablesContext.isEmpty()) {
// merge contexts
fullContext = new Properties();
fullContext.putAll(variablesContext);
fullContext.putAll(newVariablesContext);
} else {
// no new context - use inherited fully
fullContext = variablesContext;
}
applyIncludes(toInclude, fullContext);
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) {
NodeList children = source.getChildNodes();
for (int i=0; i<children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext);
}
} else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
// replace variables in all attribute values
// 通過PropertyParser替換所有${xxx}占位符(attribute屬性)
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
} else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
// replace variables ins all text nodes
// 通過PropertyParser替換所有${xxx}占位符(文本節點)
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
也就是說,Mybatis在決議標簽時,就已經靜態替換${name}占位符了,
public class PropertyParser {
private PropertyParser() {
// Prevent Instantiation
}
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
@Override
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
}
3. #{name}的作業原理
#{name}是ParameterMapping引數占位符,Mybatis將會把#{name}替換為?號,并通過OGNL來計算#{xxx}內部的OGNL運算式的值,作為PreparedStatement的setObject()的引數值,
舉例:#{item.name}將被替換為sql的?號占位符,item.name則是OGNL運算式,OGNL將計算item.name的值,作為sql的?號占位符的值,
如果只有靜態sql,#{name}將在決議xml檔案時,完成替換為?占位符,如果有動態sql的內容,#{name}將在執行sql時,動態替換為?占位符,
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseScriptNode(),
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
public class RawSqlSource implements 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 sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 在這里完成#{xxx}替換為?號
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
// 創建RawSqlSource時,就完成sql的拼接作業,因為它沒有動態sql的內容,Mybatis初始化時,就能確定最終的sql,
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
org.apache.ibatis.builder.SqlSourceBuilder.parse(String, Class<?>, Map<String, Object>),
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 使用ParameterMappingTokenHandler策略來處理#{xxx}
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
GenericTokenParser.java是通用決議占位符的工具類,它可以決議KaTeX parse error: Expected 'EOF', got '#' at position 8: {name}和#?{name},那么,決議到{name}和#{name}后,要如何處理這樣的占位符,則由不同的策略TokenHandler來完成,
4. TokenHandler
GenericTokenParser.java負責決議sql中的占位符${name}和#{name},TokenHandler則是如何處理這些占位符,
ParameterMappingTokenHandler:處理#{xxx}占位符,
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
private Class<?> parameterType;
private MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
// 創建一個ParameterMapping物件,并回傳?號占位符
parameterMappings.add(buildParameterMapping(content));
return "?";
}
//..
}
VariableTokenHandler:處理${xxx}占位符,
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
@Override
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
DynamicCheckerTokenParser:空實作,動態sql標簽,都由它來標識,
BindingTokenParser:用于在注解Annotation中處理${xxx},待研究,
至此,${name}將直接替換為靜態Properties的靜態屬性值,而#{name}將被替換為?號,并同時創建了ParameterMapping物件,系結到引數串列中,
5. DynamicSqlSource生成sql的原理
對于RawSqlSource,由于是靜態的sql,Mybatis初始化時就生成了最終可以直接使用的sql陳述句,即在創建RawSqlSource時,就直接生成,而DynamicSqlSource,則是執行sql時,才動態生成,
public class DynamicSqlSource implements SqlSource {
private Configuration configuration;
private SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 逐一呼叫各種SqlNode,拼接sql
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);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
BoundSql不僅保存了最終的可執行的sql,還保存了sql中?號占位符的引數串列,
public class BoundSql {
private String sql;
private List<ParameterMapping> parameterMappings;
// ...
}
最后,在執行sql時,通過org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(PreparedStatement)方法,遍歷List parameterMappings = boundSql.getParameterMappings()來逐一對sql中的?號占位符進行賦值操作,
整個sql處理變數占位符的流程就完成了,
6. OGNL運算式運算完成動態sql拼接
我們就舉一個略微復雜一點的ForEachSqlNode的拼接sql原理,
public class ForEachSqlNode implements SqlNode {
// OGNL運算式計算器
private ExpressionEvaluator evaluator;
//...
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
// 計算集合運算式
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
applyOpen(context);
int i = 0;
// 遍歷拼接sql
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first) {
context = new PrefixedContext(context, "");
} else if (separator != null) {
context = new PrefixedContext(context, separator);
} else {
context = new PrefixedContext(context, "");
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
return true;
}
//...
}
最后
Mybatis的全部動態sql內容,至此就全部介紹完了,在實際作業中,絕大多數的sql,都是動態sql,有識訓的朋友可以點個贊哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/202560.html
標籤:其他
