mybatis源碼分析(三)------------映射文件的解析

本篇文章主要講解映射文件的解析過程java

Mapper映射文件有哪幾種配置方式呢?看下面的代碼:node

<!-- 映射文件 -->
    <mappers>
        <!-- 經過resource指定Mapper文件 -->  方式一
        <mapper resource="com/yht/mybatisTest/dao/goods.xml" />
        
        <!-- 經過class指定接口,但須要將接口與Mapper文件同名,從而將二者創建起關係,此處接口是GoodsDao,那麼Mapper映射文件就須要是GoodsDao.xml --> 方式二
        <mapper class="com.yht.mybatisTest.dao.GoodsDao" />
        
        <!-- 掃描指定包中的接口,須要將接口名與Mapper文件同名 --> 方式三
        <package name="com.yht.mybatisTest.dao"/>
        
       <!-- 經過url指定Mapper文件位置 --> 方式四
        <mapper url="file://........" />
    </mappers>

源碼部分以下:sql

private void mapperElement(XNode parent) throws Exception { if (parent != null) {
// 循環處理mappers節點下全部的子節點
for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) {
// 獲的package節點的name屬性值 String mapperPackage
= child.getStringAttribute("name");
// 針對 方式三 的解析方法 configuration.addMappers(mapperPackage); }
else {
// 獲取mapper節點的resource屬性值 String resource
= child.getStringAttribute("resource"); // 獲取mapper節點的url屬性值
String url
= child.getStringAttribute("url");
// 獲取mapper節點的class屬性值 String mapperClass
= child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 針對 方式一 的解析方法 mapperParser.parse(); }
else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); // 針對 方式四 的解析方法
mapperParser.parse(); }
else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass);
// 針對 方式二 的解析方法 configuration.addMapper(mapperInterface); }
else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }

由上面代碼可知:針對四種不一樣的配置分別進行了解析,這裏咱們主要分析 方式一 的解析方法,進入該方法:數據庫

public void parse() {
// 檢測Mapper映射文件是否被解析過
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper節點 configurationElement(parser.evalNode(
"/mapper"));
// 將資源文件添加到 已解析資源集合 中 configuration.addLoadedResource(resource);
// 註冊Mapper接口 bindMapperForNamespace(); } // 處理 configurationElement方法中解析失敗的<ResultMap />節點 parsePendingResultMaps();
// 處理 configurationElement方法中解析失敗的<cache-ref />節點 parsePendingChacheRefs();
// 處理 configurationElement方法中解析失敗的SQL語句節點 parsePendingStatements(); }

 一 解析Mapper節點緩存

進入XMLMapperBuilder類的configurationElement方法中:mybatis

private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); }
// 設置當前的namespace builderAssistant.setCurrentNamespace(namespace);
// 解析<cache-ref/>節點 cacheRefElement(context.evalNode(
"cache-ref"));
// 解析<cache/>節點 cacheElement(context.evalNode(
"cache"));
// 解析parameterMap節點 parameterMapElement(context.evalNodes(
"/mapper/parameterMap"));
// 解析resultMap節點 resultMapElements(context.evalNodes(
"/mapper/resultMap"));
// <sql/>節點 sqlElement(context.evalNodes(
"/mapper/sql"));
// 解析<insert>等節點 buildStatementFromContext(context.evalNodes(
"select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }

XMLMapperBuilder類主要是用於解析映射配置文件,它繼承了BaseBuilder抽象類,全部對映射配置文件的解析方法都在這個類中,接下來就分別對這些節點的解析過程進行分析。app

1.<cache-ref />的解析源碼分析

1.1 使用方法:字體

<!-- 表示使用如下namespace中的cache對象,也就是說和下面namespace共用一個cache對象 -->
<cache-ref namespace="com.yht.mybatisTest.dao.GoodsDao"/>

1.2 源碼分析:fetch

private void cacheRefElement(XNode context) { if (context != null) {
// 這個方法是把當前節點的namespace做爲key,<cache-ref>節點指定的namespace屬性值做爲value,存放到HashMap中;前者共用後者的cache對象 configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute(
"namespace"));
//cacheRefResolver是一個cache引用解析器,封裝了當前XMLMapperBuilder對應的MapperBuilderAssistant對象,和被引用的namespace CacheRefResolver cacheRefResolver
= new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); try {
// 這個方法主要是指定當前mapper文件使用的cache對象,也就是設置MapperBuilderAssistant的currentCache和unresolvedCacheRef字段 cacheRefResolver.resolveCacheRef(); }
catch (IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); } } }

