Take you by hand to read the Mybatis Source Execution

Keywords: Java SQL Mybatis Database xml

Preface

As mentioned in the previous article How MyBatis builds configuration classes Yes, I also said that MyBatis is mainly divided into two phases in the running process, the first is building, the second is executing, so this article will take you to see how MyBatis goes from building to executing our first SQL statement.This part of the content will then be placed in the Public Number menu bar: serialized...-In framework analysis, welcome to discuss!

Entry (generation of proxy objects)

public static void main(String[] args) throws Exception {
    /******************************Construction**********************************/
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //Establish SqlSessionFacory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    /******************************Split Line********************************/
    /******************************Execution**********************************/
    //SqlSession Is the object that helps us operate the database
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //Obtain Mapper
    DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
    Map<String,Object> map = new HashMap<>();
    map.put("id","123");
    System.out.println(mapper.selectAll(map));
    sqlSession.close();
    sqlSession.commit();
  }

 

First of all, before you look at the source code, I want you to recall that when using native MyBatis (without integrating with Spring), all you need to do with SQL is the SqlSession object, which interacts exclusively with the database.

We mentioned in the construction that the Configuration object was parsed by calling the parse method of the XMLConfigBuilder in the build method in SqlSessionFactoryBuilder, but we did not mention where this Configuration ultimately went.Before we talk about this, we can think about what part of the Configuration will be used after it is generated.Undoubtedly, as a configuration file integration, it contains database connection related information, SQL statement related information and so on, which is essential in the whole process of query. As we have just said, SqlSession is actually a real object that we operate the database, so we can draw this conclusion: Configuration must be related to SqlSession.

Source code:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //analysis config.xml(mybatis analysis xml Yes  java dom)     dom4j sax...
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //parse(): analysis config.xml Node inside
      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.
      }
    }
}

public SqlSessionFactory build(Configuration config) {
      //Injection into SqlSessionFactory
    return new DefaultSqlSessionFactory(config);
}
public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
}

 

In fact, as you can see from the source code, Configuration is a property of SqlSessionFactory, and SqlSessionFactoryBuilder in the build method actually calls XMLConfigBuilder to parse the xml file and inject it into SqlSessionFactory.

 

 

Having made that clear, we'll look down.

From the main line we now get a SqlSessionFactory object, the next step is to get the SqlSession object, where the SqlSessionFactory.openSession() method is called to get it, and openSession is actually a further processing encapsulation of SqlSession, including adding transactions, executors, and so on.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        //Yes SqlSession Further processing and encapsulation of objects
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
        //structure SqlSession object
      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();
    }
  }

 

The small conclusion here is that since there is a Configuration object in the SqlSessionFactory object, it holds global configuration information, initializes the environment, and DataSource, which is used to create links. When we call the openSession method, a connection object is opened, passed to the SqlSession object, and given to the SqlSession for data.Library to do related operations.

Next, we now get a SqlSession object and the execution starts here.

We can start to recall that normally when we use MyBatis, the DAO layer we write should look like this:

public interface DemoMapper {
  public List<Map<String,Object>>  selectAll(Map<String,Object> map);
}

 

It is actually an interface and does not implement a class, but we can call it directly, as follows:

 DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
 Map<String,Object> map = new HashMap();
 map.put("id","123");
 System.out.println(mapper.selectAll(map));

 

As you can guess, the bottom layer of MyBatis must have used a dynamic proxy to proxy this interface. We are actually calling the proxy object that MyBatis generated for us.

When we get a Mapper, we need to call the SqlSession getMapper() method, so go deeper from there.

//getMapper The method will eventually be called here, and this is MapperRegistry Of getMapper Method
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      //MapperProxyFactory  Generate a when parsing map  map There will be our DemoMapper Of Class
    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);
    }
}

 

