Ten thousand words sorting MyBatis source code

Posted by kykin on Tue, 14 Dec 2021 10:13:17 +0100

MyBatis has been used almost since I first learned programming. I haven't seen its source code. This time, I happened to study it. After reading the source code, I still got a lot of harvest. I specially sorted it out. If there are any problems, please point them out

summary

The orm framework of MyBatis actually encapsulates JDBC. Friends who have used it must know to create a mybatis_config.xml configuration file, create a mapper interface, and create a mapper The XML file is then invoked in the service layer. Let's not analyze the source code for the moment. If you develop such an orm framework with the same functions as MyBatis, there are three problems in front of you

  • How to encapsulate the configuration (database link address, user name, password) and achieve registration only once. You don't need to manage this later
  • How to bind mapper interface and mapper XML file
  • How to generate a proxy object, let the methods in the interface find the corresponding mapper statement, and then bring the parameters in for execution
    With these problems, it is better to learn the source code step by step. Of course, these problems alone cannot be fully developed. Here I will talk about them as much as possible. If there are some unpopular configurations, I may have to study them in depth.

JDBC & native MyBatis call review

Firstly, MyBatis is a layer of encapsulation of traditional jdbc. First, let's review the traditional jdbc

JDBC

public class User {

    //id of user table
    private Integer id;

    //user name
    private String username;
    
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username == null ? null : username.trim();
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username ;
    }

}
public class JDBCDemo {
    //Create a connection to the database
    private static Connection getConnection() {
        Connection connection = null;
        try {
            //Load user driver
            Class.forName("com.mysql.cj.jdbc.Driver");
            //Address to connect to the database
            String url = "jdbc:mysql://127.0.0.1:3306/test1";
            //User name of the database
            String user = "root";
            //Password for database
            String password = "12345678";
            //Get a database connection
            connection = DriverManager.getConnection(url, user, password);
        } catch (ClassNotFoundException e) {
            System.out.println(JDBCDemo.class.getName() + "Database driver package not found!");
            return null;
        } catch (SQLException e) {
            System.out.println(JDBCDemo.class.getName() + "SQL There is a problem with the statement. The query cannot succeed!");
            return null;
        }
        return connection;//Return to the connection
    }

    public User getUser(int id) {
        //Get a connection to the database
        Connection connection = getConnection();
        //Declare a null preprocessed Statement
        PreparedStatement ps = null;
        //Declare a result set to store the results of SQL query
        ResultSet rs = null;
        try {
            //Preprocess and compile the SQL of the User table of the query
            ps = connection.prepareStatement("select * from user where id=?");
            //Set the parameter Id to the condition of the data
            ps.setInt(1, id);
            //Execute the query statement. Returns the result to the ResultSet result set
            rs = ps.executeQuery();
            //Traversal fetching from result set
            while (rs.next()) {
                //Retrieve the user id of the Statement
                int user_id = rs.getInt("id");
                //Get the user name of the Statement
                String username = rs.getString("username");
                User user = new User();
                //Stored in user object
                user.setId(user_id);
                user.setUsername(username);
                return user;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            this.close(rs, ps, connection);
        }
        return null;
    }

    /**
     * Determine whether the database is closed
     * @param rs Check whether the result set is closed
     * @param stmt Close preprocessing SQL
     * @param conn Is the database connection closed
     */
    private void close(ResultSet rs, Statement stmt, Connection conn) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            System.out.println(JDBCDemo.class.getName() + "ResultSet Closing failed!");
        }
        try {
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException e) {
            System.out.println(JDBCDemo.class.getName() + "Statement Closing failed!");
        }
        try {
            if (conn != null) {
                conn.close();
            }
        } catch (SQLException e) {
            System.out.println(JDBCDemo.class.getName() + "Connection Closing failed!");
        }
    }
    
    public static void main(String[] args) {
        //We query the user whose id is 1
        User user = new JDBCDemo().getUser(1);
        //Print out the queried data
        System.out.println(user);

    }

}