進入 cacheRefResolver.resolveCacheRef();方法:

public Cache resolveCacheRef() {
// 進入此方法
return assistant.useCacheRef(cacheRefNamespace); }

進入MapperBuilderAssistant類的userCacheRef方法,這個類是XMLMapperBuilder的一個輔助類,用於保存當前mapper文件的namespace,以及使用的cache對象

public Cache useCacheRef(String namespace) { if (namespace == null) { throw new BuilderException("cache-ref element requires a namespace attribute."); } try { unresolvedCacheRef = true;
// 根據被引用的namespace,獲取對應的cache對象 Cache cache
= configuration.getCache(namespace); if (cache == null) { throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); }
// 使當前mapper文件的cache對象currentCache指向cache,也就是共用一個cache對象 currentCache
= cache; unresolvedCacheRef = false; return cache; } catch (IllegalArgumentException e) { throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); } }

1.3 總結:對於<cahce-ref>節點的解析,就是找到被引用namespace對應的cache對象,而後是當前namespace中的currentCache執向那個cache對象,也就是二者共用一個cache對象。在這個過程當中,MapperBuilderAssistant這個輔助類保存了當前mapper文件中的namespace的值,cache對象以及其餘屬性。

2.<cache />的解析

2.1 使用方法:

<cache eviction="FIFO"  flushInterval="60000" size="512" readOnly="true"/>

cache節點有這幾個標籤:

(a)  eviction:緩存的回收策略

(b) flushInterval:刷新間隔

(c) size:要緩存的元素數目

(d) readOnly:若是爲true表示只讀,不能修改

(e) type:指定自定義的緩存的全類名

2.2 源碼分析:

 進入XMLMapperBuilder類的cacheElement方法:

private void cacheElement(XNode context) throws Exception { if (context != null) {
// 獲取<cache>節點的type屬性,默認值是PERPETUAL String type
= context.getStringAttribute("type", "PERPETUAL");
// 獲取type屬性對應的Cache接口實現 Class
<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 獲取<cache>節點的eviction屬性 String eviction
= context.getStringAttribute("eviction", "LRU"); // 根據eviction屬性獲取對應的類
Class
<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 獲取<cache>節點的flushInterval屬性 Long flushInterval
= context.getLongAttribute("flushInterval");
// 獲取size熟悉和readOnly屬性 Integer size
= context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); Properties props = context.getChildrenAsProperties();
// 以上從<cache>節點配置中獲取的屬性和對應的class,都是爲生成cache對象作準備的,此處cache對象的生成使用了構造者模式 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props); } }

經過上面的代碼可知:Cache對象是由MapperBuilderAssistant類生成的,進入useNewCache方法:

public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, Properties props) { typeClass = valueOrDefault(typeClass, PerpetualCache.class); evictionClass = valueOrDefault(evictionClass, LruCache.class);
// 這裏使用到了構造者模式,CacheBuilder是建造者的角色,Cache是生成的產品,產品類的角色 Cache cache
= new CacheBuilder(currentNamespace) .implementation(typeClass) .addDecorator(evictionClass) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .properties(props) .build();
// 將cache對象放到configuration對象的StrctMap中,cache的id做爲key,cache對象做爲value。此處的cache對象使用了裝飾器模式,最底層的對象是PerpetualCache configuration.addCache(cache);
// 記錄當前命名空間使用的cache對象 currentCache
= cache; return cache; }

CacheBuilder是Cache的建造者,接下來分析CacheBuilder這個類:

