第一部分:專案結構
user_info表:只有id和username兩個欄位
User實體類:
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
mapper:UserMapper 為根據id查詢使用者資訊
public interface UserMapper {
User getUserByUsername(String username);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<select id="getUserByUsername" resultType="com.example.mybatis.entity.User">
select * from user where username = #{username}
</select>
</mapper>
mybaitis的主配置檔案:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="application.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper\UserMapper.xml"/>
</mappers>
</configuration>
資料庫連線的屬性檔案:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/simple_orm?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
測試類:
public class Test {
public static void main(String[] args) throws IOException {
String resourcePath = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resourcePath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
}
}
第二部分:mybatis重要元件
- Configuration MyBatis所有的配置資訊都儲存在Configuration物件之中,配置檔案中的大部分配置都會儲存到該類中
- SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動時的會話,完成必要資料庫增刪改查功能
- Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護
- StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等
- ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別
- ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合
- TypeHandler 負責java資料型別和jdbc資料型別(也可以說是資料表列型別)之間的對映和轉換
- MappedStatement MappedStatement維護一條<select|update|delete|insert>節點的封裝
- SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回
- BoundSql 表示動態生成的SQL語句以及相應的引數資訊
第三部分:初始化原始碼分析
首先我把測試類貼上過來方便一點。
public class Test {
public static void main(String[] args) throws IOException {
String resourcePath = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resourcePath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
}
}
這裡的測試類是採用原始的方式使用mybatis進行測試,通過對這幾行程式碼背後執行的邏輯進行分析,來看一下mybatis基本的查詢流程。首先前兩行就是獲取resouces目錄下的配置檔案,然後通過流的方式讀取為inputStream,這個流交由SqlSessionFactoryBuilderbuild方法進行處理,具體怎麼處理的可以看下面的分析。現在先看一下建立SqlSessionFactory這個執行的邏輯:使用builder模式建立會話工廠,mybatis的所有初始化工作都是這行程式碼完成,那麼我們進去一探究竟,主要程式碼邏輯如下:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
主要就是先建立一個XMLConfigBuilder物件來解析主配置檔案,就是剛剛載入的那個mybatis的核心配置檔案,其最外層節點是configuration標籤,初始化過程就是將這個標籤以及他的所有子標籤進行解析,把解析好的資料封裝在Configuration這個類中。而Configuration這個類會包含之後執行過程中需要的所有資訊。
第二步:進入parse()方法
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
XMLConfigBuilder維護一個parsed屬性預設為false,這個方法一開始就判斷這個主配置檔案是否已經被解析,如果解析過了就拋異常。
第三步:進入parseConfiguration()方法
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
我們可以看出這個方法是對configuration的所有子標籤逐個解析。包括settings屬性配置、typeAliases配置別名、environments是配置資料庫連結和事務等等。然後把解析後的資料封裝在Configuration這個類中,parse()方法返回Configuration物件,parseConfiguration()方法就是主要對相關標籤進行解析並封裝到Configuration中。在這裡我們主要看mappers標籤的解析過程,整個過程就是對SqlMapConfig配置檔案中引用到的XXXMapper檔案進行解析,儲存其中的sql到Statement中。
第四步:進入mapperElement()方法。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
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.");
}
}
}
}
}
(1)核心配置檔案下面的mappers節點下面可能會有很多mapper節點,所以需要迴圈遍歷每一個mapper節點去解析該節點所對映的xml檔案。
(2)迴圈下面是一個if..else判斷。它先判斷mappers下面的子節點是不是package節點。因為在實際開發中有很多的xml檔案,不可能每一個xml檔案都用一個mapper節點去對映,我們乾脆會用一個package節點去對映一個包下面的所有的xml,這是多檔案對映。
(3)如果不是package節點那肯定就是mapper節點做單檔案對映。單檔案對映有3種方式
a. 第一種是resource屬性直接對映xml檔案;
b. 第二種是url屬性對映磁碟內的某個xml檔案;
c. 第三種是class屬性直接對映某個mapper介面.
(4)這裡通過檢視使用resouce方式進行對映得到的xml檔案解析流程,其他的都比較類似。
第五步:看resource方式解析xml。
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();
}
(1)第一行程式碼的意思是例項化一個錯誤上下文物件,其作用就是把使用mybatis過程中的錯誤資訊封裝起來,如果出現錯誤就會呼叫這個物件的toString方法。這個resource引數就是String型別的xml的名字,在我們的專案中是UserMapper.xml.
(2)然後和讀取核心配置檔案時候一樣的方式讀取這個UserMapper.xml獲取輸入流物件。
(3)然後建立一個mapper的xml檔案解析器,類似XMLConfigBuilder的作用,不過這裡是主要解析Mapper檔案的,XMLConfigBuilder是解析核心配置檔案的。
第六步:進入parse()方法:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
首先判斷這個xml是否被解析過了。因為configuration物件會維護一個String型別的set集合loadedResources,這個集合中存放了所有已經被解析過的xml的名字,我們在這裡是沒有被解析的,所以進入if中。
第七步:進入configurationElement()方法。
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);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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);
}
}
這個方法就是解析一個mapper.xml所有節點資料。比如解析namespace、resultMap、parameterMap、sql片段等等。重點是最後一句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
我們進入這個方法中buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
沒什麼好說的,繼續進入buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
(1)這個方法一開始是一個迴圈,遍歷一個list,這個list裡裝的是xml中的所有sql節點,比如select insert update delete ,每一個sql是一個節點。迴圈解析每一個sql節點。
(2)建立一個xml的會話解析器去解析每個節點。
第八步:進入parseStatementNode()方法
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
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
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 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 = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
看到這個方法很長,其實大致意思就是解析這個sql標籤裡的所有資料,並把所有資料通過addMappedStatement這個方法封裝在MappedStatement這個物件中。這個物件我們在第二部分介紹過,這個物件中封裝了一條sql所在標籤的所有內容,比如這個sql標籤的id ,sql語句,入參,出參,等等。我們要牢記一個sql的標籤對應一個MappedStatement物件。
第九步:進入addMapperStatement()方法
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
乍一看這個方法很長,我們只看最後三行程式碼。
(1) MappedStatement statement = statementBuilder.build();通過解析出的引數構建了一個MapperStatement物件。
(2)configuration.addMappedStatement(statement); 這行是把解析出來的MapperStatement裝到Configuration維護的Map集合中。key值是這個sql標籤的id值,我們這裡應該就是selectUserById,value值就是我們解析出來的MapperStatement物件。
其實我們解析xml的目的就是把每個xml中的每個增刪改查的sql標籤解析成一個個MapperStatement並把解析出來的這些物件裝到Configuration的Map中備用。
第十步: 返回第六步的程式碼:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
剛才到第九步都是在執行configurationElement(parser.evalNode("/mapper"));這行程式碼,接下來看下一行程式碼configuration.addLoadedResource(resource); 到第九步的時候我們已經把一個xml完全解析完了,所以在此就會把這個解析完的xml的名字裝到set集合中。
接下來我們看看bindMapperForNamespace(); 這個名字起得就很望文生義,通過名稱空間繫結mapper
第十一步:進入bindMapperForNamespace()方法。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
(1)一開始獲取名稱空間,名稱空間一般都是我們mapper的全限定名,它通過反射獲取這個mapper的class物件。
(2)if判斷,Configuration中也維護了一個Map物件,key值是我們剛才通過反射生產的mapper的class物件,value值是通過動態代理生產的class物件的代理物件。
(3)因為Map中還沒有裝我們生產的mapper物件,進入if中,它先把名稱空間存到我們剛才存xml名字的set集合中。然後再把生產的mapper的class物件存到Mapper中。
第十二步:進入addMapper()方法
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
我們發現它呼叫了mapperRegistry的addMapper方法,這個類通過名字就知道是mapper註冊類,我們再點進入看看
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
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);
}
}
}
}
我們可以看出mapperRegistry這個類維護的Map的名字是knownMappers---->(已知的mapper--->就是註冊過的mapper). 我們看他的put,key是我們生成的mapper的class物件,value是通過動態代理生成的mapper的代理物件。
到此mybatis根據主配置檔案初始化就完成了,那說了這麼久到底做了什麼呢?我們總結一下。
1、總的來說就是解析主配置檔案把主配置檔案裡的所有資訊封裝到Configuration這個物件中:
a.通過XmlConfigBuilder解析主配置檔案,然後通過XmlMapperBuild解析mappers下對映的所有xml檔案(迴圈解析)。
b.把每個xml中的各個sql解析成一個個MapperStatement物件裝在Configuration維護的一個Map集合中,key值是id,value是mapperstatement物件.
c.然後把解析過的xml的名字和名稱空間裝在set集合中,通過名稱空間反射生成的mapper的class物件以及class物件的代理物件裝在Configuration物件維護的mapperRegistry中的Map中。
2、簡化一點:主要就是把每個sql標籤解析成mapperstatement物件裝進集合,然後把mapper介面的class物件以及代理物件裝進集合,方便後來使用。
3、注意一點: 我們用resource引入xml的方法是先解析xml ,把各個sql標籤解析成mapperstatement物件裝進集合,然後再把mapper介面的class物件以及代理物件裝進集合,但是引入xml的方式有4種,其中單檔案引入方式還有url方式和class方式,看原始碼可以知道url方式就是直接引入一個xml和resource方式一模一樣。而class方式是引入一個mapper介面卻同(resource和url方式相反)
第十三步:我們看一下使用class方式引入的方法
else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
}
我們可以看出是先反射生產mapper介面的class物件,然後呼叫Configuration的addMpper方法,這個方法是不是很熟悉,我們點進去看一下
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
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);
}
}
}
}
是不是跟上面最後一步一樣,生產mapper的class物件後,再通過動態代理生產代理物件然後裝進集合。那我們介面物件生成了不還沒解析xml呢嘛,別急我們進入parser.parse()這個方法
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
你看它一開始會判斷這個mapper對應的xml是否存在於裝已經解析過的xml的set集合中,肯定沒有,沒有進入if中 重點來了---->loadXmlResource(); 這個方法看名字就知道是載入xml資源,我們點進去看一下
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
就是一頓往下走,走到 xmlParser.parse();這個方法中 我們點進去看一下:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
這個方法是不是很眼熟?沒錯,這就是我們第六步的程式碼。接下來想必大家都知道了,就是上面第六步到第九步。
我們可以看出--->用resource、url 和 class來解析的方式步驟是相反的。
resource和url是直接引入xml,那我們就先解析xml,然後通過xml的名稱空間反射生成mapper的class物件,再通過動態代理生產class物件的代理物件
而class方式填寫的是mapper介面的全限定名,就是上面的那個名稱空間,所以先生成class物件和代理物件,然後通過拼接字串就是全限定名+“.xml”獲取xml的名稱,然後再解析xml。
說到這單檔案對映就說完了,我們再說說多檔案對映。
第十四步:多檔案對映
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
}
它首先或得xml所在的包名,然後呼叫configuration的addMappers物件,是不是有點眼熟,單檔案對映是addMapper,多檔案對映是addMappers 你看人家這名字取得 絕了。我們點進去看看
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
我們看第三段程式碼,這是什麼意思呢?就是通過ResolverUtil這個解析工具類找出該包下的所有mapper的名稱通過反射生產mapper的class物件裝進集合中,然後看出迴圈呼叫addMapper(mapperClass)這個方法,這就和單檔案對映的class型別一樣了,把mapper介面的class物件作為引數傳進去,然後生產代理物件裝進集合然後再解析xml。
到此mybatis的初始化就說完了。
第四部分:獲取session會話物件原始碼分析
我們上一部分是mybatis的初始化,走的程式碼是:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
其實我們點進去會發現最後返回的是 DefaultSqlSessionFactory物件
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
獲取會話物件走的程式碼是:
SqlSession session = sqlSessionFactory.openSession();
直接open一個session,我們知道session是我們與資料庫互動的頂級api,所有的增刪改查都要呼叫session.我們進入openSession()
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
我們發現這個一個介面,不慌我們找他的實現類-->DefaultSqlSessionFactory
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我們看第二段程式碼:因為我們解析主配置檔案把所有的節點資訊都儲存在了configuration物件中,它開始直接或得Environment節點的資訊,這個節點配置了資料庫連線和事務。之後通過Environment建立了一個事務工廠,然後通過事務工廠例項化了一個事務物件。 重點來了------> 最後他建立了一個執行器Executor ,我們知道session是與資料庫互動的頂層api,session中會維護一個Executor 來負責sql生產和執行和查詢快取等。我們再來看看new這個執行器的時候的過程
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
這個過程就是判斷生成哪一種執行器的過程,mybatis的執行器有三種--->
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
SimpleExecutor: 簡單執行器,是 MyBatis 中預設使用的執行器,每執行一次 update 或 select,就開啟一個 Statement 物件,用完就直接關閉 Statement 物件(可以是 Statement 或者是 PreparedStatment 物件)
ReuseExecutor: 可重用執行器,這裡的重用指的是重複使用 Statement,它會在內部使用一個 Map 把建立的 Statement 都快取起來,每次執行 SQL 命令的時候,都會去判斷是否存在基於該 SQL 的 Statement 物件,如果存在 Statement 物件並且對應的 connection 還沒有關閉的情況下就繼續使用之前的 Statement 物件,並將其快取起來。
因為每一個 SqlSession 都有一個新的 Executor 物件,所以我們快取在 ReuseExecutor 上的Statement 作用域是同一個 SqlSession。
BatchExecutor: 批處理執行器,用於將多個SQL一次性輸出到資料庫
(貼上過來的) 我們如果沒有配置或者指定的話預設生成的就是SimpleExecutor。
執行器生成完後返回了一個DefaultSqlSession,這裡面維護了Configuration和Executor。
第五部分:查詢過程原始碼分析
首先我們把查詢的程式碼貼上過來
sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
sqlSession.selectOne("com.example.mybatis.mapper.UserMapper.getUserByUsername", "test1");
我為什麼要寫兩個一模一樣的查詢呢?因為mybatis有一級快取和二級快取,預設二級快取是不開啟的,可以通過配置開啟。而一級快取是開啟的,一級快取是session級別的快取,mybatis在查詢的時候會根據sql的id和引數等生產一個快取key,查詢資料庫的時候先查詢快取key是不是存在於快取中,如果沒有就查詢資料庫,如果存在就直接返回快取中的資料。需要注意的是除了查詢,其他的新增,更新,刪除都會清除所有快取,包括二級快取(如果開啟的話).
我們看控制檯資訊可以發現,第一次查的時候有sql語句列印,就是我紅線框的地方,然後輸出了 “我是第一次查詢的User(id=1, name=achuan, age=15)” 接著分割線下面直接輸出了 “我是第二次查詢的User(id=1, name=achuan, age=15)”,因為第一次查詢的時候拿著快取key去快取中查,沒有查到對應該key的快取,就查詢資料庫返回並把查出的資料放在快取中,第二次查詢的生成的key與第一次一樣,去快取中查到資料直接返回,沒有查詢資料庫,這樣可以提高查詢效率。
好了說了這麼多我們來開始分析原始碼-->selectOne() 我們進入selectOne()方法
<T> T selectOne(String statement, Object parameter);
@Override
public <T> T selectOne(String statement) {
return this.selectOne(statement, null);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
我們點進去發現是一個介面,不慌找它的實現類DefaultSqlSession,我們發現它進入了上面第三段程式碼,我們發現它呼叫了selectList()方法,其實查詢一個或者多個都是呼叫selectList方法,我們進入selectList()方法中
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
重點來了,我們看下這行程式碼
MappedStatement ms = configuration.getMappedStatement(statement);
我們呼叫selectOne的時候傳的引數是sql的id值 :selectUserById 和 sql的引數:1,在這行程式碼中引數statement的值就是selectUserById , 我們回憶一下,mybatis初始化的時候是不是把每個sql標籤解析成一個個的MapperStatement,並且把這些MapperStatement裝進configuration物件維護的一個Map集合中,這個Map集合的key值就是sql標籤的id,value是對應的mapperstatement物件,我們之前說裝進集合中備用就是在這裡用的,這裡用sql標籤的id值從Map中取出對應的MapperStatement物件。
比如我們現在selectOne方法呼叫的的是selectUserById 這個sql,所以現在通過selectUserById 這個key值從configuration維護的Map中取出對應的MapperStatement物件。為什麼要取出這個物件呢?因為mybatis把一個sql標籤的所有資料都封裝在了MapperStatement物件中。比如:出參型別,出參值,入參型別,入參值還有sql語句等等。
然後我們取出MapperStatement物件看下一行程式碼
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
MapperStatement被當做引數傳入query方法,這個query方法是執行器呼叫的,我們知道執行器的作用是sql的生成執行和查詢快取等操作,在這個query方法中我們會查詢快取和執行sql語句,我們進入query()方法
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
我們點進去發現是進入的Executor介面,不慌,找他的實現類,它先走的是CachingExcutor快取執行器,我們研究一下程式碼,我們看第二段程式碼他一開始從MapperStatement中獲取BoundSql 這個物件,因為真正的sql語句封裝在這個物件中,而且這個物件也負責把sql中的佔位符替換成我們傳的引數,只是MapperStatement維護了BoundSql 的引用而已。
然後我們繼續看createCacheKey,這個的意思就是根據這些引數生成一個快取key,當我們呼叫同一個sql,並且傳的引數是一樣的時候,生成的快取key是相同的。
然後我們看第三段程式碼,它一開始就是獲取快取,但是他這個快取並不是我們儲存查詢結果的地方(具體是快取什麼的我也不太清楚,我猜測這裡查的是二級快取,具體我沒測試,不出意外的話應該是二級快取,我們沒有開啟二級快取,所以這裡為null),它查詢快取為null,就會走最後一句程式碼
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
我們發現它又呼叫了delegate的query方法,delegate是什麼呢?我們看一下CachingExcutor的屬性
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
我們發現delegate是一個執行器的引用,在這裡其實是SimpleExcutor簡單執行器的引用,我們知道獲取一個會話session的時候會建立一個執行器,如果沒有配置的話預設建立的就是SimpleExcutor,在這裡把SimpleExcutor的引用維護到CachingExcutor中。實際這裡用到了委託者模式----->大致意思就是我自己不行我就找行的來做[手動滑稽] ,這裡就是快取執行器不行未能執行sql就交給SimpleExcutor來執行,我們進入這個query方法內。
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一點進去發現是Executor介面,不慌我們看他的實現類,他有兩個實現類快取執行器和基礎執行器,而基礎執行器有三個正常的兒子,他先回撥用爸爸基礎執行器裡面的query方法,也就是上面第二段程式碼,乍一看好像有點看不懂,沒事我們來分析一下,我們直接看try裡面的程式碼很容易明白
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一開始宣告瞭一個集合list,然後通過我們之前建立的快取key去本地快取localCache中查詢是否有快取,下面判斷,如果集合不是null就處理一下快取資料直接返回list,如果沒有快取,他回從資料庫中查,你看他們這名字起的一看就知道是什麼意思queryFromDatabase,我們現在執行的是第一條selectOne,沒有快取我們進入queryFromDatabase方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
你看這段程式碼,先在本地快取中佔個位,然後執行doQuery從資料庫中查資料,然後移除剛才的快取中的佔位,最後把查出來的資料put進本地快取中,我不知道他這個佔位又移除到底想搞什麼么蛾子,反正我們明白,那不重要,重要的是他執行了doQuery從資料庫中查到資料並放入快取中,我們接著看一下doQuery這個方法的程式碼
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
點進去是BaseExecutor抽象類,不慌找他的兒子SimpleExecutor,找到doQuery。doQuery方法一開始從configuration 中拿出會話處理器,會話處理器我們上面的元件介紹提到過,作用是 裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數等,那我們現在複習一下jdbc運算元據庫的步驟:
1 註冊驅動 2 獲取連線 3 建立會話物件 也就是上面提到的statement 或者是可以防止注入攻擊的prepareStatement 4 執行sql語句 5 處理結果集 6 關閉連線
他獲取會話處理器後,執行了prepareStatement(handler, ms.getStatementLog());這個是重點,熟悉的東西來了,我們進入這個方法看看
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
一開始就是獲取資料庫連線,然後執行handler.prepare();這個方法的作用就是根據連線事務啥的建立 會話物件 就是上面jdbc操作中的 第3 步。我們進入這個方法,跟之前一樣用到了委託者模式然後也是有兩個實現類,一個抽象類有三個實現類。
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
return delegate.prepare(connection, transactionTimeout);
}
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
點進去一看是一個介面,不慌走RoutingStatementHandler,這裡用到了委託者模式,委託給BaseStatementHandler, 到此就執行到了上面的第三段程式碼,我們觀察這段程式碼try中的三行程式碼
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
下面兩個就是設定會話物件的屬性不重要,重要的是instantiateStatement(connection),我們點進去看看
protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
點進去是抽象類,不慌,在PrepareStatmentHandler,我們發現return的全是prepareStatement預編譯會話物件,說明mybatis預設就可以防止注入攻擊。
然後我們返回獲取會話物件之前的程式碼
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
會話物件獲取完之後,又執行 了handler.parameterize(stmt);這個執行的步驟基本跟獲取會話物件的步驟一模一樣,最終執行的是三個兒子之一的PrepareStatementHandler中的parameterize方法
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
你看這裡用到了parameterHandler 引數處理器 ,這個處理器作用是:負責對使用者傳遞的引數轉換成JDBC Statement 所對應的資料型別 , 就是把String轉成varchar之類的。
到這裡 我們 獲取了資料庫連線 ,又獲得了會話物件,引數也設定好了,是不是該執行sql了,prepareStatement這個方法就執行完了,我們再返回撥用prepareStatement這個方法的方法
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
看,之前的操作就是為了返回預編譯的會話物件,返回後直接執行query方法,我們進入query方法:
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
我們點進去最終執行還是PrepareStatementHandler的query方法,把會話物件轉換成PreparedStatement預編譯的會話物件(這裡又轉換了一次,那之前的理解可能有點誤差),然後直接用會話物件呼叫execute方法,是不是jdbc一模一樣,在jdbc中我們獲取了會話物件也是呼叫execute方法。
sql執行了是不是該處理結果集了,我們看他的return, 用到了resultSetHandler,結果集處理器,這個元件上面的元件介紹提到過,作用是:負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合,就是把我們查到的資料轉換成list型別,我們現在是selectOne,所以這個集合中只有一條資料。
到此就把一次查詢的步驟說完了,其實說到底就是封裝了jdbc運算元據庫的步驟,最終還是和jdbc運算元據庫的步驟一模一樣。他的封裝就是為了讓我們可以更方便的傳參和處理結果集。
這時候已經把查詢出來的一條資料放在快取中了,再次呼叫第二條查詢語句的話,就不會運算元據庫了,而是直接從快取中拿這條資料。