SQL statement execution process of MyBatis

1, Revisit JDBC

Java Database Connectivity, or JDBC for short. It is an application program interface in Java language to standardize how client programs access the database. It provides methods such as querying and updating data in the database. With the development of Java ORM framework, there are few opportunities to write JDBC code in the production system to access the database, but we still need to be familiar with the basic process. Let's take a simple query as an example to review JDBC.
public static void main(String[] args) throws Exception {
    Connection conn = getConnection();  
    String sql = "select * from user where 1=1 and id = ?";
    PreparedStatement stmt = conn.prepareStatement(sql);
    stmt.setString(1, "501440165655347200");
    ResultSet rs = stmt.executeQuery();
        String username = rs.getString("username");
        System.out.print("full name: " + username);
From the above code, a simple database query operation can be divided into several steps.
  • Create Connection
  • Pass in a parameterized query SQL statement to build a precompiled object PreparedStatement
  • Set parameters
  • Execute SQL
  • Get data from result set
So how does Mybatis complete this process?

2, sqlSession

In the previous chapter, we have seen that the userMapper injected through @ Autowired in the Service layer is a proxy class. When executing the method, it actually calls the invoke notification method of the proxy class.
public class MapperProxy<T> implements InvocationHandler{
    public Object invoke(Object proxy, Method method, Object[] args)

        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);

1. Create MapperMethod object

There are only two attributes in MapperMethod object, SqlCommand and MethodSignature.
SqlCommand contains the name of the execution method and the type of the method, such as unknown, insert, update, delete, select, flow. MethodSignature can be simply understood as the signature information of a method. It includes: return value type, void, set type, Cursor, etc. It mainly obtains the name of the @ Param annotation on the method parameter, which is convenient for obtaining the parameter value in the next step. For example, if @ Param is added to the method:
User getUserById(@Param(value="id")String id,@Param(value="password")String password);

The parameter will be resolved to {0=id, 1=password}.

2. Execute

Judge the SQL type and return value type of the method and call the corresponding method. Taking the method User getUserById(String id,String password) as an example, it will call the selectOne() method.
public class MapperMethod {
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
        case INSERT: {}
        case UPDATE: {}
        case DELETE: {}
        case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
            //No return value
        } else if (method.returnsMany()) {
            //Return collection type
            result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
            //return Map type
            result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
            //return Cursor
            result = executeForCursor(sqlSession, args);
        } else {
            //Will parameter args Convert to SQL Parameters of the command
            //One is added by default< param+Parameter name of parameter index
            //{password=123456, id=501441819331002368, param1=501441819331002368, param2=123456}
            Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
        return result;
You can see that sqlSession.selectOne can get the value in the database and complete the conversion. The sqlSession here is the object of the SqlSessionTemplate instance, so it will call to.
public class SqlSessionTemplate{
    public <T> T selectOne(String statement, Object parameter) {
        return this.sqlSessionProxy.<T> selectOne(statement, parameter);
sqlSessionProxy is also a proxy object. We also analyzed its creation carefully last class. In short, it will actually call SqlSessionInterceptor.invoke().

3. Create sqlSession object

As the main top-level API of MyBatis, sqlSession represents the session interacting with the database and completes the necessary database addition, deletion, modification and query functions. Its creation, execution, submission and resource cleaning are all completed in the notification method of SqlSessionInterceptor.
private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        //establish SqlSession object
        SqlSession sqlSession = getSqlSession(
        try {
            //call sqlSession Practical method
            Object result = method.invoke(sqlSession, args);
            return result;
        } catch (Throwable t) {
        } finally {
            if (sqlSession != null) {
                closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
The above key point is to create SqlSession and execute its method. It is an object of DefaultSqlSession instance. There is mainly an actuator created through configuration. Here, it is simpleexecution. Then, what the invoke method actually calls is DefaultSqlSession.selectOne().

3, Get BoundSql object

The selectOne() method in DefaultSqlSession will eventually call the selectList() method. It first obtains the corresponding MappedStatement object from the full name of the request method from the data master configuration, then calls the query method of the executor.

1. Get MappedStatement object

//statement Is the full name of the calling method, parameter For parameter Map
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    //stay mapper.xml Each of SQL Nodes are encapsulated as MappedStatement object
    //stay configuration You can obtain the corresponding method by the full name of the request method MappedStatement object
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
There is a method wrapCollection(parameter). We can understand that if the parameter is a collection type or an array type, it will set the parameter name to the name of the corresponding type.
private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
        StrictMap<Object> map = new StrictMap<Object>();
        map.put("collection", object);
        if (object instanceof List) {
        map.put("list", object);
        return map;
    } else if (object != null && object.getClass().isArray()) {
        StrictMap<Object> map = new StrictMap<Object>();
        map.put("array", object);
        return map;
    return object;

2. Get BoundSql object

In the housekeeper object of configuration, all SQL nodes in mapper.xml are saved. Each node corresponds to a MappedStatement object, and various dynamically generated sqlnodes are saved in the SqlSource object. One method of the SqlSource object is getBoundSql(). Let's take a look at the properties of the BoundSql class.
public class BoundSql { 
    //Dynamically generated SQL,After parsing, the file with occupancy SQL
    private final String sql;
    //Information for each parameter. Such as parameter name and input/Output type and corresponding JDBC Type, etc
    private final List<ParameterMapping> parameterMappings;
    private final Object parameterObject;
    private final Map<String, Object> additionalParameters;
    private final MetaObject metaParameters;
Seeing these attributes explains the meaning of BoundSql. That is, it represents dynamically generated SQL statements and corresponding parameter information. Different types of SQL will generate different types of SqlSource objects. For example, static SQL will generate StaticSqlSource objects and dynamic SQL will generate DynamicSqlSource objects.
  • Static SQL
Static SQL is relatively simple. You can directly create a BoundSql object and return it.
public class StaticSqlSource implements SqlSource {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  • Dynamic SQL
Dynamic SQL should call the corresponding apply method according to different sqlNode nodes, and some should judge whether to add the current node through Ognl expression, such as IfSqlNode.
public class DynamicSqlSource implements SqlSource {
    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        //rootSqlNode by sqlNode The outermost encapsulation of the node, i.e MixedSqlNode. 
        //Parse all sqlNode,take sql Content set to context
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        //Setting parameter information will SQL#{} replace with placeholder
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        //establish BoundSql object
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        return boundSql;
rootSqlNode.apply(context) is an iterative calling process. The last generated content is saved in the DynamicContext object, such as select * from user WHERE uid=#{uid}.
Then we call the SqlSourceBuilder.parse() method. It mainly does two things:
  1. Replace the #{} in the SQL statement with a placeholder
  2. Encapsulate #{} the fields into ParameterMapping objects and add them to parameterMappings.
The ParameterMapping object stores the type information of the parameter. If it is not configured, it is null.
ParameterMapping{property='uid', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

Finally, the returned BoundSql object contains a SQL with placeholders and the specific information of parameters.

4, Execute SQL

After creating the BoundSql object, call the query method and go to cacheingexecution. Query(). The front of this method is the judgment of L2 cache. If L2 cache is enabled and there is data in the cache, it will be returned.

1. Cache

public class CachingExecutor implements Executor {
    public <E> List<E> query(MappedStatement ms, Object parameterObject, 
        RowBounds rowBounds, ResultHandler resultHandler, 
        CacheKey key, BoundSql boundSql)throws SQLException {
        //Application of L2 cache
        //If configured</cache>Then enter this process
        Cache cache = ms.getCache();
        if (cache != null) {
            if (ms.isUseCache() && resultHandler == null) {
                //Get data from cache
                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);
Next, look at the query method, create a PreparedStatement precompiled object, execute SQL and get the return collection.
public class SimpleExecutor extends BaseExecutor {
    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();
            //obtain Statement Is the default type PreparedStatementHandler
            //Note that if the plug-in is configured here, the StatementHandler A proxy may be returned
            StatementHandler handler = configuration.newStatementHandler(wrapper, 
                          ms, parameter, rowBounds, resultHandler, boundSql);
//establish PreparedStatement Object and set parameter values stmt = prepareStatement(handler, ms.getStatementLog()); //implement execute And return the result set return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } } }
The prepareStatement method obtains the database connection and builds the Statement object to set the SQL parameters.

1. Create PreparedStatement

public class SimpleExecutor extends BaseExecutor {
    private Statement prepareStatement(StatementHandler handler, Log statementLog) {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        return stmt;
  • Get Connection

We see that the getConnection method is where the Connection is obtained. However, this Connection is also a proxy object, and its caller processor is ConnectionLogger. Obviously, it is to print the log more conveniently.
public abstract class BaseExecutor implements Executor {
    protected Connection getConnection(Log statementLog) throws SQLException {
        //from c3p0 Get a connection from the connection pool
        Connection connection = transaction.getConnection();
        //If the log level is Debug,Generates a proxy object return for this connection
        //Its processing class is ConnectionLogger
        if (statementLog.isDebugEnabled()) {
            return ConnectionLogger.newInstance(connection, statementLog, queryStack);
        } else {
            return connection;
  • Perform precompiling
This is the same as our JDBC code. Get the SQL and call the prepareStatement(sql) of the connection. However, because connection is a proxy object, it seems not so simple.
public class PreparedStatementHandler
    protected Statement instantiateStatement(Connection connection) throws SQLException {
        String sql = boundSql.getSql();
        return connection.prepareStatement(sql);
Therefore, when executing connection. Preparestatement (SQL), the actual call is invoke() of ConnectionLogger class.
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] params)throws Throwable {
        try {
            if ("prepareStatement".equals(method.getName())) {
                if (isDebugEnabled()) {
                    debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
                //call connection.prepareStatement
                PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
                //Again for stmt The proxy object is created, and the notification class is PreparedStatementLogger
                stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
                return stmt;

public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl, 
            new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
Sure enough, it's not that simple. The PreparedStatement returned in the end is a proxy object.
  • Set parameters
When setting parameters, there are many options, such as stmt.setString(), stmt.setInt(), stmt.setFloat(), or stmt.setObject(). Of course, as an excellent ORM framework, Mybatis cannot be so rude. It first obtains all JDBC type processors according to the Java type of the parameter, and then obtains the corresponding processors according to the JDBC type. Here we do not configure JDBC type, so its type is NULL, and the last returned is StringTypeHandler.
public class StringTypeHandler extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps, int i, 
            String parameter, JdbcType jdbcType)throws SQLException {
        ps.setString(i, parameter);

2. Execute

After the SQL precompilation is completed, execute() is executed.
public class PreparedStatementHandler{
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
        PreparedStatement ps = (PreparedStatement) statement;
        return resultSetHandler.<E> handleResultSets(ps);
The PreparedStatement object here is also a proxy class. When calling the notification class PreparedStatementLogger to execute, it only prints the value of the parameter. That is, Parameters: 501868995461251072(String).

5, Process return value

In the above method, we see that the SQL has been submitted to the database for execution, so the last step is to obtain the return value.
public class DefaultResultSetHandler implements ResultSetHandler {
    public List<Object> handleResultSets(Statement stmt) throws SQLException {
        final List<Object> multipleResults = new ArrayList<Object>();
        int resultSetCount = 0;
        //take ResultSet Package into ResultSetWrapper object
        ResultSetWrapper rsw = getFirstResultSet(stmt);
        //return mapper.xml Configured in rsultMap In fact, we don't have a configuration, but there will be a default one
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        int resultMapCount = resultMaps.size();
        //Process the return value of the database, and finally add it to multipleResults
        while (rsw != null && resultMapCount > resultSetCount) {
            ResultMap resultMap = resultMaps.get(resultSetCount);
            handleResultSet(rsw, resultMap, multipleResults, null);
        return collapseSingleResultList(multipleResults);

1. ResultSetWrapper object

In the above code, we can see that the ResultSet object is encapsulated into a ResultSetWrapper object in the first step. We need to look at it in detail.
public class ResultSetWrapper {
    public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
        //All registered processor types
        this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        //ResultSet object
        this.resultSet = rs;
        //Metadata column name, column type and other information
        final ResultSetMetaData metaData = rs.getMetaData();
        final int columnCount = metaData.getColumnCount();
        //Loop column, column name, column corresponding JDBC Type and column Java Get all types
        for (int i = 1; i <= columnCount; i++) {
The above key point is to get the information on the database column, which will be used in parsing.

2. Process return value

The handleResultSet method is finally invoked to DefaultResultSetHandler.handleRowValuesForSimpleResultMap().  
public class DefaultResultSetHandler implements ResultSetHandler {
    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, 
            ResultMap resultMap, ResultHandler<?> resultHandler, 
            RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {
        DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
        //Skip line Mybatis of RowBounds Paging function
        skipRows(rsw.getResultSet(), rowBounds);
        while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
            Object rowValue = getRowValue(rsw, discriminatedResultMap);
            storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
In this place, we refer to RowBounds, an object paged in Mybatis. But in fact, I don't use it. Because it is a logical page, not a physical page.
  • RowBounds
Two properties in the rowboundaries object control paging: offset and limit. Offset means that the paging starts from the first data, and limit means how many data are taken in total. Because we have not configured it, it defaults to offset, starting from 0, and limit takes the maximum value of Int.
public class RowBounds {
    public static final int NO_ROW_OFFSET = 0;
    public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
    public RowBounds() {
        this.offset = NO_ROW_OFFSET;
        this.limit = NO_ROW_LIMIT;
The skipRows method is to skip offset, and its implementation is relatively simple.  
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    for (int i = 0; i < rowBounds.getOffset(); i++) {
How to control the Limit after the offset is skipped? This depends on the while loop above.  
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
    //Processing data
The key is the shouldProcessMoreRows() method, which is actually a simple judgment.
private boolean shouldProcessMoreRows(ResultContext<?> context, 
                            RowBounds rowBounds) throws SQLException {
    //It is to see whether the obtained data is small and consistent Limit
    return context.getResultCount() < rowBounds.getLimit();
  • obtain
The while loop obtains each row of data in the ResultSet, and then obtains the data through rs.getxxx().
public class DefaultResultSetHandler implements ResultSetHandler {
    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
        //Create a return value type. For example, we return User Entity class
        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            final MetaObject metaObject = configuration.newMetaObject(rowValue);
            boolean foundValues = this.useConstructorMappings;
            if (shouldApplyAutomaticMappings(resultMap, false)) {
            //Automatic mapping
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
        //This process is configured ResultMap,Manually configure database column names and Java Mapping of entity class fields
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        return rowValue;

1. Get return value type

The first step is to get the return value type. The process is to get the Class object, then get the constructor, set the accessible and return the instance.
private  <T> T instantiateClass(Class<T> type, 
            List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    Constructor<T> constructor;
    if (constructorArgTypes == null || constructorArgs == null) {
        //Get constructor
        constructor = type.getDeclaredConstructor();
        if (!constructor.isAccessible()) {
            //Set accessibility
        //Return instance
        return constructor.newInstance();
After returning, it is wrapped as a MetaObject object object. Mybatis will be wrapped into different Wrapper objects according to different return value types. In this example, because it is an entity class, BeanWrapper will be returned.
private MetaObject(Object object, ObjectFactory objectFactory, 
            ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
    this.originalObject = object;
    this.objectFactory = objectFactory;
    this.objectWrapperFactory = objectWrapperFactory;
    this.reflectorFactory = reflectorFactory;

    if (object instanceof ObjectWrapper) {
        this.objectWrapper = (ObjectWrapper) object;
    } else if (objectWrapperFactory.hasWrapperFor(object)) {
        this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
    } else if (object instanceof Map) {
        this.objectWrapper = new MapWrapper(this, (Map) object);
    } else if (object instanceof Collection) {
        this.objectWrapper = new CollectionWrapper(this, (Collection) object);
    } else {
        this.objectWrapper = new BeanWrapper(this, object);


In mapper.xml, we can declare a resultMap node, which corresponds the column name in the database to the field name in Java, and applies it to the resultMap of the SQL node. You can also directly use resultType to return a Bean without configuring it. However, these two methods correspond to two parsing methods.
private boolean applyAutomaticMappings(ResultSetWrapper rsw, 
        ResultMap resultMap, MetaObject metaObject, 
        String columnPrefix) throws SQLException {  
    //Gets the type of the corresponding field
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
        for (UnMappedColumnAutoMapping mapping : autoMapping) {
            final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
            if (value != null) {
                foundValues = true;
            if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
                //Because the return value type is a BeanWapper,Set the value to by reflection JavaBean Yes.
                metaObject.setValue(mapping.property, value);
    return foundValues;
  • obtain
The focus of the above code is to obtain the type processor of the corresponding field, call the getResult method of the corresponding type processor, and get the data value from the ResultSet.
//type yes Java Type of field jdbcType Is the name of the database column JDBC type
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
    //Get from all processors first Java Type of processor
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
    TypeHandler<?> handler = null;
    if (jdbcHandlerMap != null) {
        //Again according to JDBC Gets the actual processor of the type
        handler = jdbcHandlerMap.get(jdbcType);
    return (TypeHandler<T>) handler;
Take ID as an example. In Java, it is a String type, in the database, it is VARCHAR, and the last returned type processor is StringTypeHandler. When called, it's very simple.
return rs.getString(columnName); 
  • set up
Get the value through rs.getString(), and then set it in the return value type. Because we return a JavaBean, which corresponds to the BeanWapper object. In fact, the method is a reflection call.
public class BeanWrapper extends BaseWrapper {
    private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
        try {
            Invoker method = metaClass.getSetInvoker(prop.getName());
            Object[] params = {value};
            try {
                method.invoke(object, params);
            } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        } catch (Throwable t) {
            throw new ReflectionException("Could not set property '" + prop.getName() 
                            + "' of '" + object.getClass() + "' with value '"
                            + value + "' Cause: " + t.toString(), t); } } } 
All columns are parsed and the specified Bean is returned. Finally, it is added to the list, and the whole method returns. In the selectOne method, get the first data of the list. If the data record is greater than 1, there is an error.
public class DefaultSqlSession implements SqlSession {
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.<T>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;

6, Summary

The whole process of Mybatis execution method is briefly summarized.
  • Get SqlSession and call different methods according to the return value type of the method. Like selectOne.
  • Get the BoundSql object and generate SQL statements according to the passed parameters
  • Get the Connection object from the database Connection pool and create a proxy for it to print logs
  • Get the PreparedStatement precompiled object from the Connection and create a proxy for it
  • Precompile SQL and set parameters
  • Execute and return data sets
  • Converting datasets to Java objects
After seeing this, recall the steps of the JDBC instance at the beginning. You can see that the main processes between them are the same. Mybatis just made some encapsulation on this basis to better serve our applications.

Posted by Arkane on Sun, 05 Dec 2021 07:12:11 -0800