// 這個類是建造者角色,根據<cache>節點中配置的各類屬性來生成不一樣的Cache對象,<cache>節點中配置的屬性都被賦予了這個類中的下面這些屬性,而後又爲這些屬性提供了不一樣的賦值方法,能夠靈活的生成任意組合的Cache對象;這是典型的建造者模式
public
class CacheBuilder { private String id; // Cache對象的惟一表示,通常狀況下對應Mapper映射文件的namespace private Class<? extends Cache> implementation; // Cache接口的真正實現類,默認是PerpetualCache private List<Class<? extends Cache>> decorators; // 裝飾器集合,默認只包含LRUCache.class private Integer size; // Cache的大小 private Long clearInterval; //清理時間週期 private boolean readWrite; // 是否可讀寫 private Properties properties;// 其它配置信息
public CacheBuilder(String id) { this.id = id; this.decorators = new ArrayList<Class<? extends Cache>>(); } // 這幾個方法就是爲生成的Cache對象使用到的方法 public CacheBuilder implementation(Class<? extends Cache> implementation) { this.implementation = implementation; return this; } public CacheBuilder addDecorator(Class<? extends Cache> decorator) { if (decorator != null) { this.decorators.add(decorator); } return this; } public CacheBuilder size(Integer size) { this.size = size; return this; } public CacheBuilder clearInterval(Long clearInterval) { this.clearInterval = clearInterval; return this; } // 生成Cache對象,cache對象是產品角色 public Cache build() {
//implement爲null,decorators爲空,則給予默認值 setDefaultImplementations();
// 根據implement指定的類型,建立Cache對象 Cache cache
= newBaseCacheInstance(implementation, id);
// 根據<cache>節點下配置的<properties>信息,初始化Cache對象 setCacheProperties(cache);
// 若是cache對象的類型是PerpetualCahce類型,那麼爲其添加decorators集合中的裝飾器,cache對象自己使用了裝飾器模式
if (PerpetualCache.class.equals(cache.getClass())) { // issue #352, do not apply decorators to custom caches for (Class<? extends Cache> decorator : decorators) {
// 爲Cache對象添加裝飾器 cache
= newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache);// 爲cache對象配置屬性 }
// 添加mybatis中提供的標準裝飾器 cache
= setStandardDecorators(cache); } return cache; } }

 3.<resultMap/>解析

3.1 使用方法:

<resultMap id="goodsMap" type="goods">
        <id column="id" property="id"/>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
        <result column="name" property="name"/>
    </resultMap>

3.2 源碼解析

<resultMap>節點定義了數據庫的結果集和javaBean對象之間的映射關係,在解析<resultMap>節點以前,先看兩個類ResultMapping和ResultMap。

每一個ResultMapping對象記錄告終果集中的一列與javaBean中一個屬性之間的映射關係,看它的屬性字段:

private Configuration configuration; //Configuration對象 private String property; // 對應節點的property屬性,表示的是javaBean中對應的屬性 private String column; // 對應節點的column屬性,表示的是從數據庫中獲得的列名或者列名的別名 private Class<?> javaType; //對應節點的javaType屬性,表示的是一個javaBean的徹底限定名,或者一個類型別名 private JdbcType jdbcType;// 對應節點的jdbcType屬性,表示的是進行映射的列的JDBC類型 private TypeHandler<?> typeHandler;// 對應節點的typeHandler屬性,表示的是類型處理器 private String nestedResultMapId; // 對應節點的resultMap屬性 嵌套的結果映射時有用到 private String nestedQueryId; //對應節點的select屬性 嵌套查詢時有用到 private Set<String> notNullColumns; private String columnPrefix; private List<ResultFlag> flags; private List<ResultMapping> composites; private String resultSet; //對應節點的resultSet屬性 private String foreignColumn;// 對應節點的foreignColumn屬性 private boolean lazy; //是否延遲加載,對應節點的fetchType屬性

對於ResultMap類,每一個<resultMap>節點都會被解析成一個ResutltMap對象,看它的屬性:

private String id;  //<resultMap>節點id的屬性 private Class<?> type; // <resultMap>節點type的屬性 private List<ResultMapping> resultMappings; //ResutlMapping的集合 private List<ResultMapping> idResultMappings; //記錄了映射關係中帶有ID標誌的映射關係 例如<id>節點和<constructor>節點的<idArg>子節點 private List<ResultMapping> constructorResultMappings; //記錄映射關係中帶有Constructor標誌的映射關係,例如<constructor>全部子元素 private List<ResultMapping> propertyResultMappings; // 記錄映射關係中不帶有Constructor標誌的映射關係 private Set<String> mappedColumns; // 記錄全部映射關係中涉及的column熟悉的集合 private Discriminator discriminator;// 鑑別器 對應<discriminator>節點 private boolean hasNestedResultMaps; // 是否含有嵌套的結果映射,若是有,則爲true private boolean hasNestedQueries; // 是否含有嵌套查詢,若是有,則爲true private Boolean autoMapping; //是否開啓自動映射