Here is a brief introduction to the next three main classes, which will be described later on

  • DriverManager: when the method getConnection is called, DriverManager will try to find the appropriate driver from the driver loaded in initialization and explicitly load the driver using the same class loader as the current applet or application.
  • Connection: connection to the database. Execute the SQL statement and return the result in the context of the connection.
  • Statement: an object used to execute a static SQL statement and return the results it generates.
  • ResultSet: represents the database result set

    Native MyBatis call

    After a general understanding, we can look at the writing method of native MyBatis
    mybatis_config

    <?xml version="1.0" encoding="UTF-8" ?>
    <!--
    
         Copyright 2009-2017 the original author or authors.
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
            http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    
    -->
    <!DOCTYPE configuration
          PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
          "http://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
    
      <!-- autoMappingBehavior should be set in each test case -->
    
      <environments default="development">
          <environment id="development">
             <!--Configure transaction manager-->
              <transactionManager type="JDBC" />
              <!--Configure the data source type and database link information-->
              <dataSource type="UNPOOLED">
                  <property name="driver" value="org.hsqldb.jdbcDriver"/>
                  <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
                  <property name="username" value="root"/>
                  <property name="password" value="12345678"/>
    
              </dataSource>
          </environment>
      </environments>
      <!--mapper File location configuration-->
      <mappers>
          <mapper resource="org/apache/ibatis/autoconstructor/AutoConstructorMapper.xml"/>
      </mappers>
    
    </configuration>

    mapper

    public interface AutoConstructorMapper {
    PrimitiveSubject getSubject(final int id);
    
    @Select("SELECT * FROM subject")
    List<PrimitiveSubject> getSubjects();
    
    @Select("SELECT * FROM subject")
    List<AnnotatedSubject> getAnnotatedSubjects();
    
    @Select("SELECT * FROM subject")
    List<BadSubject> getBadSubjects();
    
    @Select("SELECT * FROM extensive_subject")
    List<ExtensiveSubject> getExtensiveSubjects();
    }

    xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    
         Copyright 2009-2017 the original author or authors.
    
         Licensed under the Apache License, Version 2.0 (the "License");
         you may not use this file except in compliance with the License.
         You may obtain a copy of the License at
    
            http://www.apache.org/licenses/LICENSE-2.0
    
         Unless required by applicable law or agreed to in writing, software
         distributed under the License is distributed on an "AS IS" BASIS,
         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
         See the License for the specific language governing permissions and
         limitations under the License.
    
    -->
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
    <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
      SELECT * FROM subject WHERE id = #{id}
    </select>
    </mapper>

    use

    private static SqlSessionFactory sqlSessionFactory;
    
    @BeforeAll
    static void setUp() throws Exception {
    // create a SqlSessionFactory
    try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/autoconstructor/mybatis-config.xml")) {
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    }
    }
    
    @Test
    void fullyPopulatedSubject() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      final Object subject = mapper.getSubject(1);
      assertNotNull(subject);
    }
    }

Parsing mybatis_config.xml

First, we come to the first question, how does MyBatis parse mybatis_config.xml

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      reader.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

Mybatis core class Configuration is created through the XMLConfigBuilder::parse method. Mybatis loads all configurations during initialization, and basically saves the results into Configuration. It only creates one during initialization, which contains mybatis config All configurations in the XML, including all parsed mapper s and their mapping relationships, can be found here after loading. It is the core class in mybatis. The member variables are as follows. You can see a lot of mybatis by name_ The figure of config. (the member variables are as follows. You can first feel the mybatis source code without almost no comments in advance, and then feel the pain of watching the source code QAQ)

public class Configuration {
    protected Environment environment;
    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls;
    protected boolean useActualParamName = true;
    protected boolean returnInstanceForEmptyRow;
    protected boolean shrinkWhitespacesInSql;

    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected Class<? extends VFS> vfsImpl;
    protected Class<?> defaultSqlProviderType;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
    protected Integer defaultStatementTimeout;
    protected Integer defaultFetchSize;
    protected ResultSetType defaultResultSetType;
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
    protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

