Mybatis?MappedStatement類核心原理詳解
MappedStatement
MappedStatement 類是 Mybatis 框架的核心類之一,它存儲了一個 sql 對應的所有信息
Mybatis 通過解析 XML 和 mapper 接口上的注解,生成 sql 對應的 MappedStatement 實例,并放入 SqlSessionTemplate 中 configuration 類屬性中
正真執(zhí)行 mapper 接口中的方法時,會從 configuration 中找到對應的 mappedStatement,然后進行后續(xù)的操作
MyBatis通過MappedStatement描述<select|update|insert|delete>或者@Select、@Update等注解配置的SQL信息。在介紹MappedStatement組件之前,我們先來了解一下MyBatis中SQL Mapper的配置。不同類型的SQL語句需要使用對應的XML標簽進行配置。這些標簽提供了很多屬性,用來控制每條SQL語句的執(zhí)行行為。下面是標簽中的所有屬性:
<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;
對應字段含義
id:在命名空間中唯一的標識符,可以被用來引用這條配置信息。
parameterType:用于指定這條語句的參數類的完全限定名或別名。這個屬性是可選的,MyBatis能夠根據Mapper接口方法中的參數類型推斷出傳入語句的類型。
parameterMap:引用通過標簽定義的參數映射,該屬性已經廢棄。
resultType:從這條語句中返回的期望類型的類的完全限定名或別名。注意,如果返回結果是集合類型,則resultType屬性應該指定集合中可以包含的類型,而不是集合本身。
resultMap:用于引用通過標簽配置的實體屬性與數據庫字段之間建立的結果集的映射(注意:resultMap和resultType屬性不能同時使用)。
flushCache:用于控制是否刷新緩存。如果將其設置為true,則任何時候只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值為false。
useCache:是否使用二級緩存。如果將其設置為true,則會導致本條語句的結果被緩存在MyBatis的二級緩存中,對應標簽,該屬性的默認值為true。
timeout:驅動程序等待數據庫返回請求結果的秒數,超時將會拋出異常。
fetchSize:用于設置JDBC中Statement對象的fetchSize屬性,該屬性用于指定SQL執(zhí)行后返回的最大行數。
statementType:參數可選值為STATEMENT、PREPARED或CALLABLE,這會讓MyBatis分別使用Statement、PreparedStatement或CallableStatement與數據庫交互,默認值為PREPARED。
resultSetType:參數可選值為FORWARD_ONLY、SCROLL_SENSITIVE或SCROLL_INSENSITIVE,用于設置ResultSet對象的特征,具體可參考第2章JDBC規(guī)范的相關內容。默認未設置,由JDBC驅動決定。
databaseId:如果配置了databaseIdProvider,MyBatis會加載所有不帶databaseId或匹配當前databaseId的語句。
resultOrdered:這個設置僅針對嵌套結果select語句適用,如果為true,就是假定嵌套結果包含在一起或分組在一起,這樣的話,當返回一個主結果行的時候,就不會發(fā)生對前面結果集引用的情況。這就使得在獲取嵌套結果集的時候不至于導致內存不夠用,默認值為false。
resultSets:這個設置僅對多結果集的情況適用,它將列出語句執(zhí)行后返回的結果集并每個結果集給一個名稱,名稱使用逗號分隔。
lang:該屬性用于指定LanguageDriver實現,MyBatis中的LanguageDriver用于解析<select|update|insert|delete>標簽中的SQL語句,生成SqlSource對象。
介紹jdbc的幾個相關屬性
resultSetType
在創(chuàng)建PreparedStatement時,resultSetType參數設置的是TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE,
這兩個參數的共同特點是允許結果集(ResultSet)的游標可以上下移動。而默認的TYPE_FORWARD_ONLY參數只允許結果集的游標向下移動。
如果PreparedStatement對象初始化時resultSetType參數設置為TYPE_FORWARD_ONLY,在從ResultSet(結果集)中讀取記錄的時,對于訪問過的記錄就自動釋放了內存。而設置為TYPE_SCROLL_INSENSITIVE或TYPE_SCROLL_SENSITIVE時為了保證能游標能向上移動到任意位置,已經訪問過的所有都保留在內存中不能釋放。所以大量數據加載的時候,就OOM了。
statement = conn.prepareStatement(querySql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
resultSetType是設置ResultSet對象的類型標示可滾動,或者是不可滾動。取值如下:
默認只能向前滾動。
fetchSize
默認情況下pgjdbc driver會一次性拉取所有結果集,也就是在executeQuery的時候。對于大數據量的查詢來說,非常容易造成OOM。這種場景就需要設置fetchSize,執(zhí)行query的時候先返回第一批數據,之后next完一批數據之后再去拉取下一批。
場景與方案
場景:java端從數據庫讀取100W數據進行后臺業(yè)務處理。
常規(guī)實現1:分頁讀取出來。缺點:需要排序后分頁讀取,性能低下。
常規(guī)實現2:一次性讀取出來。缺點:需要很大內存,一般計算機不行。
非常規(guī)實現:建立長連接,利用服務端游標,一條一條流式返回給java端。
非常規(guī)實現優(yōu)化:jdbc中有個重要的參數fetchSize(它對業(yè)務實現無影響,即不會限制讀取條數等),優(yōu)化后可顯著提升性能。
public static void getAll(int fetchSize) { try { long beginTime=System.currentTimeMillis(); Connection connection = DriverManager.getConnection(MYSQL_URL); connection.setAutoCommit(false); //為了設置fetchSize,必須設置為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、先在服務端執(zhí)行查詢后將數據緩存在服務端。(耗時相對較長)
2、java端獲取數據時,利用服務端游標進行指針跳動,如果fetchSize為1000,則一次性跳動1000條,返回給java端緩存起來。(耗時較短,跳動次數為N/1000)
3、在調用next函數時,優(yōu)先從緩存中取數,其次執(zhí)行2過程。(內存讀取,耗時可忽略)
MappedStatement是怎么來的
還是以XML配置方式為例進行分析,簡單說下源碼查找的過程。Mapper對應的SQL語句定義在xml文件中,順著源碼會發(fā)現完成xml解析工作的是XMLMapperBuilder,其中對xml中“select|insert|update|delete”類型元素的解析方法為buildStatementFromContext;buildStatementFromContext使用了XMLStatementBuilder類對statement進行解析,并最終創(chuàng)建了MappedStatement。
所以,XMLStatementBuilder#parseStatementNode方法就是我們分析的重點。但是,在此之前需要有一點準備工作要做。由于MappedStatement最終是由MapperBuilderAssistant構建的,它其中存儲了一些Mapper級別的共享信息并應用到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é)省篇幅,我直接通過代碼注釋的方式進行說明了,部分我認為不關鍵或不常用的內容沒有多說。
/** * parseStatementNode方法是對select、insert、update、delete這四類元素進行解析,大體分為三個過程: * 1、解析節(jié)點屬性:如我們最常用的id、resultMap等; * 2、解析節(jié)點內的sql語句:首先把sql語句中包含的<include></include>等標簽轉為實際的sql語句,然后執(zhí)行靜態(tài)或動態(tài)節(jié)點處理; * 3、根據以上解析到的內容,使用builderAssistant創(chuàng)建MappedStatement,并加入Configuration中。 * <p> * 以上過程中最關鍵的是第二步,它會根據實際使用的標簽,把sql片段轉為不同的SqlNode,以鏈表方式存儲到SqlSource中。 */ public void parseStatementNode() { //獲取標簽的id屬性,如selectById,對應Mapper接口中的方法名稱 String id = context.getStringAttribute("id"); //獲取databaseId屬性,我們一般都沒有寫。 String databaseId = context.getStringAttribute("databaseId"); /** * 這段代碼雖然不起眼,但是一定要進去看一下:其內部完成了對id的再次賦值, * 處理的方式是:id=namespace+"."+id,也就是當前Mapper的完全限定名+"."+id, * 比如我們之前例子中的com.raysonxin.dao.CompanyDao.selectById * 這也是Mapper接口中不能存在重載方法的根本原因。 * */ if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } /** * 下面這塊代碼會依次獲取fetchSize、timeout、resultMap等屬性, * 需要注意的是,有些屬性雖然我們沒有設置,但是mybatis會設置默認值, * 具體可以查看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"); //默認值:XMLLanguageDriver LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); //默認值:PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); //默認值: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 標簽,比如說,我們include了BaseColumns, * 它會把這個include標簽替換為BaseColumns內的sql內容 * */ XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. //處理selectKey,主要針對不同的數據庫引擎做處理 processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) /** * 到了關鍵步驟了:就是通過這句代碼完成了從xml標簽到SqlSource的轉換, * SqlSource是一個接口,這里返回的可能是DynamicSqlSource、也可能是RawSqlSource, * 取決于xml標簽中是否包含動態(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í)行它而定義的。SqlSource是個半成品的sql語句,因為對于其中的動態(tài)標簽還沒靜態(tài)化,其中的參數也未賦值。正是如此,才為我們后續(xù)的調用執(zhí)行提供了基礎,接下來重點看看SqlSource的構建過程。為了先從整體上了解,我畫了一個時序圖來描述SqlSource的解析、創(chuàng)建過程。
sqlsource是mapped statement對象中的一個屬性, 是一對一關系, 在創(chuàng)建mapped statement對象時創(chuàng)建,
sqlsource的主要作用就是創(chuàng)建一個sql語句
SqlSource主要有四種實現類, 其主要作用的就是以下三種 :
RawSqlSource : 存儲的是只有“#{}”或者沒有標簽的純文本sql信息
DynamicSqlSource : 存儲的是寫有“${}”或者具有動態(tài)sql標簽的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 : 映射關系
- parameterObject : 參數值
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 : 上下文, 里面包含了各種參數, 方便使用
- method
getBoundSql :
構建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); }
到此這篇關于Mybatis MappedStatement類核心原理詳解的文章就介紹到這了,更多相關Mybatis MappedStatement內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot整合canal實現數據緩存一致性解決方案
canal主要用途是基于?MySQL?數據庫增量日志解析,提供增量數據訂閱和消費,canal是借助于MySQL主從復制原理實現,本文將給大家介紹SpringBoot整合canal實現數據緩存一致性解決方案,需要的朋友可以參考下2024-03-03FluentMybatis實現mybatis動態(tài)sql拼裝和fluent api語法
本文主要介紹了FluentMybatis實現mybatis動態(tài)sql拼裝和fluent api語法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08SpringBoot 使用 OpenAPI3 規(guī)范整合 knife4j的詳細過程
Swagger工具集使用OpenAPI規(guī)范,可以生成、展示和測試基于OpenAPI規(guī)范的API文檔,并提供了生成客戶端代碼的功能,本文給大家介紹SpringBoot使用OpenAPI3規(guī)范整合knife4j的詳細過程,感興趣的朋友跟隨小編一起看看吧2023-12-12