Understanding MyBatis Mapper and Transaction Mechanisms

  1. How MyBatis Mappers Are Implemented and Why Interfaces Suffice for SQL Execution

MyBatis employs JDK dynamic proxies to implement mapper interfaces, allowing developers to work with interfaces rather then concrete implementations. These mapper proxies are obtained through the SqlSession interface, which serves as the primary API for database operations.

// SqlSession interface method
<T> T getMapper(Class<T> mapperType);


// DefaultSqlSession implementation
@Override
public <T> T getMapper(Class<T> mapperType) {
  // Retrieve mapper proxy from Configuration
  return configuration.getMapper(mapperType, this);
}


The Configuraton class acts as a global repository for all configuration data, including mapper proxy objects. During initialization, MyBatis scans mapper interfaces and registers their proxy factories using a MapperRegistry. The Configuration class maintains a reference to this MapperRegistry, enabling efficient retrieval of mapper proxies.

// Configuration class method
public <T> T getMapper(Class<T> mapperType, SqlSession sqlSession) {
  return mapperRegistry.getMapper(mapperType, sqlSession);
}


MapperRegistry maintains a map that serves as a cache to minimize the overhead of creating proxy objects repeatedly.

// MapperRegistry's internal map
private final Map<Class<?>, MapperProxyFactory<?>> mapperFactories = new HashMap<>();


// MapperProxyFactory creates proxy instances
public T createProxy(SqlSession sqlSession) {
    // Implementation of InvocationHandler
    final MapperProxy<T> proxyHandler = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return createProxyInstance(proxyHandler);
}

protected T createProxyInstance(MapperProxy<T> proxyHandler) {
    // Generate proxy using JDK dynamic proxy
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(), 
        new Class[] { mapperInterface }, 
        proxyHandler
    );
}


How do proxy objects execute database operations?

When a method is invoked on the proxy, it analyzes the method's SQL operation type (SELECT, INSERT, UPDATE, DELETE), identifies the corresponding MapperStatement, and prepares the necessary information in a MapperMethod object. The execute method in MapperMethod then routes the operation to the appropriate SqlSession API based on the method type.

// MapperProxy class
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // Direct invocation for Object class methods
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
      // Core proxy logic
      return methodInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}


private MethodInvoker methodInvoker(Method method) throws Throwable {
    // Method caching logic
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
        return new StandardMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
    });
}


private static class StandardMethodInvoker implements MethodInvoker {
    private final MapperMethod mapperMethod;

    public StandardMethodInvoker(MapperMethod mapperMethod) {
      this.mapperMethod = mapperMethod;
    }

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


The MapperMethod class contains two key components: SqlCommand and MethodSignature. The SqlCommand records the operation type (SELECT, INSERT, UPDATE, DELETE) and the corresponding MapperStatement ID. This ID is used to locate the MapperStatement in the global configuration, which is created during XML parsing and stored in the Configuration's map with the format "MapperInterfaceName.methodName".

The MethodSignature component tracks the method's return type.

// MapperMethod execute method
public Object execute(SqlSession sqlSession, Object[] parameters) {
  Object result;
  // Determine operation type
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertParameters(parameters);
      result = handleRowCount(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertParameters(parameters);
      result = handleRowCount(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertParameters(parameters);
      result = handleRowCount(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, parameters);
        result = null;
      } else if (method.returnsMultiple()) {
        result = executeForMultipleResults(sqlSession, parameters);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, parameters);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, parameters);
      } else {
        Object param = method.convertParameters(parameters);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() && 
            (result == null || !method.getReturnType().isInstance(result))) {
          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;
}


  1. How Transactions Are Implemented in MyBatis

MyBatis fundamentally wraps JDBC database operations. The MyBatis JdbcTransaction (containing dataSource and connection properties) closely resembles standard JDBC transactions but extends support for connection pooling.

// DefaultSqlSessionFactory creates SqlSession with transaction management
private SqlSession createSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction transaction = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactory(environment);
    // Build transaction object
    transaction = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // Create executor
    final Executor executor = configuration.newExecutor(transaction, execType);
    // Create SqlSession
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(transaction);
    throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}


// MyBatis Transaction interface
public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
}


The actual database connection operations—commit, rollback, and setAutoCommit—are the core JDBC transaction commands. The close method merely returns the connection to the pool for reuse and has no direct relation to transaction management.

Both SqlSession and Executor delegate their transaction methods to the Transaction interface, meaning all transaction commits, rollbacks, and closes are handled by the Transaction implementation.

            // Begins transaction by setting autoCommit to false
            SqlSession sqlSession = MybatisSqlSessionFactory.openSession();
		try {
			StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
			
			Student student = new Student();
			student.setName("yy");
			student.setEmail("email@email.com");
			student.setDob(new Date());
			student.setPhone(new PhoneNumber("123-2568-8947"));
			
			studentMapper.insertStudent(student);
			// Commit transaction
			sqlSession.commit();
			
			studentMapper.insertStudent(student);
			// Multiple commits
			sqlSession.commit();
		} catch (Exception e) {
		        // Rollback current uncommitted transaction
			sqlSession.rollback();
		} finally {
			sqlSession.close();
		}


Note: When executing insertStudent(student), the method itself doesn't handle transaction commits, rollbacks, or closes. As shown in the example, these operations must be explicitly called.

Therefore, when analyzing methods like insert() or update(), remember that they don't contain transaction logic. Focus on the explicit commit(), rollback(), and close() calls.

Within a single connection's lifecycle, multiple transactions can exist. In JDBC, each insert operation creates a database record, and rollback() only affects the current uncommitted transaction.

Tags: MyBatis JDBC Dynamic Proxy Mapper Transaction Management

Posted on Thu, 08 Oct 2026 16:20:54 +0000 by WorldBizEduComputerShops