    protected Properties variables = new Properties();
    protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

    protected boolean lazyLoadingEnabled = false;
    protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

    protected String databaseId;
    /**
     * Configuration factory class.
     * Used to create Configuration for loading deserialized unread properties.
     *
     * @see <a href='https://github.com/mybatis/old-google-code-issues/issues/300'>Issue 300 (google code)</a>
     */
    protected Class<?> configurationFactory;

    protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
    //Aliases for common classes
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
        .conflictMessageProducer((savedValue, targetValue) ->
            ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
    protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
    protected final Set<String> loadedResources = new HashSet<>();
    protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

    /*
     * A map holds cache-ref relationship. The key is the namespace that
     * references a cache bound to another namespace and the value is the
     * namespace which the actual cache is bound to.
     */
    protected final Map<String, String> cacheRefMap = new HashMap<>();
... Method skimming
}

XPathParser usage

Let's take a look at the construction method of XMLConfigBuilder

public class XMLConfigBuilder extends BaseBuilder {
    private boolean parsed;//Has mybatis config. Been resolved xml
    //Parsing mybatis config xml
    private final XPathParser parser;
    private String environment;
    //Create and cache Reflctor objects
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    public XMLConfigBuilder(Reader reader, String environment, Properties props) {
      this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    }
}

Its interior will encapsulate an XPathParser object. Let's take a look at its usage first

<employee id="${id_var}">
  <blah something="that"/>
  <first_name>Jim</first_name>
  <last_name>Smith</last_name>
  <birth_date>
    <year>1970</year>
    <month>6</month>
    <day>15</day>
  </birth_date>
  <height units="ft">5.8</height>
  <weight units="lbs">200</weight>
  <active bot="YES" score="3.2">true</active>
</employee>
  @Test
  void constructorWithInputStreamValidationVariablesEntityResolver() throws Exception {

    try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
      XPathParser parser = new XPathParser(inputStream, false, null, null);
      System.out.println(parser.evalLong("/employee/birth_date/year").equals(1970L));//true
      System.out.println(parser.evalNode("/employee/birth_date/year").getLongBody().equals(1970L));//true
      System.out.println(parser.evalNode("/employee").evalString("@id"));//${id_var}
      System.out.println(parser.evalNode("/employee/active").getDoubleAttribute("score"));//3.2    }
  }

XPathParser encapsulates the JDK's native Document, EntityResolver, XPath and Properties. It is more convenient to parse XML files. I wrote an example above for specific usage, so that all values can be obtained.

Node resolution

After initializing XMLConfigBuilder, it will call its parse() method. Parsing xml, mapper parsing and mapper binding are all completed in this method. Let's look at this method

/**
 * Parsing mybatis config xml
 * Initialization call
 * @return
 */
public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  //Find configuration node resolution
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

parser. The step of evalnode ("/ configuration") is to obtain the configuration node, and the next step is to resolve each child node in the parseConfiguration method

private void parseConfiguration(XNode root) {
  try {
    //Resolve each node
    // issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    //Class alias registration
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    //Parsing typeHandler
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

It should be familiar to see here, mybatis_config.xml nodes placed in + Element are the corresponding parsing methods. Since we only configured environments and mappers, let's take a look at these two methods.

Parsing Environment

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        //Create datasource and datasourceFactory and set corresponding property values
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
        break;
      }
    }
  }
}

Transaction

Here, TransactionFactory is responsible for creating Transaction. There are two subclasses of Transaction in mybatis


Transaction defines

public interface Transaction {

  /**
   * Get the corresponding database connection object
   */
  Connection getConnection() throws SQLException;

  /**
   * Commit transaction
   */
  void commit() throws SQLException;

