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
- Method Type Check: Determines whether the method requires SQL execution. Methods inherited from
Object, such astoString()orhashCode(), are executed directly without involving SQL. - Caching Mechanism: Uses caching to improve performence when retrieving
MapperMethodinstances. 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.
- Parameter Conversion: Converts method arguments into SQL parameters using
convertArgsToSqlCommandParam(). - Delegation to SqlSession: Calls corresponding methods in
sqlSession:insert(),update(),delete(), orselectOne(). 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.
- 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.
- 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
- Placeholder Strategy: Initially inserts a placeholder into the cache. After successful retrieval, replaces it with actual results.
localCache.putObject(key, EXECUTION_PLACEHOLDER);
- Actual Query: Delegates to
doQuery(), usually implemented bySimpleExecutor.
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);