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.
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);
}
} prepareStatementobtains 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:
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