  /**
   * Rollback transaction
   */
  void rollback() throws SQLException;

  /**
   * Close database connection
   */
  void close() throws SQLException;

  /**
   * Get transaction timeout
   */
  Integer getTimeout() throws SQLException;

}

JdbcTransaction encapsulates the transaction isolation level, connection and data source. The method is basically to call the corresponding method of connection

public class JdbcTransaction implements Transaction {

  private static final Log log = LogFactory.getLog(JdbcTransaction.class);

     //Database connection corresponding to transaction
     protected Connection connection;
     //datasource to which the database connection belongs
     protected DataSource dataSource;
     //Transaction isolation level
     protected TransactionIsolationLevel level;
     //Auto submit
     protected boolean autoCommit;
     @Override
     public void rollback() throws SQLException {
       if (connection != null && !connection.getAutoCommit()) {
         if (log.isDebugEnabled()) {
           log.debug("Rolling back JDBC Connection [" + connection + "]");
         }
         connection.rollback();
       }
     }
     ..Other strategies
   }

Another is ManagedTransaction, which gives the commit and rollback to the container implementation

public class ManagedTransaction implements Transaction {
       ...Other strategies
    //Container implementation
    @Override
    public void commit() throws SQLException {
      // Does nothing
    }

    @Override
    public void rollback() throws SQLException {
      // Does nothing
    }

}

DataSource

After creating the TransactionFactory, it's the turn of the DataSourceFactory. There's nothing to say. A bunch of class aliases will be registered in the Configuration constructor, and then created through reflection. There's nothing to say.

public Configuration() {
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
. . . Other strategies
}

Let's mainly talk about DataSource, that is, data source, which is actually the encapsulation of driver. Let's take a look at how MyBatis is encapsulated.

The core method of UnpooledDataSource is as follows. Is it very friendly? In fact, it is an encapsulation of traditional JDBC. It's not much different from the example written above. The main difference is that multiple can be supported and encapsulated.