You can see here that the mapperProxyFactory object takes a value from an object called knownMappers with type as the key. This knownMappers is a HashMap that stores our DemoMapper object, where type is the Mapper interface we wrote above.Then someone will ask, when was this knownMappers generated?In fact, this is one of the things I missed out in my last article. When parsing, I call the parse() method, and I'm sure everyone remembers that there is a bindMapperForNamespace method inside this method that helps us build knownMappers and put our Mapper interface in.

public void parse() {
      //Determine if the file has been parsed before
    if (!configuration.isResourceLoaded(resource)) {
        //analysis mapper file
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //Here: Binding Namespace Inside Class object*
      bindMapperForNamespace();
    }

    //Nodes that could not be resolved before resolving
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          configuration.addLoadedResource("namespace:" + namespace);
            //This will be the interface class afferent
          configuration.addMapper(boundType);
        }
      }
    }
  }
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 {
          //Interface information here put enter konwMappers. 
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
}

 

So after getMapper, we get a Class, and then the code is simple to generate a standard proxy class and call the newInstance() method.

public T newInstance(SqlSession sqlSession) {
    //This will be called first newInstance Method
    //Dynamic proxy logic in MapperProxy inside
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    //Call the following from here newInstance Method
    return newInstance(mapperProxy);
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
     //jdk Incoming Dynamic Proxy
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

 

At this point, the creation of the proxy object (MapperProxy) is complete, and it is clear that the bottom level of MyBatis is to instantiate our interface proxy classes to manipulate the database.

But we seem to get an empty object, the logic to call the method?I don't seem to see it at all, so this is where Java is tested.

We know that if a class is to be called a proxy object, it must implement the InvocationHandler interface, implement its invoke method, make a wave of speculation, and the logic must be in the invoke method.

So you can click into the MapperProxy class and find that it does implement the InvocationHandler interface. Here I delete some unused code first, leaving only useful code for analysis

/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class MapperProxy<T> implements InvocationHandler, Serializable {

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
      //structure
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //This is a very standard JDK Dynamic proxy
    //Called when executed invoke Method
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
          //Class to which the judgment method belongs
          //Is it called Object Default Method
          //If not, do not change the behavior of the original method
        return method.invoke(this, args);
      } else if (method.isDefault()) {
          //Handling of default methods
          //Determine whether it is default Method, which is the default method defined in the interface.
          //If it is the default method in the interface, bind the method to the proxy object and call it.
          //Not detailed here
        if (privateLookupInMethod == null) {
          return invokeDefaultMethodJava8(proxy, method, args);
        } else {
          return invokeDefaultMethodJava9(proxy, method, args);
        }
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //If it's not the default method, then actually start execution MyBatis Proxy logic.
    //Obtain MapperMethod Proxy Object
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //implement
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
      //Dynamic proxies have caches, computeIfAbsent If there is one in the cache, take it directly from the cache
      //If not in the cache, new One and put it in the cache
      //Because dynamic proxies are resource intensive
    return methodCache.computeIfAbsent(method,
        k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }
}

 

Before the method starts proxying, it first decides whether the method of the Object class has been called. If so, MyBatis does not change its behavior and returns directly. If it is the default method, it binds to the proxy object and then invokes it (not the focus of this article). If not, it is the mapper interface method that we defined, and then execution begins.

Execution methods require a MapperMethod object, which is used by MyBatis execution method logic. The way MyBatis obtains the MapperMethod object here is to first go to the method cache to see if it already exists, or if it does not, to new one and save it in the cache, because creating a proxy object is a resource-intensive operation.All in all, you get a MapperMethod object here, and you really execute the logic through the excute() method of MapperMethod.

Execution logic

Sentence Type Judgment

Here we will first determine the type of SQL: SELECT|DELETE|UPDATE|INSERT. Our example here is SELECT. The others are all similar. Interested students can see it for themselves.Once you have determined that the SQL type is SELECT, you can begin to determine the return value type and do different things depending on the situation.Then start getting parameters--"Execute SQL.

  //execute() Here's the real execution SQL Places
  public Object execute(SqlSession sqlSession, Object[] args) {
      //Decide which one SQL Sentence
      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:
            //Our example is a query

            //Determine whether there is a return value
          if (method.returnsVoid() && method.hasResultHandler()) {
              //No return value
            executeWithResultHandler(sqlSession, args);
            result = null;
          } else if (method.returnsMany()) {
              //Return value multiline This method is called here
            result = executeForMany(sqlSession, args);
          } else if (method.returnsMap()) {
              //Return Map
            result = executeForMap(sqlSession, args);
          } else if (method.returnsCursor()) {
              //Return Cursor
            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());
      }
      if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName()
            + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
      }
      return result;
  }

  //Return value multiline This method is called here
  private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
      //Method to execute when returning multiple rows of values
      List<E> result;
      //param Is the parameter we passed in, if it is Map,So this is actually Map object
      Object param = method.convertArgsToSqlCommandParam(args);
      if (method.hasRowBounds()) {
          //If Paging
        RowBounds rowBounds = method.extractRowBounds(args);
          //implement SQL Location
        result = sqlSession.selectList(command.getName(), param, rowBounds);
      } else {
          //Without
          //implement SQL Location
        result = sqlSession.selectList(command.getName(), param);
      }
      // issue #510 Collections & arrays support
      if (!method.getReturnType().isAssignableFrom(result.getClass())) {
        if (method.getReturnType().isArray()) {
          return convertToArray(result);
        } else {
          return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
        }
      }
      return result;
  }

  /**
  *  Method of getting parameter name
  */
  public Object getNamedParams(Object[] args) {
      final int paramCount = names.size();
      if (args == null || paramCount == 0) {
          //If the parameter passed in is empty
        return null;
      } else if (!hasParamAnnotation && paramCount == 1) {
          //If there is no comment on the parameter, for example@Param,If there is only one parameter, the parameter is returned directly
        return args[names.firstKey()];
      } else {
          //If a comment is added to the parameter, or if there are multiple parameters.
            //that MyBatis Encapsulates the parameter to one Map,But be aware that because jdk Why, we can only get the parameter subscript and parameter name, but the parameter name becomes arg0,arg1.
          //So when passing in multiple parameters, it's best to add@Param,Otherwise, assume multiple incoming String,Causes#{} Failure to get value
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            //entry.getValue Is the parameter name
          param.put(entry.getValue(), args[entry.getKey()]);
          //If you pass many String,Can also be used param1,param2.. . 
          // add generic param names (param1, param2, ...)
          final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
          // ensure not to overwrite parameter named with @Param
          if (!names.containsValue(genericParamName)) {
            param.put(genericParamName, args[entry.getKey()]);
          }
          i++;
        }
        return param;
      }
    }

 

