preface
Mybatis is a common ORM framework in Java development. In daily work, we are directly through Spring Boot automatic configuration, and directly use, but do not know how Mybatis is to execute a SQL statement, and this article is to uncover the mystery of Mybatis.
Based on the component
If we want to understand the execution process of Mybatis, we must first understand what are some important classes in Mybatis, what are the responsibilities of these classes?
SqlSession
We’re all familiar with it. It hides the low-level details by providing the methods needed to interact with the database. Its default implementation class is DefaultSqlSession
Executor
This is the executor to which all database operations in SqlSession are delegated. It has multiple implementation classes that can use different functionality.
Configuration
It is a very important configuration class, it contains all the useful information of Mybatis, including XML configuration, dynamic SQL statements and so on, we can see this class everywhere.
MapperProxy
This is a very important proxy class, it is the proxy in Mybatis mapping SQL interface. This is the Dao interface we often write about.
The working process
Initial use
First, we need to get an SqlSessionFactory object, which is used to get the SqlSession object.
// Read the configuration
InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
Create an SqlSessionFactory object
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
Copy the code
Once we have an SqlSessionFactory object, we can use its openSession method to get an SqlSession object.
SqlSession sqlSession = sqlSessionFactory.openSession(true);
Copy the code
Finally, we get the Mapper through the SqlSession object so that we can get data from the database.
// Get the Mapper object
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// Execute the method to get data from the database
Hero hero = mapper.selectById(1);
Copy the code
Detailed process
Obtain the MapperProxy object
Our main focus now is the getMapper method, which creates a proxy object for us that provides important support for executing SQL statements.
/ / SqlSession objects
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Copy the code
The getMapper method delegates the Configuration object to obtain the corresponding Mapper proxy object. As mentioned before, the Configuration object contains all the important information in Mybatis, including the Mapper proxy object we need. And this information has to be done at the time of reading configuration information, namely implement the sqlSessionFactoryBuilder is. The build method.
/ / the Configuration object
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
Copy the code
We can see that it delegates the retrieval of the Mapper proxy object to the MapperRegistry object. In fact, it’s not the Mapper proxy object we want, it’s the factory of the Mapper proxy object, Mybatis uses factory mode here.
public class MapperRegistry {
private final Configuration config;
private finalMap<Class<? >, MapperProxyFactory<? >> knownMappers =new HashMap<>();
public MapperRegistry(Configuration config) {
this.config = config;
}
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: "+ e, e); }}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); } } } } }Copy the code
I just kept the getMapper method and the addMapper method.
In the getMapper method, it gets the MapperProxyFactory object, and we know from the name that this is a MapperProxy object factory, but we want to get a MapperProxy object, not a factory object, Let’s look at getMapper method, which through mapperProxyFactory. NewInstance to create a proxy object.
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
Copy the code
We create a MapperProxy object and use the proxy.newProxyInstance method to create a Proxy object, which is the desired result. Which object is being represented here? MapperInterface is a member variable that refers to the object to be proxied. Mybatis will generate a MapperProxyFactory object for each interface that needs to be proxied. The function of this object is to create the required proxy object.
Cache execution method
Once we get the proxy object Mapper, we can execute the methods in it.
Here’s an example:
// The interface required by Myabtis
public interface HeroMapper {
Hero selectById(Integer id);
}
Copy the code
// HeroMapper interface corresponding XML file
<! DOCTYPEmapper
PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test.HeroMapper">
<select id="selectById" resultType="test.Hero">
select * from hero where id = #{id}
</select>
</mapper>
Copy the code
We execute the selectById method to get information about a user.
// Get the Mapper object
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// Execute the method to get data from the database
Hero hero = mapper.selectById(1);
Copy the code
Mapper is a reference to a proxy object, and this proxy class is MapperProxy, so we mainly want to understand what the proxy class MapperProxy does.
public class MapperProxy<T> implements InvocationHandler.Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
returncachedInvoker(method).invoke(proxy, method, args, sqlSession); }}catch (Throwable t) {
throwExceptionUtil.unwrapThrowable(t); }}private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
return methodCache.computeIfAbsent(method, m -> {
return new PlainMethodInvoker(newMapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }}private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super(a);this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
returnmapperMethod.execute(sqlSession, args); }}}Copy the code
Invoke (method). Invoke (proxy, method, args, sqlSession); invoke(proxy, method, args, sqlSession);
Let’s start with the cachedInvoker Method, which takes a Method of type, so this Method represents the Method that we’re executing heromapper.selectByid, It first gets a call from the cache to see if a method executor, PlainMethodInvoker, has previously been created for that method. This is actually just a wrapper class, which is optional and, in engineering terms, much easier to maintain. The executor has only one member object, MapperMethod, and the constructor of the MapperMethod passes HeroMapper, HeroMapper. SelectById, and Cofiguration.
With all the above steps done, we can then see that the Invoke method of PlainMethodInvoker executes, which delegates the real operation to MapperMethod, executes the Execute method under MapperMethod, This approach is the focus of this article.
Structural parameters
As you can see from the above parsing, this method is eventually executed.
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
return result;
}
Copy the code
In this method, we can see some familiar keywords: Select, update, delete, insert, select, update, update, update, insert, etc. It’s still back to the SqlSession object we mentioned in the beginning.
In this method, first structure parameters, namely we see convertArgsToSqlCommandParam method, its internal implementation methods to transformation parameters are as follows:
Use @param for custom naming
Amethod (@ Param int a, @ Param int b) will construct a map – > [{” a “, a_arg}, {” b “, b_arg}, {” param1, “a_arg}, {” param2,” b_arg}]. A and param1 are the names of parameter A, a_arg is the actual value passed.
Although there are only two parameters, there will end up being four key-value pairs in the Map because Mybatis will end up generating parameter names prefixed with param and named according to the position of the parameter.
Do not use the @ param
Amethod (int a, int b), will construct the map – > [{” arg0 “, a_arg}, {” arg1, “b_arg}, {” param1,” a_arg}, {” param2, “b_arg}]. Since there is no custom name for the parameter, Myabtis takes a default name for the parameter, prefixed by arg and suffixed by position.
If there is only one argument and the argument is a set, multiple key-value pairs will be stored:
Amethod (Collection
a), in which case map -> [{“arg0″, a_arg}, {” Collection “, a_arg}] is constructed.
Amethod (List
a), in which case map -> [{“arg0”, a_arg}, {“collection”, a_arg}, {” List “, a_arg}] is constructed.
Amethod (Integer[] a), in which case map -> [{“arg0”, a_arg}, {“array”, a_arg}] is constructed
However, if there are two parameters, it is not stored this way, but in the normal way:
Amethod (List < Integer > a, List < Integer > b) will construct a map – > [{” arg0 “, a_arg}, {” arg1, “b_arg}, {” param1,” a_arg}, {” param2, “b_arg}]
Amethod (List < Integer > a, int b) will construct a map – > [{” arg0 “, a_arg}, {” arg1, “b_arg}, {” param1,” a_arg}, {” param2, “b_arg}]
Objects that do not take arguments
There are two special objects in Mybatis: RowBounds and ResultHandler, which are not put into the map as arguments, but take up positions.
Amethod (int a, RowBounds rb, int b), in which case, Will construct a map – > [{” arg0 “, a_arg}, {” arg2, “b_arg}, {” param1,” a_arg}, {” param2, “b_arg}]
Note that the b arguments are named arg2 and param2, arg2 because it is in the third position of the argument and param2 because it is the second valid argument.
Gets the SQL object to execute
With the parameters constructed, we need to find the SQL statement to execute.
@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; }}Copy the code
Although the statement here is of type String, but it is not really a SQL statement, it is a search for corresponding MapperStatement the name of the object, in our case, it is a test. HeroMapper. SelectById, Mybatis uses this name to find objects that contain SQL statements.
We trace the execution of the code and end up with the following method, which is an overloaded method with three arguments.
@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(); }}Copy the code
In line 4, you can see that it gets a MapperStatement object from the Configuration object through statement, The MapperStatement object contains information provided by the < SELECT >,
, < DELETE >, and < INSERT > elements. The information defined in these elements is stored in this object, for example: Sql statements, resultMap, fetchSize, and so on.
Execute SQL statement
Once the object containing the SQL statement information is retrieved, it is handed to the Execute executor object to perform the subsequent processing, known as the executor.query method.
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
Copy the code
Get the Sql statement that needs to be executed, and then create a cache key for level 1 caching.
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
/ /...
If there is data in the cache, it is returned directly from the cache, otherwise it is queried from the database
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
return list;
}
Copy the code
Finally, a doQuery method is executed
@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); }}Copy the code
This code creates a StatementHandler handler for a Statement object. The handler is responsible for preparing a PrepareStatement object in JDBC, including: Create a PrepareStatement object, set the SQL statement to be executed, and assign values to the parameters in the SQL statement. Once this is done, it’s time to get the data from the database.
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
Copy the code
The fourth line of code executes the corresponding Sql query, followed by processing the results.
conclusion
Mybatis proxies our Dao interface class through MapperProxy to help us execute predefined Sql statements and Cache corresponding execution results through Cache. Create a PrepareStatement object using StatementHandler and perform SQL operations using JDBC.