/**
 * The getConnection() method and its overloaded method are implemented
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class UnpooledDataSource implements DataSource {

  //Class loader to load Driver class
  private ClassLoader driverClassLoader;
  //Related configuration of database connection driver
  private Properties driverProperties;
  //Cache all registered database connection drivers
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();

  //The name of the driver for the database connection
  private String driver;
  //url of the database connection
  private String url;
  //user name
  private String username;
  //password
  private String password;
  //Auto submit
  private Boolean autoCommit;
  //Transaction isolation level
  private Integer defaultTransactionIsolationLevel;
  private Integer defaultNetworkTimeout;

  static {
    //Register JDBC drivers with DriverManager
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return doGetConnection(username, password);
  }

  private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (driverProperties != null) {
      props.putAll(driverProperties);
    }
    if (username != null) {
      props.setProperty("user", username);
    }
    if (password != null) {
      props.setProperty("password", password);
    }
    return doGetConnection(props);
  }

  //Create a new connection at a time
  private Connection doGetConnection(Properties properties) throws SQLException {
    //Initialize database driver
    initializeDriver();
    //Create a real database connection
    Connection connection = DriverManager.getConnection(url, properties);
    //Configure autoCommit and isolation levels for database connections
    configureConnection(connection);
    return connection;
  }

  private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {//Check whether the driver is registered
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);//Register driver
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();//Create Driver object
        //Register the Driver. DriverProxy is an internal class defined in UnpooledDataSource and a static proxy class of the Driver
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        //Add drivers to registeredDrivers
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

It should be clear to see the whole line here. Let's take a look at the tradition

  private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      //Set transaction isolation level
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }

  private static class DriverProxy implements Driver {
    private Driver driver;

    DriverProxy(Driver d) {
      this.driver = d;
    }

    . . . slightly
  }
  . . . slightly

}

You think it's over here, far from it. When I look at the code, I also see a PooledDataSource. As we all know, each time I use it, I create a Connection. It's wasteful to use it up and destroy it. It's best to take it. Of course, there is a better implementation, such as Druid. I think it's good to learn from Mybatis. If you are interested, you can understand it. If you are not interested, you can not see it (you can directly see the Druid source code).

Let's take a look at PoolState. The author maintains the Connection object with two list s.

public class PoolState {

  //Data source object
  protected PooledDataSource dataSource;
  //Idle state connection collection
  protected final List<PooledConnection> idleConnections = new ArrayList<>();
  //Active state connection set
  protected final List<PooledConnection> activeConnections = new ArrayList<>();
  protected long requestCount = 0;//Number of database connections requested
  protected long accumulatedRequestTime = 0;//Cumulative time of connection
  protected long accumulatedCheckoutTime = 0;//Connection cumulative checkoutTime duration
  protected long claimedOverdueConnectionCount = 0;//Number of timeout connections
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0;//Cumulative timeout
  protected long accumulatedWaitTime = 0;//Cumulative waiting time
  protected long hadToWaitCount = 0;//Waiting times
  protected long badConnectionCount = 0;//Invalid number of connections
}

Now let's take a look at how MyBatis implements PooledDataSource. The core is to get and put back the Connection. Personally, I think the logic of thread pool is similar.

public class PooledDataSource implements DataSource {
protected void pushConnection(PooledConnection conn) throws SQLException {

  synchronized (state) {
    state.activeConnections.remove(conn);//
    if (conn.isValid()) {//Is the connection valid
      //Check whether the number of idle connections has reached the online limit
      if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        state.accumulatedCheckoutTime += conn.getCheckoutTime();//Cumulative checkout duration
        if (!conn.getRealConnection().getAutoCommit()) {//Rollback uncommitted transactions
          conn.getRealConnection().rollback();
        }
        //Create PooledConnection
        //The proxy object is actually eliminated, and the realConnection is actually used
        PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
        state.idleConnections.add(newConn);//Add to inactive collection
        newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
        newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
        conn.invalidate();//Set the original PooledConnection object to invalid
        if (log.isDebugEnabled()) {
          log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
        }
        state.notifyAll();
      } else {
        //The free collection is full. Close it directly
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        conn.getRealConnection().close();
        if (log.isDebugEnabled()) {
          log.debug("Closed connection " + conn.getRealHashCode() + ".");
        }
        conn.invalidate();
      }
    } else {
      if (log.isDebugEnabled()) {
        log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
      }
      state.badConnectionCount++;
    }
  }
}

private PooledConnection popConnection(String username, String password) throws SQLException {
  boolean countedWait = false;
  PooledConnection conn = null;
  long t = System.currentTimeMillis();
  int localBadConnectionCount = 0;

  //Spin without getting the connection object
  while (conn == null) {
    synchronized (state) {
      /**
       * Processing whether there are idle connections
       */
      //idleConnections idle state connections
      if (!state.idleConnections.isEmpty()) {//idle connection 
        // Pool has available connection
        conn = state.idleConnections.remove(0);//Get connection
        if (log.isDebugEnabled()) {
          log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
        }
      } else {
        // Pool does not have available connection if the number of active connections does not reach the maximum, a new connection can be created
        if (state.activeConnections.size() < poolMaximumActiveConnections) {
          // Can create new connection creates a new database connection and encapsulates it as a PooledConnection object
          conn = new PooledConnection(dataSource.getConnection(), this);
          if (log.isDebugEnabled()) {
            log.debug("Created connection " + conn.getRealHashCode() + ".");
          }
        } else {//If the number of active connections has reached the maximum, you cannot create a new connection
          // Cannot create new connection gets the first active connection created
          PooledConnection oldestActiveConnection = state.activeConnections.get(0);
          long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
          if (longestCheckoutTime > poolMaximumCheckoutTime) {//Check whether the connection timed out
            // Can claim excess connection statistics the information of timeout connections
            state.claimedOverdueConnectionCount++;
            state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
            state.accumulatedCheckoutTime += longestCheckoutTime;
            //Move timed out connections out of the activeConnections collection
            state.activeConnections.remove(oldestActiveConnection);
            //If the timeout connection is not submitted, it will be rolled back automatically
            if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
              try {
                oldestActiveConnection.getRealConnection().rollback();
              } catch (SQLException e) {
                /*
                   Just log a message for debug and continue to execute the following
                   statement like nothing happened.
                   Wrap the bad connection with a new PooledConnection, this will help
                   to not interrupt current executing thread and give current thread a
                   chance to join the next competition for another valid/good database
                   connection. At the end of this loop, bad {@link @conn} will be set as null.
                 */
                log.debug("Bad connection. Could not roll back");
              }
            }
            //Create a new PooledConnection object and reuse the old Collection object
            conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
            //Multiplex timestamp
            conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
            conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
            //Timeout PooledConnection set to invalid
            oldestActiveConnection.invalidate();
            if (log.isDebugEnabled()) {
              log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // If there are no idle connections, no new connections can be created, and no timeout connections, you can only block the wait
            try {
              if (!countedWait) {
                state.hadToWaitCount++;//Count waiting times
                countedWait = true;
              }
              if (log.isDebugEnabled()) {
                log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
              }
              long wt = System.currentTimeMillis();
              state.wait(poolTimeToWait);
              state.accumulatedWaitTime += System.currentTimeMillis() - wt;
            } catch (InterruptedException e) {
              break;
            }
          }
        }
      }
      if (conn != null) {
        // ping to server and check the connection is valid or not
        if (conn.isValid()) {//Detect that the connection is valid
          //
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
          conn.setCheckoutTimestamp(System.currentTimeMillis());
          conn.setLastUsedTimestamp(System.currentTimeMillis());
          state.activeConnections.add(conn);//Statistics
          state.requestCount++;
          state.accumulatedRequestTime += System.currentTimeMillis() - t;
        } else {
          //The current connection is invalid. Continue to choose from
          if (log.isDebugEnabled()) {
            log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
          }
          state.badConnectionCount++;
          localBadConnectionCount++;
          conn = null;
          if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
            if (log.isDebugEnabled()) {
              log.debug("PooledDataSource: Could not get a good connection to the database.");
            }
            throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
          }
        }
      }
    }

  }

  if (conn == null) {
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }
    throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
  }

  return conn;
}
. . . slightly
}