SQL Execution (Level 2 Cache)

The core method to execute SQL is selectList, and even if selectOne, the bottom level actually calls the selectList method and takes the first one.

 @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      try {
        //MappedStatement:analysis XML Resolving an object SQL  Will be encapsulated into MappedStatement,It holds all our executions SQL Information Required
        MappedStatement ms = configuration.getMappedStatement(statement);
        //query,adopt executor
        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();
      }
  }

 

Here we also see the MappedStatement object mentioned in the previous construction, which is generated by parsing the Mapper.xml configuration to store SQL information. Executing SQL requires the information about SQL stored in this object. Executor object is called inside selectList to execute SQL statement. This object is one of the four objects of MyBatis and will be said later.

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
      //Obtain sql Sentence
      BoundSql boundSql = ms.getBoundSql(parameterObject);
      //Generate a cached key  
      //Here is-1181735286:4652640444:com.DemoMapper.selectAll:0:2147483647:select * from test WHERE id =?:2121:development
      CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
      return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  //Secondary Cache Query
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        //Secondary Cached Cache
      Cache cache = ms.getCache();
      if (cache != null) {
        //If Cache Enter if not empty
        //Refresh the cache if needed (some caches are refreshed regularly and need to be used)
        flushCacheIfRequired(ms);
        //If this statement Cache is used (the scope of the secondary cache is namespace,You can also think of it here ms)
        if (ms.isUseCache() && resultHandler == null) {
          ensureNoOutParams(ms, boundSql);
          @SuppressWarnings("unchecked")
          //Get it from the cache first
          List<E> list = (List<E>) tcm.getObject(cache, key);
          if (list == null) {
              //Query the database if the cached data equals null
            list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            //Put data into secondary cache after query
            tcm.putObject(cache, key, list); // issue #578 and #116
          }
          //Return
          return list;
        }
      }
      //If cache There is no such thing, so query the first level cache directly
      return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

 