如今咱們進入<resultMap>節點的源碼解析部分,進入XMLMapperBuilder的resultMapElement方法:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 獲取<resutlMap>節點的id屬性 String id
= resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
// 獲取<resultMap>節點的type屬性 String type
= resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
// 獲取<resultMap>節點的extends屬性,該屬性指定了<resultMap>節點的繼承關係 String extend
= resultMapNode.getStringAttribute("extends");
// 讀取resultMap節點的autoMapping屬性 Boolean autoMapping
= resultMapNode.getBooleanAttribute("autoMapping");
// 解析type類型 Class
<?> typeClass = resolveClass(type); Discriminator discriminator = null; List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(additionalResultMappings); List<XNode> resultChildren = resultMapNode.getChildren();
// 處理<resultMap>的全部子節點
for (XNode resultChild : resultChildren) {
// 處理<constructor>節點
if ("constructor".equals(resultChild.getName())) { processConstructorElement(resultChild, typeClass, resultMappings); } else if ("discriminator".equals(resultChild.getName())) {
// 處理<discriminator>節點 discriminator
= processDiscriminatorElement(resultChild, typeClass, resultMappings); } else {
// 處理<id>,<result>,<association>,<collection>等節點 ArrayList
<ResultFlag> flags = new ArrayList<ResultFlag>(); if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); }
// 建立ResultMapping對象,並添加到集合中 resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } ResultMapResolver resultMapResolver
= new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try {
// 建立ResultMap對象,並添加到Configuration.resultMaps集合中
return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }

接下來,咱們分析上面紅色字體的方法首先是buildResultMappingFromContext方法,根據字面意思也能夠知道,該方法是從上下文環境中獲取到的屬性信息建立ResultMapping對象,進入該方法:

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, ArrayList<ResultFlag> flags) throws Exception {
// 獲取每個映射關係中 property,column,....的屬性值 String property
= context.getStringAttribute("property"); String column = context.getStringAttribute("column"); String javaType = context.getStringAttribute("javaType"); String jdbcType = context.getStringAttribute("jdbcType"); String nestedSelect = context.getStringAttribute("select"); String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping> emptyList())); String notNullColumn = context.getStringAttribute("notNullColumn"); String columnPrefix = context.getStringAttribute("columnPrefix"); String typeHandler = context.getStringAttribute("typeHandler"); String resulSet = context.getStringAttribute("resultSet"); String foreignColumn = context.getStringAttribute("foreignColumn"); boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
// 解析javaType,jdbcType和TypeHandler Class
<?> javaTypeClass = resolveClass(javaType); @SuppressWarnings("unchecked") Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler); JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 建立ResultMapping對象
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resulSet, foreignColumn, lazy); }

4.<sql/>的解析

4.1 使用用法

<sql id="sql_where_key"> id = #{id} </sql>

4.2 源碼解析

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
// 遍歷<sql>節點
for (XNode context : list) {
// 獲取databaseId屬性 String databaseId
= context.getStringAttribute("databaseId");
// 獲取id屬性 String id
= context.getStringAttribute("id");
// 爲id添加命名空間 id
= builderAssistant.applyCurrentNamespace(id, false);
// 以id爲key,context爲value存放到Map中
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) sqlFragments.put(id, context); } }

5.<select>,<insert>等sql節點的解析

5.1 使用方法:

<select id="selectGoodsById" resultMap="goodsMap"> select * from goods <where>
              <include refid="sql_where_key" />
            </where>
    </select>

5.2 源碼解析

在源碼解析前,先了解SQLSource接口和MappedStatement類

SqlSource接口表示映射文件或者註解中描述的sql語句,可是它並非數據庫可執行的sql語句,由於它還可能包含有動態sql語句相關的節點或者佔位符等須要解析的元素。

public interface SqlSource {
// 根據映射文件或者註解描述的sql語句,以及傳入的參數,返回可執行的sql BoundSql getBoundSql(Object parameterObject); }

MappedStatement表示映射文件中定義的sql節點,它的部分屬性以下:

private String resource;  //節點中id的屬性 private Configuration configuration; private String id; private Integer fetchSize; private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; private SqlSource sqlSource; //sqlSource對象,對應一條sql語句 private Cache cache;
private SqlCommandType sqlCommandType; // SQL的類型,INSERT,SELECT 等

SQL節點的解析是XMLStatementBuilder類來解析的,進入解析方法的入口:

public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return; 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"); LanguageDriver langDriver = getLanguageDriver(lang); Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); 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);

// 以上代碼是獲取sql節點中的各類屬性值,如 useCache,resultMap,resultType,paramterMap,timeout等
// Include Fragments before parsing 解析SQL語句前,先處理<sql>節點中的<include/>節點 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 處理selectKey節點 // Parse selectKey after includes and remove them. processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析SQL語句
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed) SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); String resultSets = context.getStringAttribute("resultSets"); 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)) ? new Jdbc3KeyGenerator() : new NoKeyGenerator(); } // 將SQL節點解析爲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); }

到這裏,映射配置文件的整個解析過程就結束了,在下一篇文章中,咱們介紹SQL的執行過程。

相關文章
相關標籤/搜索