转载

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 ,并用 InterceptorChainhandler 实例进行装饰。

3. 通过 SimpleExecutor 持有的 Transaction 实例获取对应的数据库连接 connection。

4. handler 通过数据库连接初始化数据库语句 java.sql.Statement 或其子类 stmt ,设置超时时间和 fetchSize 。

5. 用 handlerstmt 进行参数化处理(比如 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. 问题

  1. 只在 XML 里定义 SQL、没有对应的 Java 接口类能否使用 MyBatis ?

    答:可以,通过 SqlSession 的方法来调用 XML 里的 SQL 语句。

  2. Mapper 接口类里可以进行方法重载吗?

    答:不能,因为 MyBatis 里根据 类名 + “.” + 方法名 来查找 SQL 语句,重载会导致这样的组合出现多条结果。

欢迎关注我的微信公众号: coderbee笔记

MyBatis Mapper 代理实现数据库调用原理
原文  https://coderbee.net/index.php/framework/20191026/2015
正文到此结束
Loading...