When MyBatis queries, it does not directly query the database, but instead queries with a secondary cache. Since the scope of the secondary cache is namespace, which can also be interpreted as a mapper, it will also be determined if the mapper has a secondary cache turned on or if it has not, it will go to the first level cache to continue the query.

SQL Query (Level 1 Cache)

  //Level 1 Cache Query
  @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 {
         //Query Stack+1
        queryStack++;
        //Level 1 Cache
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            //Processing with output resources for stored procedures
          handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            //If the cache is empty, take it from the database
          list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
      } finally {
         //Query Stack-1
        queryStack--;
      }
      if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
          deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
          // issue #482
          clearLocalCache();
        }
      }
      //Result Return
      return list;
  }

 

If the first level cache finds the result, it returns the result directly. If the first level cache does not find the result, it will eventually enter the database for query.

SQL Execution (Database Query)

  //Database Query
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      List<E> list;
      //In First Level Cache put A placeholder
      localCache.putObject(key, EXECUTION_PLACEHOLDER);
      try {
        //call doQuery Method Query Database
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
      } finally {
        localCache.removeObject(key);
      }
      //To Cache put Real Data
      localCache.putObject(key, list);
      if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
      }
      return list;
  }
  //Real Database Query
  @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();
        //Encapsulation, StatementHandler Also MyBatis One of the Four Objects
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        //#The SQL of {} ->? Is initialized here
        stmt = prepareStatement(handler, ms.getStatementLog());
        //The real query will not occur until the parameters have been assigned.
        return handler.query(stmt, resultHandler);
      } finally {
        closeStatement(stmt);
      }
  }

 

Before a true database query, our statement would be the same: select * from test where id =?, so first replace the placeholder with the real parameter value, so the parameter assignment will proceed.

parameter assignment

Since the underlying encapsulation of MyBatis is java's most basic jdbc, assignment must also be the putString() method that calls jdbc.

   /********************************Parameter assignment section*******************************************/
    //Because yes#{}, so prepareStatement is used, precompiled SQL
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        //Get Connected Objects
        Connection connection = getConnection(statementLog);
        //Initialization prepareStatement
        stmt = handler.prepare(connection, transaction.getTimeout());
        //Acquired PrepareStatement After that, here you go#{} Assignment
        handler.parameterize(stmt);
        return stmt;
    }

    /**
    * Pre-compile SQL for put values
    */
    @Override
    public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        //parameter list
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              //Get it xml in#{}   The name of the parameter, for example #{id}  propertyName==id
              String propertyName = parameterMapping.getProperty();
              if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } else {
                  //metaObject Stores the corresponding relationship between parameter name and value
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              JdbcType jdbcType = parameterMapping.getJdbcType();
              if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
              }
              try {
                //Here for preparedStatement Assignment, by typeHandler,setParameter Ultimately, a call is made to something called setNonNullParameter Method.The code is pasted below.
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
              } catch (TypeException | SQLException e) {
                throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
              }
            }
          }
        }
    }
    //jdbc assignment
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
          throws SQLException {
        //This is the most native jdbc Assignment of
        ps.setString(i, parameter);
    }
    /********************************Parameter assignment section*******************************************/

 

