MyBatis SQL Execution Process

Executing SQL Statements

When calling a mapper method like:

User user = mapper.selectUser(1);

The call is handled through JDK dynamic proxy, which routes all method calls to the MapperProxy.invoke() method.

Frequently Asked Questions
  • Why use MapperProxy? It solves hardcoding issues and enables compile-time checking. Its main responsibility is mapping method calls to corresponding statement IDs.
  • What happens inside the invoke method? It determines how to locate and execute the appropriate SQL statement.
1. MapperProxy.invoke() Implementation
  1. Method Type Check: Determines whether the method requires SQL execution. Methods inherited from Object, such as toString() or hashCode(), are executed directly without involving SQL.
  2. Caching Mechanism: Uses caching to improve performence when retrieving MapperMethod instances. This design pattern is widely used throughout MyBatis.
final MapperMethod mapperMethod = cachedMapperMethod(method);

The Map.computeIfAbsent() method retrieves a value based on a key. If the value is null, it computes and assigns a new value.

In Java 8 and later, default interface methods are specially handled and return a DefaultMethodInvoker. Regular methods return a PlainMethodInvoker, which wraps a MapperMethod.

A MapperMethod contains two primary attributes:

// Statement identifier
private final SqlCommand command;
// Method signature including return type information
private final MethodSignature method;

These are inner classes of MapperMethod. Additionally, MapperMethod defines several executor methods.

2. MapperMethod.execute() Implementation

The actual SQL execution begins here:

mapperMethod.execute(sqlSession, args);

This method performs different operations depending on the SQL command type (INSERT, UPDATE, DELETE, SELECT) and the expected return type.

  1. Parameter Conversion: Converts method arguments into SQL parameters using convertArgsToSqlCommandParam().
  2. Delegation to SqlSession: Calls corresponding methods in sqlSession: insert(), update(), delete(), or selectOne(). For queries, selectOne() is typically used.
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);

3. DefaultSqlSession.selectOne() Implementation

The default implementation uses DefaultSqlSession. Internally, selectOne() delegates to selectList():

@Override
public <T> T selectOne(String statement, Object parameter) {
    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;
    }
}

In selectList(), a MappedStatement is retrieved from the configuration using the statement ID. This object contains all configuration details from the XML mapper file, including ID, statement type, SQL source, input/output parameters, etc.

Then, the query is passed to the Executor.query() method. The executor is created during session initialization and may be wrapped with second-level cache decorators and plugin interceptors.

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        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();
    }
}

If plugins are configured, their logic executes first. Then, if second-level caching is enabled (default), control passes to CachingExecutor, followed by BaseExecutor.query().

4. CachingExecutor.query() Implementation
Cache Key Generation

Question: How is the second-level cache key constructed? What constitutes an idetnical query?

In BaseExecutor.createCacheKey(), six elements determine cache key uniqueness:

cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset()); // Usually 0
cacheKey.update(rowBounds.getLimit());  // Typically Integer.MAX_VALUE
cacheKey.update(boundSql.getSql());
cacheKey.update(value); // Environment ID, e.g., "development"
cacheKey.update(configuration.getEnvironment().getId());

Thus, two queries are considered equal only if they share the same method, pagination settings, SQL text, paramter values, and environment configuration.

The CacheKey internally stores these components in a list and uses optimized hashing techniques:

  • Multiplicative Hash: hashcode = multiplier * hashcode + baseHashCode; where base is 17 and multiplier is 37.
  • Additive Hash: Maintains a checksum for additional collision resistance.

The overridden equals() method efficiently compares keys by first checking hash codes, then individual components to prevent collisions.

Second-Level Cache Management

After generating the cache key, the system attempts to retrieve data from the second-level cache:

Cache cache = ms.getCache();
if (cache != null) {
    // Process second-level cache logic
}

The cache object originates from the <cache> tag in the mapper XML file. During parsing, XMLMapperBuilder.cacheElement() creates the cache instance:

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);

This constructs a cache hierarchy using CacheBuilder:

Cache cache = new CacheBuilder(currentNamespace)
    .implementation(valueOrDefault(typeClass, PerpetualCache.class))
    .addDecorator(valueOrDefault(evictionClass, LruCache.class))
    .clearInterval(flushInterval)
    .size(size)
    .readWrite(readWrite)
    .blocking(blocking)
    .properties(props)
    .build();

Transactional Cache Handling

Why use TransactionalCacheManager (TCM)?

To maintain consistency between transactions and cache updates. Without TCM, dirty reads could occur when a transaction rolls back after writing to the cache but not committing to the database.

First-Level Cache Difference: Session-scoped caches automatically clear upon rollback since each session represents a single transaction.

  1. Writing to Second-Level Cache:
tcm.putObject(cache, key, list);

Data is temporarily stored in a pending map until transaction commit triggers actual cache insertion.

  1. Reading from Second-Level Cache:
List<E> list = (List<E>) tcm.getObject(cache, key);

Retrieval involves traversing decorated cache layers until reaching the underlying PerpetualCache.

5. BaseExecutor.query() Implementation
Local Cache Management

The queryStack prevents recursive query processing and ensures proper cache handling.

If flushCache=true, the local cache is cleared before proceeding:

if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
}

For uncached requests, data is fetched via queryFromDatabase():

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

If LocalCacheScope=STATEMENT, the cache is cleared post-execution:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    clearLocalCache();
}

Database Query Execution
  1. Placeholder Strategy: Initially inserts a placeholder into the cache. After successful retrieval, replaces it with actual results.
localCache.putObject(key, EXECUTION_PLACEHOLDER);

  1. Actual Query: Delegates to doQuery(), usually implemented by SimpleExecutor.
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

Tags: MyBatis sql-execution jdk-proxy Caching transaction-management

Posted on Tue, 06 Oct 2026 16:33:11 +0000 by stevenye