MyBatis Mapper 代理实现数据库调用原理
1. Mapper 代理层执行
Mapper 代理上执行方法调用时,调用被委派给 MapperProxy 来处理。
public class MapperProxy<T> implements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } // 接口里声明的方法,转换为 MapperMethod 来调用 final MapperMethod mapperMethod = cachedMapperMethod(method); // 与 Spring 集成时此处的 sqlSession 仍然 SqlSessionTemplate return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } }
MapperMethod 根据 mapperInterface.getName() + "." + method.getName()
从 Configuration 对象里找到对应的 MappedStatement
,从而得到要执行的 SQL 操作类型(insert/delete/update/select/flush),然后调用传入的 sqlSession 实例上的相应的方法。
public Object execute(SqlSession sqlSession, Object[] args) { Object result; if (SqlCommandType.INSERT == command.getType()) { // 把参数转换为 SqlSession 能处理的格式 Object param = method.convertArgsToSqlCommandParam(args); // 在 sqlSession 上执行并处理结果 result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); ...省略
如果上述方法传入的是 SqlSessionTemplate
,那么这些方法调用会被 SqlSessionInterceptor
拦截,加入与 Spring 事务管理机制协作的逻辑,具体可以看这篇文章MyBatis 事务管理,这里不再展开,最终会调用到 DefaultSqlSession
实例上的方法。
2. 会话层的执行过程
SqlSession 里声明的所有方法的第一个参数如果是 String statement
,则都是 mapperInterface.getName() + "." + method.getName()
,表示要调用的 SQL 语句的标识符。通过它从 configuration 找到 MappedStatement
。
会话层最主要的逻辑是进行参数的包装,获取对应的 MappedStatement
,然后调用持有的 Executor
的方法去执行。
public class DefaultSqlSession implements SqlSession { private Configuration configuration; private Executor executor; private boolean autoCommit; private boolean dirty; private List<Cursor<?>> cursorList; public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; } 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(); } }
3. Executor
执行的过程
我们知道 JDBC 里有三种数据库语句:java.sql.Statement/PreparedStatement/CallableStatement
,每种语句的执行方式是不一样的,MyBatis 创建了 StatementHandler
抽象来表示数据库语句的处理逻辑,有对应的三种具体实现:SimpleStatementHandler/PreparedStatementHandler/CallableStatementHandler
。
RoutingStatementHandler
是个门面模式,构建时根据要执行的数据库语句类型实例化 SimpleStatementHandler/PreparedStatementHandler/CallableStatementHandler
中的一个类作为目标 delegate,并把调用都转给这个 delegate 的方法。
不同的 handler 实现实现了对应的:数据库语句的创建、参数化设置、执行语句。
通过这层抽象,MyBatis 统一了 Executor 里的执行流程,以下以 SimpleExecutor 的流程为例:
1. 对于传入的 MappedStatement ms
,得到 Configuration configuration
。
2. configuration
通过 ms
的语句类型得到一个 RoutingStatementHandler
的实例(内部有个 delegate 可以委派) handler
,并用 InterceptorChain
对 handler
实例进行装饰。
3. 通过 SimpleExecutor
持有的 Transaction
实例获取对应的数据库连接 connection。
4. handler
通过数据库连接初始化数据库语句 java.sql.Statement
或其子类 stmt
,设置超时时间和 fetchSize 。
5. 用 handler
对 stmt
进行参数化处理(比如 PreparedStatement
设置预编译语句的参数值)。
6. handler
执行相应的 stmt
完成数据库操作。
7. 用 ResultSetHandler
对结果集进行处理。ResultSetHandler
会调用 TypeHandler
来进行 Java 类型与数据库列类型之间转换。
// 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(); // 创建 handler 来负责具体的执行 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 创建数据库语句 stmt = prepareStatement(handler, ms.getStatementLog()); // 执行数据库操作 return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } } // Configuration public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } // RoutingStatementHandler public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 根据SQL语句的执行方式创建对应的 handler 实例 switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { // 创建数据库连接 Connection connection = getConnection(statementLog); // 创建数据库语句 Statement stmt = handler.prepare(connection, transaction.getTimeout()); // 参数化设置 handler.parameterize(stmt); return stmt; } protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } } // BaseStatementHandler public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null; try { // 由具体的子类来创建对应的 Statement 实例 statement = instantiateStatement(connection); // 通用参数设置 setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } } // PreparedStatementHandler protected Statement instantiateStatement(Connection connection) throws SQLException { String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } else { return connection.prepareStatement(sql); } } // PreparedStatementHandler public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); }
4. 问题
只在 XML 里定义 SQL、没有对应的 Java 接口类能否使用 MyBatis ?
答:可以,通过 SqlSession 的方法来调用 XML 里的 SQL 语句。Mapper 接口类里可以进行方法重载吗?
答:不能,因为 MyBatis 里根据 类名 + “.” + 方法名 来查找 SQL 语句,重载会导致这样的组合出现多条结果。
欢迎关注我的微信公众号: coderbee笔记 。