Formal implementation

When the parameter assignment is complete, SQL can be executed, and the code above shows that when the parameter assignment is complete, the database query is made directly through the hanler.query() method.

     @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        //adopt jdbc Make a database query.
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        //Processing result set resultSetHandler Also MyBatis One of the four objects
        return resultSetHandler.handleResultSets(ps);
    }

 

It's obvious that this is actually a call to the native jdbc we're familiar with to query the database.

Executive Summary

At this point, the execution phase of MyBatis has done two things from a macro perspective:

  1. Proxy object generation

  2. Execution of SQL

While the execution of SQL takes a lot of space to analyze, although it is based on the main line of a query statement, it must look confusing, so here I will talk about a flowchart to help you understand:

 

 

Result Set Processing

During the SQL execution phase, MyBatis has finished querying the data, so there is one last problem: result set processing, in other words, encapsulating the result set as an object.When we don't use frames, we use loops to get result sets, and then we get them one by one through the getXXXX() method, which is too labor intensive to see how MyBatis works.

  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
      ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
      //resultMap Multiple values can be specified through multiple tags, so there are multiple result sets
      final List<Object> multipleResults = new ArrayList<>();

      int resultSetCount = 0;
      //Get the current first result set
      ResultSetWrapper rsw = getFirstResultSet(stmt);

      //Get All resultMap
      List<ResultMap> resultMaps = mappedStatement.getResultMaps();
      //resultMap Quantity
      int resultMapCount = resultMaps.size();
      validateResultMapsCount(rsw, resultMapCount);
      //Loop through each result set
      while (rsw != null && resultMapCount > resultSetCount) {
          //Start encapsulating result set list.get(index) Get result set
        ResultMap resultMap = resultMaps.get(resultSetCount);
        //afferent resultMap Processing result set rsw Current Result Set (Main Line)
        handleResultSet(rsw, resultMap, multipleResults, null);
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }

      String[] resultSets = mappedStatement.getResultSets();
      if (resultSets != null) {
        while (rsw != null && resultSetCount < resultSets.length) {
          ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
          if (parentMapping != null) {
            String nestedResultMapId = parentMapping.getNestedResultMapId();
            ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
            handleResultSet(rsw, resultMap, null, parentMapping);
          }
          rsw = getNextResultSet(stmt);
          cleanUpAfterHandlingResultSet();
          resultSetCount++;
        }
      }
      //If there is only one result set, take the first from the multiple result set
      return collapseSingleResultList(multipleResults);
  }
  //Processing result set
  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
     //Processing result set
        try {
        if (parentMapping != null) {
          handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
          if (resultHandler == null) {
            //judge resultHandler Whether it is empty or not, if a default is created for empty.
            //Result Set Processor
            DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
            //Processing row data
            handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
            multipleResults.add(defaultResultHandler.getResultList());
          } else {
            handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
          }
        }
      } finally {
        // issue #228 (close resultsets)
        //Close Result Set
        closeResultSet(rsw.getResultSet());
      }
  }

 

The code above creates an object that handles the result set and eventually calls the handleRwoValues() method to process the row data.

  //Processing row data
  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
      //Is there an embedded result set
      if (resultMap.hasNestedResultMaps()) {
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
      } else {
          //No embedded result set exists
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
      }
  }
  //Called when no result set is embedded
  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
        throws SQLException {
      DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
      //Get the current result set
      ResultSet resultSet = rsw.getResultSet();
      skipRows(resultSet, rowBounds);
      while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
          //Traversing result set
        ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
        //Get row data, pack row data into one Object
        Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
        storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
      }
  }

 

