preface
Welcome to our GitHub repository Star: github.com/bin39232820… The best time to plant a tree was ten years ago, followed by now
omg
- MyBatis source code learning (a)
- MyBatis source code learning (ii)
After the complex parsing process, now, MyBatis has entered the ready state, waiting for the user to command, SQL execution or the following points
- Generate implementation classes for the Mapper interface
- Generate SQL from the configuration information and set the runtime parameters into the SQL
- Implementation of the first and second level cache
- Plug-in mechanism
- Database connection acquisition and management
- Query result processing and lazy loading
SQL Execution Process
In the first place? I still put the front of the simplest process code to come out, our source code walk is based on that code
public void selectUser() throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("configuration.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
List<User> users = mapper.select();
System.out.println(users);
}
Copy the code
Now that we’ve got sqlSessionFactory ready, what’s next? Are we going to get sqlSession.getMapper through him or something
SQL execution in and out
When using MyBatis alone for database operations, we usually call getMapper method of SqlSession interface first to generate implementation classes for our Mapper interface. You can then perform database operations through Mapper. For example:
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao mapper = sqlSession.getMapper(UserDao.class);
Copy the code
SqlSession generates proxy objects for the interface using JDK dynamic proxy. When an interface method is invoked, the associated call is intercepted by the proxy logic. In the agent logic, the SQL and other information corresponding to the current method can be obtained according to the method name and method owning interface, and the database operation can be carried out after getting these information
Create a proxy object for Mapper
Let’s start with the getMapper method of DefaultSqlSession, as follows
// -☆ -defaultsQLSession Public <T> T getMapper(Class<T> type) {return configuration.<T>getMapper(type, this); } // -☆- Configuration public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } / / - do - MapperRegistryCopy the code
public <T> T getMapper(Class<T> type, SqlSession SqlSession) {// Get MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); If (mapperProxyFactory == null) {throw new BindingException("... ") ); } the try {/ / create a proxy object return mapperProxyFactory. NewInstance (sqlSession); } catch (Exception e) {throw new BindingException("..." ); }}Copy the code
After successive calls, the creation logic of the Proxy object for the Mapper interface emerges. If you haven’t analyzed the MyBatis configuration file parsing, you probably don’t know when elements in the knownMappers collection are deposited, so here’s a brief explanation. MyBatis calls MapperRegistry’s addMapper method to deposit the mapping of Class to MapperProxyFactory objects into knownMappers while parsing the nodes of the configuration file. The specific code will not be analyzed, you can read my previous articles, or analyze the relevant code.
Once you get the MapperProxyFactory object, you can call the factory method to generate a proxy object for the Mapper interface. The logic is as follows
// -☆- MapperProxyFactory public T newInstance(SqlSession SqlSession) {// Create a MapperProxy object, MapperProxy implements the InvocationHandler interface, Final MapperProxy<T> MapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }Copy the code
Protected T newInstance(MapperProxy<T> MapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); }Copy the code
The above code starts by creating a MapperProxy object that implements the InvocationHandler interface. The object is then passed as a parameter to the overloaded method, where the JDK dynamic proxy interface is called to generate the proxy object for Mapper. Now that the proxy object has been created, you can invoke the interface methods for database operations. Since interface methods are intercepted by proxy logic, let’s focus on the proxy logic to see what the proxy logic does.
Executing agent logic
Instead of using dynamic proxies, we implement the method shown below
The agent logic of the Mapper interface method first does some testing of the intercepted method to determine whether to perform subsequent database operations. The corresponding code is as follows
Public Object invoke(Object proxy, Method Method, Object[] args) throws Throwable {try {// If a Method is defined in an Object class, Call if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args); /* * The following code first appeared in mybatis- version 3.4.2 to support the * new feature in JDK 1.8 - default methods. The logic of this code is not analyzed. Interested students can * go to Github to see the relevant discussion (Issue #709). The link is as follows: * * https://github.com/mybatis/mybatis-3/issues/709 */ } else if (isDefaultMethod(method)) { protected T NewInstance (MapperProxy<T> MapperProxy) {// Create proxy objects through JDK dynamic proxy return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod MapperMethod = cachedMapperMethod(method); // Call the execute method to execute the SQL return mappermethod. execute(sqlSession, args); }Copy the code
As above, the proxy logic first checks whether the intercepted methods are defined in Object, such as equals, hashCode, and so on. For such methods, simply execute them. In addition, MyBatis provides support for the default method of JDK1.8 interface from version 3.4.2. After the detection, the MapperMethod object is fetched or created from the cache, and the SQL is executed through the execute method in the object. Before we look at the execute method, let’s look at the creation of the MapperMethod object. The MapperMethod creation process may seem mundane, but it contains some important logic that cannot be ignored.
Create the MapperMethod object
Let’s look at the constructor of a MapperMethod and see what kind of logos are involved in its constructor. The following
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<? > mapperInterface, Method Method, Configuration config){ This.mand = new SqlCommand(config, mapperInterface, method); this.mand = new SqlCommand(config, mapperInterface, method); This. Method = new MethodSignature(config, mapperInterface, method); }}Copy the code
The logic of the MapperMethod constructor is simple, creating SqlCommand and MethodSignature pairs. These two objects record different information, and I’m not going to talk about these two processes, but we’re done with the initialization logic of the MapperMethod
Execute the execute method
As mentioned above, the execute method mainly consists of a switch statement that performs database operations based on the SQL type. The logic of this method is clear and does not require much analysis. But convertArgsToSqlCommandParam method in the code above occurrences are frequent, this analysis:
/ / - being - MapperMethod public Object convertArgsToSqlCommandParam (Object [] args) {return paramNameResolver.getNamedParams(args); } public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (! HasParamAnnotation && paramCount == 1) {/* * If the method parameter list has no @param annotation and only one non-special parameter, the value of the * parameter is returned. For example: * List findList(RowBounds rb, String name) * names = {1: "0"} * in this case, return args[names.firstKey()], i.e. Args [1] -> name */ return args[names.firstkey ()]; } else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : Names.entryset ()) {// Add < parameter name, parameter Value > key-value pair to param param.put(entry.getValue(), args[entry. // genericParamName = param + index. Such as param1, param2,... paramN final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); // Check if genericParamName is included in names, and when? // The user explicitly sets the parameter name to param1, i.e. @param ("param1") if (! Names.containsvalue (genericParamName) {// Add <param*, value> to param. Put (genericParamName, args[entry.getKey()]); }i++; } return param; }}Copy the code
ConvertArgsToSqlCommandParam is an empty shell method and the method call the ParamNameResolver getNamedParams method finally. The main logic of the getNamedParams method is to return different results depending on the condition, and the code for this method is not hard to understand
The execution process of the query statement
There are many methods corresponding to query statements, including the following:
- executeWithResultHandler
- executeForMany
- executeForMap
- executeForCursor
These methods internally call select methods in SqlSession, such as selectList, selectMap, selectCursor, and so on. The return value types of these methods are different, so you need a special handling method for each return type. Take the selectList method, which returns a value of type List. However, if our Mapper or Dao interface method returns a value of type array or Set, it is not appropriate to directly return the result of type List to Mapper/Dao. Methods such as execute simply encapsulate methods such as SELECT, so we should focus on those methods next.
SelectOne method analysis
It may come as a surprise that this section chooses to analyze the selectOne method rather than the other. SelectList, selectMap, selectCursor, and so on have been mentioned before, but here we examine an unmentioned method. There’s nothing special about doing this, mainly because selectOne internally calls the selectList method. The point of analyzing the selectOne method here is to show you that the selectOne and selectList methods are related, and analyzing the selectOne method is the same thing as analyzing the selectList method. If you don’t believe it, then we look at the source code, source code before no secrets.
// -☆ -defaultsQLSESSION Public <T> T selectOne(String statement, <T> List = this.<T>selectList(statement, parameter); If (list.size() == 1) {// Return list.get(0); } else if (list.size() > 1) {// Throw new TooManyResultsException("... "); ); } else { return null; }}Copy the code
This exception I don’t believe you didn’t come across, ha ha, only know how to read the source code to solve these abnormal, how do you say Before the encounter an API will not with me in the first place is baidu, encounter are, but for now we often use the framework I’ll point in to see the source code, the feeling is a bit progress slowly, Of course, I am not familiar with the box or Baidu, ha ha.
As above, the selectOne method internally calls the selectList method and takes the first element of the selectList return value as its own return value. If the selectList returns a list element greater than 1, an exception is thrown. The above code is easier to understand, not to say. Let’s look at the implementation of the selectList method.
// -☆ -defaultsQLSession Public <E> List<E> selectList(String statement, Return this.selectList(statement, parameter, rowbound.default); }Copy the code
private final Executor executor; public <E> List<E> selectList(String statement, Object parameter, RowBounds RowBounds) {try {/ / get MappedStatement MappedStatement ms = configuration. GetMappedStatement (statement); Return Executor. Query (ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } the catch (Exception e) {throw ExceptionFactory. WrapException ("..." ); } finally { ErrorContext.instance().reset(); }}Copy the code
As mentioned above, the executor variable is of type executor. Executor is an interface with the following implementation classes:
Executor has so many implementation classes that you can guess which implementation class the Executor variable corresponds to. To figure out this problem, we need to check the source. As a reminder, if you follow the openSession method of DefaultSqlSessionFactory, you will soon see the trail of executor variable creation. For space reasons, this article does not analyze the source code of the openSession method. By default, executor is of type CachingExecutor, which is a decorator class used to add level 2 caching to the target executor. So who is the target Executor? The default is SimpleExecutor
Now that you know the identity of the Executor variable, let’s move on to the call stack for the selectOne method. Let’s take a look at how the Query method of CachingExecutor is implemented. As follows:
// -☆ -cachingExecutor public <E> List<E> query(MappedStatement MS, Object parameterObject, RowBounds RowBounds, ResultHandler ResultHandler) throws SQLException {// Obtain BoundSql BoundSql BoundSql = Ms. GetBoundSql (parameterObject); CacheKey Key = createCacheKey(MS, parameterObject, rowBounds, boundSql); Query (ms, parameterObject, rowBounds, resultHandler, key, boundSql); return query(MS, parameterObject, rowBounds, resultHandler, key, boundSql); }Copy the code
The above code gets the BoundSql object, creates the CacheKey object, and then passes both objects to the reload method. The BoundSql retrieval process is more complex, which I’ll examine in the next section. CacheKey and the subsequent primary and secondary caches will be analyzed in separate chapters. The above method code is no different from the implementation in SimpleExecutor’s BaseExecutor superclass, except for the overloaded methods it calls. Keep reading
// -☆ -cachingExecutor public <E> List<E> query(MappedStatement MS, Object parameterObject, RowBounds RowBounds, ResultHandler resultHandler, CacheKey key, BoundSql BoundSql) throws SQLException {// Obtain Cache information from MappedStatement Cache = ms.getCache(); // If no cache or reference cache is configured in the mapping file, cache = null if (cache! = null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); List<E> list = (List<E>) tcm.getObject(cache, key); If (list == null) { List = delegate.<E> Query (MS, parameterObject, rowBounds, resultHandler, Key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; Return delegate.<E> Query (ms, parameterObject, rowBounds, resultHandler, Key, boundSql); }Copy the code
The above code refers to the secondary cache. If the secondary cache is empty or missed, the query method of the decorated class is called. Let’s look at how the query method with the same signature is implemented in BaseExecutor.
// -☆ -baseexecutor public <E> List<E> query(MappedStatement MS, Object parameter, RowBounds RowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 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) {/ / stored procedure related processing logic, this article does not analyze the process of storage, so this method is not analyzed handleLocallyCachedOutputParameters (ms, the key parameter, boundSql); } else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) {for (DeferredLoad DeferredLoad: deferredLoads) {deferredload.load (); } deferredLoads.clear(); if (configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; }Copy the code
The preceding method is mainly used to search for the query result from the level-1 cache. If the cache is not matched, the database is queried. In the code above, a new class DeferredLoad appears, which is used for lazy loading. The implementation of this class isn’t complicated, but I’m a little confused about its purpose. I am not completely clear about this, so I will not analyze it. Next, we’ll look at the implementation of the queryFromDatabase method
// -☆ -baseExecutor private <E> List<E> queryFromDatabase(MappedStatement MS, Object parameter, RowBounds RowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // Store a placeholder localCache.putobject (key, EXECUTION_PLACEHOLDER) into the cache; List = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally {// remove placeholder localCache.removeObject(key); } // Cache the query result localcache.putobject (key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }Copy the code
The above code is still not the end of the selectOne method call stack. Regardless of caching operations, queryFromDatabase will eventually call doQuery for the query. So let’s keep tracking.
// -☆ -simpleExecutor 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(); / / create StatementHandler StatementHandler handler = configuration. NewStatementHandler (wrapper, ms, parameter, rowBounds, resultHandler, boundSql); STMT = prepareStatement(handler, Ms. GetStatementLog ()); Return handler.<E>query(STMT, resultHandler); } finally {// closeStatement closeStatement(STMT); }}Copy the code
There’s still a lot of logic in the doQuery method, and it’s not at all obvious that the end point is imminent, but this is one step closer to the end point. Next, we will skip the StatementHandler and Statement creation processes, which will be explained later. Here, we take the PreparedStatementHandler as an example to see how its Query method is implemented. As follows:
// - * -preparedStatementhandler public <E> List<E> query(Statement Statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; SQL ps.execute(); Return resultSetHandler.<E>handleResultSets(ps); }Copy the code
At this point, there seems to be hope that the whole call process is finally coming to an end. But don’t get too excited, the processing of SQL execution results can be quite complex, and I’ll cover it in a section later. That’s how the selectOne method works, and even though I’ve simplified the code analysis, it still looks pretty complicated. The query process involves many method calls. It is difficult to have a deep understanding of the query process of MyBatis without making these call methods clear.
At the end
DefaultSqlSession (selectOne); DefaultSqlSession (selectOne); SelectList () > 1 to see if an exception needs to be thrown. SelectList () calls the executor. Query method. NewExecutor is generated when openSession is called. By default, executor type is CachingExecutor, So the next step is to look at the steps in cachingExecutor.query that I’m not going to go through all the way down here (which involves one or two caches), And then he’s going to call Query and then BaseExecutor queryFromDatabase and that’s the method to call the database and then doQuery and then jDBC-like operations and that’s the whole process of a query.
from
- A small Mybatis source book
Daily for praise
Ok, everybody, that’s all for this article, you can see people here, they are real fans.
Creation is not easy, your support and recognition, is the biggest motivation for my creation, we will see in the next article
Six pulse excalibur | article “original” if there are any errors in this blog, please give criticisms, be obliged!