Inside MyBatis: How SqlSessionFactory, Executor, and Mappers Work Together

This article walks through MyBatis' internal workflow—from parsing the global XML configuration and building the SqlSessionFactory, to creating a SqlSession, executing a query via the Executor, handling caching, and finally mapping results—illustrated with key code snippets and a diagram.

Programmer DD
Programmer DD
Programmer DD
Inside MyBatis: How SqlSessionFactory, Executor, and Mappers Work Together

Recently I wanted to write a MyBatis pagination plugin, so I first needed to understand how MyBatis works internally. Below is a step‑by‑step analysis of the core components and the execution flow.

Core Components

SqlSession

Executor

StatementHandler

ParameterHandler

ResultSetHandler

TypeHandler

MappedStatement

Configuration

Before diving into the workflow, here is the global MyBatis configuration file used in the example:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <!-- environments configuration will be removed after Spring integration -->
  <environments default="development">
    <environment id="development">
      <!-- use JDBC transaction management -->
      <transactionManager type="JDBC"/>
      <!-- database connection pool -->
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="sqlMapper/userMapper.xml"/>
  </mappers>
</configuration>

Step 1: Create SqlSessionFactory

The creation starts with XMLConfigBuilder which parses the XML file and builds a Configuration object. Key methods include:

public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

During parsing the following elements are processed:

private void parseConfiguration(XNode root) {
    try {
        propertiesElement(root.evalNode("properties"));
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        settingsElement(root.evalNode("settings"));
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

The mapperElement method resolves each mapper entry. It supports three ways to specify a mapper: resource, url, or class. In our example the resource form is used, so the parser loads the XML mapper file via Resources.getResourceAsStream and creates an XMLMapperBuilder to parse it.

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    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.");
                }
            }
        }
    }
}

After the mapper files are parsed, the configuration processes each <mapper> element to build statements. The method configurationElement extracts the namespace, cache, parameter maps, result maps, SQL fragments, and the CRUD statements:

private void configurationElement(XNode context) {
    String namespace = context.getStringAttribute("namespace");
    if (namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

Each SQL node is turned into a MappedStatement by parseStatementNode, which finally calls builderAssistant.addMappedStatement. The MappedStatement objects are stored in Configuration.mappedStatements keyed by their statement IDs.

public void parseStatementNode() {
    // ... omitted for brevity ...
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

The addMappedStatement method registers the statement in the configuration map:

public MappedStatement addMappedStatement(String id, SqlSource sqlSource,
        StatementType statementType, SqlCommandType sqlCommandType,
        Integer fetchSize, Integer timeout, String parameterMap,
        Class<?> parameterType, String resultMap, Class<?> resultType,
        ResultSetType resultSetType, boolean flushCache, boolean useCache,
        boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty,
        String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
    // ... validation ...
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
    // ... set other properties ...
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

When the configuration is ready, SqlSessionFactoryBuilder.build creates a DefaultSqlSessionFactory:

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try { inputStream.close(); } catch (IOException ignored) {}
    }
}

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

Step 2: Create SqlSession

From the factory a SqlSession is opened. The default implementation creates a transaction, an executor, and a DefaultSqlSession:

public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        Environment environment = configuration.getEnvironment();
        TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

Step 3: Execute a SQL Request

Using the session, a typical query looks like:

User user = sqlSession.selectOne("test.findUserById", 1);

The selectList method (used internally by selectOne) performs the following actions:

Retrieve the MappedStatement from the configuration by statement ID.

Delegate the query to the Executor.

Create a cache key for the query; if the result is cached, return it.

If not cached, call queryFromDatabase to hit the DB.

Store the result list in the local cache.

Build a StatementHandler to prepare and parameterize the JDBC Statement.

Execute the statement and let ResultSetHandler convert the ResultSet into a List.

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        return result;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

The executor’s query method checks the local cache and, if needed, calls queryFromDatabase which ultimately invokes doQuery in SimpleExecutor:

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.<E>query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}
prepareStatement

obtains a JDBC connection, lets the handler create the PreparedStatement, and then calls parameterize to set the parameters. The query method of the handler executes the statement and passes the ResultSet to ResultSetHandler.handleResultSets, which builds the final list of result objects.

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Connection connection = getConnection(statementLog);
    Statement stmt = handler.prepare(connection);
    handler.parameterize(stmt);
    return stmt;
}

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E>handleResultSets(ps);
}

Summary of the Process

Generate the SQL statement dynamically and wrap it in a BoundSql object.

Create a cache key for the query.

If the cache misses, read data directly from the database.

Execute the query, obtain a List result, and store it in the cache.

Instantiate a StatementHandler based on the parameters.

Pass the created Statement to the handler and call parameterize to set values.

Invoke StatementHandler.query() to get the final List result set.

The following diagram visualises the whole flow:

MyBatis execution flow diagram
MyBatis execution flow diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMyBatisORMExecutorsqlSessionFactory
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.