The code here essentially encapsulates the result set of each row directly into an Object object, so the key is how to make the row data an Object object using the getRowValue() method.

 private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
      //Create an empty Map Storage value
      final ResultLoaderMap lazyLoader = new ResultLoaderMap();
      //Create an empty object to load row data
      Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
      if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())){
          //Return value by reflection operation
          //here metaObject.originalObject = rowValue
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        boolean foundValues = this.useConstructorMappings;
        if (shouldApplyAutomaticMappings(resultMap, false)) {
      //Determine if automatic mapping is required, default automatic mapping, or resultMap On Node autoMapping Configure whether to map automatically
            //Here is the operation of automatic mapping.
          foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
      }
      return rowValue;
  }

 

We don't use ResultMap here, so it's automatic mapping (the default), so we go into the applyAutomaticMappings() method, which completes the encapsulation of the object.

   private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
        //Automatic Mapping Parameter List
      List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
        //Whether the column was found
      boolean foundValues = false;
      if (!autoMapping.isEmpty()) {
          //ergodic
        for (UnMappedColumnAutoMapping mapping : autoMapping) {
            //Get value from column name
          final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
          if (value != null) {
              //If the value is not empty, the column was found
            foundValues = true;
          }
          if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
            // gcode issue #377, call setter on nulls (value is not 'found')
              //Assign values here
            metaObject.setValue(mapping.property, value);
          }
        }
      }
      return foundValues;
  }

 

We can see that this method uses metaObject.setValue(mapping.property, value) by traversing the parameter list; the returned object is assigned, but it may be a Map, it may be a custom object. What's the difference?

In fact, all assignments are done internally through an object called ObjectWrapper, so we can go in and see how it differs between Map and custom object assignments, and the problem is solved.

First look at the metaObject.setValue() method of the code above

 public void setValue(String name, Object value) {
      PropertyTokenizer prop = new PropertyTokenizer(name);
      if (prop.hasNext()) {
        MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
        if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
          if (value == null) {
            // don't instantiate child path if value is null
            return;
          } else {
            metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
          }
        }
        metaValue.setValue(prop.getChildren(), value);
      } else {
          //This method will eventually be called objectWrapper.set()Assigning values to results
        objectWrapper.set(prop, value);
      }
  }

 

Let's look at the implementation class of objectWrapper:

 

 

In our example today, DemoMapper returns Map, so objectWrapper calls MapWrapper's set method. If it is a custom type, it calls BeanWrapper's set method. Here's how the set method differs between the two classes:

  //MapWrapper Of set Method
  public void set(PropertyTokenizer prop, Object value) {
      if (prop.getIndex() != null) {
        Object collection = resolveCollection(prop, map);
        setCollectionValue(prop, collection, value);
      } else {
        //It's actually called Map Of put Method puts property name and value into map in
        map.put(prop.getName(), value);
      }
  }

  //BeanWrapper Of set Method
  public void set(PropertyTokenizer prop, Object value) {
      if (prop.getIndex() != null) {
        Object collection = resolveCollection(prop, object);
        setCollectionValue(prop, collection, value);
      } else {
        //Assign values here, call them through reflection assignment setXX()Method Assignment
        setBeanProperty(prop, object, value);
      }
  }
  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);
      }
  }

 

The above two set methods are different implementations of MapWrapper and BeanWrapper. MapWrapper's set method essentially puts the attribute name and value into the map's key and value, while BeanWrapper uses reflection and calls Bean's set method to inject the value.

 

 

epilogue

So far, the execution process of MyBatis has ended. This article mainly talks about how MyBatis executes a query statement after building a configuration object, and how the result set is processed after a first-level query statement, mapped to the data type we defined.

Since it's a source analysis article, it may seem confusing if I just look at it roughly, so I'll provide another flowchart to make it easier to understand.

 

 

Of course, this flowchart does not cover the content of the writeback cache. I will discuss the first level cache and the second level cache related content of MyBatis in the third source code analysis.

Posted by rorobobo on Fri, 21 Feb 2020 19:46:42 -0800