The following is to encapsulate the TransactionFactory and DataSource into the environment, and then insert the environment into the Configuration. The builder mode is used. If you are interested in the builder mode, you can see my blog https://juejin.cn/post/698594...

Parse Mapper


There are about four types of mapper configurations. It's easy to understand from the source code. The contents are similar. Let's take a look at the analysis of resource

public void parse() {
  //Determine whether the mapping file has been loaded
  if (!configuration.isResourceLoaded(resource)) {
    //Bind sql via xml
    configurationElement(parser.evalNode("/mapper"));//Processing mapper nodes
    //The parsed xml is added to loadedResources
    configuration.addLoadedResource(resource);
    //Scan annotation binding sql
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      //package is processed separately
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        //Resolve corresponding configuration
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try(InputStream inputStream = Resources.getUrlAsStream(url)){
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          }
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

Due to mybatis_config specifies the path of mapper, and the specified resource, mapperparser, will be loaded parse(); Finally, the following method will be called to parse the select tag and add the parsed results to configuration::mappedStatements

public void parseStatementNode() {
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // Process the include node first
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  // Parse selectKey after includes and remove them.
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    //According to the useGeneratedKeys configuration in the global
    //Whether it is an insert statement determines whether it is implemented using the KeyGenerator interface
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }
  //Parse the original sql statement and replace #{} with?, Put parameters
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  //Gets the value of resultType in the select tag
  String resultType = context.getStringAttribute("resultType");
  //If there is no corresponding class object in typeAliasRegistry, get the corresponding class object
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");
  //Generate mappedStatements and add them to the mappedStatements of the configuration
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

I believe you can also see the annotation version. It is in
The annotation is through MapperRegistry::addMapper. The specific process is similar to the above process. It is also used to parse the values in the above methods`
MapperBuilderAssistant
`Add to configuration
Because all the parsing results are stuffed into the configuration. Finally, the configuration is stuffed into the configuration`
DefaultSqlSessionFactory
`
The parsing part is finished

Get SqlSession

Here is how to get SqlSession. Mybatis gives you DefaultSqlSession by default.

//Get database connection from data source
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    //Get the transactionFactory in the environment and create one without
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    //Execute sql statements
    final Executor executor = configuration.newExecutor(tx, execType);
    //Create sqlsession
    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();
  }
}

