Mybatis?MappedStatement類核心原理詳解
MappedStatement
MappedStatement 類是 Mybatis 框架的核心類之一,它存儲了一個 sql 對應(yīng)的所有信息
Mybatis 通過解析 XML 和 mapper 接口上的注解,生成 sql 對應(yīng)的 MappedStatement 實例,并放入 SqlSessionTemplate 中 configuration 類屬性中
正真執(zhí)行 mapper 接口中的方法時,會從 configuration 中找到對應(yīng)的 mappedStatement,然后進行后續(xù)的操作
MyBatis通過MappedStatement描述<select|update|insert|delete>或者@Select、@Update等注解配置的SQL信息。在介紹MappedStatement組件之前,我們先來了解一下MyBatis中SQL Mapper的配置。不同類型的SQL語句需要使用對應(yīng)的XML標(biāo)簽進行配置。這些標(biāo)簽提供了很多屬性,用來控制每條SQL語句的執(zhí)行行為。下面是標(biāo)簽中的所有屬性:
<select
id="getUserById"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="userResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD ONLY">
public final class MappedStatement {
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;對應(yīng)字段含義
id:在命名空間中唯一的標(biāo)識符,可以被用來引用這條配置信息。
parameterType:用于指定這條語句的參數(shù)類的完全限定名或別名。這個屬性是可選的,MyBatis能夠根據(jù)Mapper接口方法中的參數(shù)類型推斷出傳入語句的類型。
parameterMap:引用通過標(biāo)簽定義的參數(shù)映射,該屬性已經(jīng)廢棄。
resultType:從這條語句中返回的期望類型的類的完全限定名或別名。注意,如果返回結(jié)果是集合類型,則resultType屬性應(yīng)該指定集合中可以包含的類型,而不是集合本身。
resultMap:用于引用通過標(biāo)簽配置的實體屬性與數(shù)據(jù)庫字段之間建立的結(jié)果集的映射(注意:resultMap和resultType屬性不能同時使用)。
flushCache:用于控制是否刷新緩存。如果將其設(shè)置為true,則任何時候只要語句被調(diào)用,都會導(dǎo)致本地緩存和二級緩存被清空,默認(rèn)值為false。
useCache:是否使用二級緩存。如果將其設(shè)置為true,則會導(dǎo)致本條語句的結(jié)果被緩存在MyBatis的二級緩存中,對應(yīng)標(biāo)簽,該屬性的默認(rèn)值為true。
timeout:驅(qū)動程序等待數(shù)據(jù)庫返回請求結(jié)果的秒數(shù),超時將會拋出異常。
fetchSize:用于設(shè)置JDBC中Statement對象的fetchSize屬性,該屬性用于指定SQL執(zhí)行后返回的最大行數(shù)。
statementType:參數(shù)可選值為STATEMENT、PREPARED或CALLABLE,這會讓MyBatis分別使用Statement、PreparedStatement或CallableStatement與數(shù)據(jù)庫交互,默認(rèn)值為PREPARED。
resultSetType:參數(shù)可選值為FORWARD_ONLY、SCROLL_SENSITIVE或SCROLL_INSENSITIVE,用于設(shè)置ResultSet對象的特征,具體可參考第2章JDBC規(guī)范的相關(guān)內(nèi)容。默認(rèn)未設(shè)置,由JDBC驅(qū)動決定。
databaseId:如果配置了databaseIdProvider,MyBatis會加載所有不帶databaseId或匹配當(dāng)前databaseId的語句。
resultOrdered:這個設(shè)置僅針對嵌套結(jié)果select語句適用,如果為true,就是假定嵌套結(jié)果包含在一起或分組在一起,這樣的話,當(dāng)返回一個主結(jié)果行的時候,就不會發(fā)生對前面結(jié)果集引用的情況。這就使得在獲取嵌套結(jié)果集的時候不至于導(dǎo)致內(nèi)存不夠用,默認(rèn)值為false。
resultSets:這個設(shè)置僅對多結(jié)果集的情況適用,它將列出語句執(zhí)行后返回的結(jié)果集并每個結(jié)果集給一個名稱,名稱使用逗號分隔。
lang:該屬性用于指定LanguageDriver實現(xiàn),MyBatis中的LanguageDriver用于解析<select|update|insert|delete>標(biāo)簽中的SQL語句,生成SqlSource對象。
介紹jdbc的幾個相關(guān)屬性
resultSetType
在創(chuàng)建PreparedStatement時,resultSetType參數(shù)設(shè)置的是TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE,
這兩個參數(shù)的共同特點是允許結(jié)果集(ResultSet)的游標(biāo)可以上下移動。而默認(rèn)的TYPE_FORWARD_ONLY參數(shù)只允許結(jié)果集的游標(biāo)向下移動。
如果PreparedStatement對象初始化時resultSetType參數(shù)設(shè)置為TYPE_FORWARD_ONLY,在從ResultSet(結(jié)果集)中讀取記錄的時,對于訪問過的記錄就自動釋放了內(nèi)存。而設(shè)置為TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE時為了保證能游標(biāo)能向上移動到任意位置,已經(jīng)訪問過的所有都保留在內(nèi)存中不能釋放。所以大量數(shù)據(jù)加載的時候,就OOM了。
statement = conn.prepareStatement(querySql,ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);resultSetType是設(shè)置ResultSet對象的類型標(biāo)示可滾動,或者是不可滾動。取值如下:

默認(rèn)只能向前滾動。
fetchSize
默認(rèn)情況下pgjdbc driver會一次性拉取所有結(jié)果集,也就是在executeQuery的時候。對于大數(shù)據(jù)量的查詢來說,非常容易造成OOM。這種場景就需要設(shè)置fetchSize,執(zhí)行query的時候先返回第一批數(shù)據(jù),之后next完一批數(shù)據(jù)之后再去拉取下一批。
場景與方案
場景:java端從數(shù)據(jù)庫讀取100W數(shù)據(jù)進行后臺業(yè)務(wù)處理。
常規(guī)實現(xiàn)1:分頁讀取出來。缺點:需要排序后分頁讀取,性能低下。
常規(guī)實現(xiàn)2:一次性讀取出來。缺點:需要很大內(nèi)存,一般計算機不行。
非常規(guī)實現(xiàn):建立長連接,利用服務(wù)端游標(biāo),一條一條流式返回給java端。
非常規(guī)實現(xiàn)優(yōu)化:jdbc中有個重要的參數(shù)fetchSize(它對業(yè)務(wù)實現(xiàn)無影響,即不會限制讀取條數(shù)等),優(yōu)化后可顯著提升性能。
public static void getAll(int fetchSize) {
try {
long beginTime=System.currentTimeMillis();
Connection connection = DriverManager.getConnection(MYSQL_URL);
connection.setAutoCommit(false); //為了設(shè)置fetchSize,必須設(shè)置為false
String sql = "select * from test";
PreparedStatement psst = connection.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
psst.setFetchSize(fetchSize);
ResultSet rs = psst.executeQuery();
int totalCount=0;
while (rs.next()) {
totalCount++;
}
rs.close();
psst.close();
connection.close();
long endTime=System.currentTimeMillis();
System.out.println("totalCount:"+totalCount+";fetchSize:"+fetchSize+";耗時:"+(endTime-beginTime)+"ms");
} catch (SQLException e) {
e.printStackTrace();
}
}原理分析
1、先在服務(wù)端執(zhí)行查詢后將數(shù)據(jù)緩存在服務(wù)端。(耗時相對較長)
2、java端獲取數(shù)據(jù)時,利用服務(wù)端游標(biāo)進行指針跳動,如果fetchSize為1000,則一次性跳動1000條,返回給java端緩存起來。(耗時較短,跳動次數(shù)為N/1000)
3、在調(diào)用next函數(shù)時,優(yōu)先從緩存中取數(shù),其次執(zhí)行2過程。(內(nèi)存讀取,耗時可忽略)
MappedStatement是怎么來的
還是以XML配置方式為例進行分析,簡單說下源碼查找的過程。Mapper對應(yīng)的SQL語句定義在xml文件中,順著源碼會發(fā)現(xiàn)完成xml解析工作的是XMLMapperBuilder,其中對xml中“select|insert|update|delete”類型元素的解析方法為buildStatementFromContext;buildStatementFromContext使用了XMLStatementBuilder類對statement進行解析,并最終創(chuàng)建了MappedStatement。
所以,XMLStatementBuilder#parseStatementNode方法就是我們分析的重點。但是,在此之前需要有一點準(zhǔn)備工作要做。由于MappedStatement最終是由MapperBuilderAssistant構(gòu)建的,它其中存儲了一些Mapper級別的共享信息并應(yīng)用到MappedStatement中。所以,先來簡單了解下它的由來:
public class XMLMapperBuilder extends BaseBuilder {
private final XPathParser parser;
private final MapperBuilderAssistant builderAssistant;
//省略部分字段和方法
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
//省略部分字段和方法
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//省略部分代碼
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
} 好了,下面正是開始XMLStatementBuilder#parseStatementNode的分析了。為了節(jié)省篇幅,我直接通過代碼注釋的方式進行說明了,部分我認(rèn)為不關(guān)鍵或不常用的內(nèi)容沒有多說。
/**
* parseStatementNode方法是對select、insert、update、delete這四類元素進行解析,大體分為三個過程:
* 1、解析節(jié)點屬性:如我們最常用的id、resultMap等;
* 2、解析節(jié)點內(nèi)的sql語句:首先把sql語句中包含的<include></include>等標(biāo)簽轉(zhuǎn)為實際的sql語句,然后執(zhí)行靜態(tài)或動態(tài)節(jié)點處理;
* 3、根據(jù)以上解析到的內(nèi)容,使用builderAssistant創(chuàng)建MappedStatement,并加入Configuration中。
* <p>
* 以上過程中最關(guān)鍵的是第二步,它會根據(jù)實際使用的標(biāo)簽,把sql片段轉(zhuǎn)為不同的SqlNode,以鏈表方式存儲到SqlSource中。
*/
public void parseStatementNode() {
//獲取標(biāo)簽的id屬性,如selectById,對應(yīng)Mapper接口中的方法名稱
String id = context.getStringAttribute("id");
//獲取databaseId屬性,我們一般都沒有寫。
String databaseId = context.getStringAttribute("databaseId");
/**
* 這段代碼雖然不起眼,但是一定要進去看一下:其內(nèi)部完成了對id的再次賦值,
* 處理的方式是:id=namespace+"."+id,也就是當(dāng)前Mapper的完全限定名+"."+id,
* 比如我們之前例子中的com.raysonxin.dao.CompanyDao.selectById
* 這也是Mapper接口中不能存在重載方法的根本原因。
* */
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
/**
* 下面這塊代碼會依次獲取fetchSize、timeout、resultMap等屬性,
* 需要注意的是,有些屬性雖然我們沒有設(shè)置,但是mybatis會設(shè)置默認(rèn)值,
* 具體可以查看mybatis的官方說明。
*/
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
//默認(rèn)值:XMLLanguageDriver
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
//默認(rèn)值:PREPARED
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
//默認(rèn)值:DEFAULT
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
/**
* 英文注釋也說了,在sql解析前處理 include 標(biāo)簽,比如說,我們include了BaseColumns,
* 它會把這個include標(biāo)簽替換為BaseColumns內(nèi)的sql內(nèi)容
* */
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//處理selectKey,主要針對不同的數(shù)據(jù)庫引擎做處理
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
/**
* 到了關(guān)鍵步驟了:就是通過這句代碼完成了從xml標(biāo)簽到SqlSource的轉(zhuǎn)換,
* SqlSource是一個接口,這里返回的可能是DynamicSqlSource、也可能是RawSqlSource,
* 取決于xml標(biāo)簽中是否包含動態(tài)元素,比如 <if test=""></if>
* */
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
//下面這些是針對selectKey、KeyGenerator等進行處理,暫時跳過了。
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
/**
* 節(jié)點及屬性都解析完成了,使用builderAssistant創(chuàng)建MappedStatement,
* 并保存到Configuration#mappedStatements。
* */
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}我們在xml中定義的select等語句就是通過這個parseStatementNode方法解析為MappedStatement的,整體來看比較容易理解,核心就是SqlSource的創(chuàng)建過程,
SqlSource是什么
SqlSource是整個MappedStatement的核心,MappedStatement其他一大堆字段都是為了準(zhǔn)確的執(zhí)行它而定義的。SqlSource是個半成品的sql語句,因為對于其中的動態(tài)標(biāo)簽還沒靜態(tài)化,其中的參數(shù)也未賦值。正是如此,才為我們后續(xù)的調(diào)用執(zhí)行提供了基礎(chǔ),接下來重點看看SqlSource的構(gòu)建過程。為了先從整體上了解,我畫了一個時序圖來描述SqlSource的解析、創(chuàng)建過程。
sqlsource是mapped statement對象中的一個屬性, 是一對一關(guān)系, 在創(chuàng)建mapped statement對象時創(chuàng)建,
sqlsource的主要作用就是創(chuàng)建一個sql語句

SqlSource主要有四種實現(xiàn)類, 其主要作用的就是以下三種 :
RawSqlSource : 存儲的是只有“#{}”或者沒有標(biāo)簽的純文本sql信息
DynamicSqlSource : 存儲的是寫有“${}”或者具有動態(tài)sql標(biāo)簽的sql信息
StaticSqlSource : 是DynamicSqlSource和RawSqlSource解析為BoundSql的一個中間環(huán)節(jié)
BoundSql
private final String sql; private final List<ParameterMapping> parameterMappings; private final Object parameterObject;
oundSql類的作用就是生成我們最終執(zhí)行的sql語句, 里面包含一些屬性
- sql : 執(zhí)行的sql語句 (包含 ?)
- parameterMappings : 映射關(guān)系
- parameterObject : 參數(shù)值
DynamicSqlSource
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final 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);
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;
}
}- field
configuration : 上下文, 里面包含了各種參數(shù), 方便使用
- method
getBoundSql :
構(gòu)建DynamicContext對象, 里邊包含一個StringJoiner屬性, 遍歷sqlNode節(jié)點是用來拼接sql
將拼接的sql傳入, 將#{}替換成? , 并創(chuàng)建一個StaticSqlSource對象
通過StaticSqlSource對象獲取BoundSql對象
RawSqlSource
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;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}- field
sqlSource : 此屬性的類型為StaticSqlSource
- method
getBoundSql : 獲取BoundSql對象
StaticSqlSource
只包含一個創(chuàng)建BoundSql對象的方法
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
到此這篇關(guān)于Mybatis MappedStatement類核心原理詳解的文章就介紹到這了,更多相關(guān)Mybatis MappedStatement內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合canal實現(xiàn)數(shù)據(jù)緩存一致性解決方案
canal主要用途是基于?MySQL?數(shù)據(jù)庫增量日志解析,提供增量數(shù)據(jù)訂閱和消費,canal是借助于MySQL主從復(fù)制原理實現(xiàn),本文將給大家介紹SpringBoot整合canal實現(xiàn)數(shù)據(jù)緩存一致性解決方案,需要的朋友可以參考下2024-03-03
SpringBoot中MockMVC單元測試的實現(xiàn)
Mock是一種用于模擬和替換類的對象的方法,以便在單元測試中獨立于外部資源進行測試,本文主要介紹了SpringBoot中MockMVC單元測試的實現(xiàn),具有應(yīng)該的參考價值,感興趣的可以了解一下2024-02-02
Java 隊列實現(xiàn)原理及簡單實現(xiàn)代碼
這篇文章主要介紹了Java 隊列實現(xiàn)原理及簡單實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10
FluentMybatis實現(xiàn)mybatis動態(tài)sql拼裝和fluent api語法
本文主要介紹了FluentMybatis實現(xiàn)mybatis動態(tài)sql拼裝和fluent api語法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08
java加載properties文件的六種方法總結(jié)
這篇文章主要介紹了java加載properties文件的六種方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-05-05
SpringBoot 使用 OpenAPI3 規(guī)范整合 knife4j的詳細過程
Swagger工具集使用OpenAPI規(guī)范,可以生成、展示和測試基于OpenAPI規(guī)范的API文檔,并提供了生成客戶端代碼的功能,本文給大家介紹SpringBoot使用OpenAPI3規(guī)范整合knife4j的詳細過程,感興趣的朋友跟隨小編一起看看吧2023-12-12