When you get the mapper, a proxy object is generated for you through dynamic proxy`
final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
`The source code is as follows

@Override
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}


public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}


//Mapper interface proxy object
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 newInstance(SqlSession sqlSession) {
  //Create proxy object
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}


@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

mapper method execution

When executing, we just need to see how the agent executes, that is`
MapperProxy
It internally encapsulates a PlainMethodInvoker `, and the final execution is to call the invoke method of this internal class

private static class PlainMethodInvoker implements MapperMethodInvoker {
      private final MapperMethod mapperMethod;

      public PlainMethodInvoker(MapperMethod mapperMethod) {
        super();
        this.mapperMethod = mapperMethod;
      }

      @Override
      public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
        //
        return mapperMethod.execute(sqlSession, args);
      }
    }
    

During execution, the previously resolved parameters will be used and a MapperMethod will be constructed

    public class MapperMethod {

    //Specific implementation
    public Object execute(SqlSession sqlSession, Object[] args) {
      Object result;
      switch (command.getType()) {//Call different methods according to sql type
        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 {
            //Analytical parameters
            Object param = method.convertArgsToSqlCommandParam(args);
            //Call sqlSession to execute
            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;
    }

}
@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;
  }
}

//Finally call this one
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    //Mybatis was parsed earlier_ config. XML has been loaded into the configuration
    MappedStatement ms = configuration.getMappedStatement(statement);
    //
    return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

Finally, the cacheingexecution is called. Remember that the previous sql statement is encapsulated in SqlSource? The following ms.getBoundSql(parameterObject); Eventually called`
sqlSource.getBoundSql(parameterObject);
Create a new BoundSql `. Now you have the sql and parameters

//
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

Next, the query operation will be executed, and finally BaseExecutor will be called to execute the query operation

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    //Query L1 cache
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      //Processing for stored procedures
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    //Query complete
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  //Add placeholder to cache
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    //Call doQuery
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    //Delete placeholder
    localCache.removeObject(key);
  }
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}


@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);
  }
}
//Get Statement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}

//Get Connection
protected Connection getConnection(Log statementLog) throws SQLException {
  Connection connection = transaction.getConnection();
  if (statementLog.isDebugEnabled()) {
    return ConnectionLogger.newInstance(connection, statementLog, queryStack);
  } else {
    return connection;
  }
}

//Get through BaseStatementHandler
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    statement = instantiateStatement(connection);
    setStatementTimeout(statement, transactionTimeout);
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
  }
}

//Create PreparedStatement
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
  String sql = boundSql.getSql();
  if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
    String[] keyColumnNames = mappedStatement.getKeyColumns();
    if (keyColumnNames == null) {
      return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
    } else {
      return connection.prepareStatement(sql, keyColumnNames);
    }
  } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    return connection.prepareStatement(sql);
  } else {
    return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  }
}


//PreparedStatementHandler executes the query, which should be very friendly
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  return resultSetHandler.handleResultSets(ps);
}

So far, the whole query process of the native MyBatis is over. The value here introduces the query process, and the process of addition, deletion and modification is similar. I won't repeat it here.

Reference - MyBatis technology insider

Topics: Mybatis source code analysis