一条 SQL 是如何在 MyBatis 中执行的

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 前言MyBatis 执行 SQL 的核心接口为 SqlSession 接口,该接口提供了一些 CURD 及控制事务的方法,另外还可以通过 SqlSession 先获取 Mapper 接口的实例,然后通过 Mapper 接口执行 SQL,Mapper 接口方法的执行最终还是委托到 SqlSession 中的方法。

前言


MyBatis 执行 SQL 的核心接口为 SqlSession 接口,该接口提供了一些 CURD 及控制事务的方法,另外还可以通过 SqlSession 先获取 Mapper 接口的实例,然后通过 Mapper 接口执行 SQL,Mapper 接口方法的执行最终还是委托到 SqlSession 中的方法。因此可以由 SqlSession 入手分析 SQL 执行流程。由于本篇文章内容较多,感兴趣的小伙伴可以先收藏,待空闲时间耐心阅读,或直接翻到最后查看总结。


SQL 执行流程分析


MyBatis 中的 SQL 都是由 SqlSession 进行执行,由于日常工作中使用的 SQL 类型多为查询,并且 MyBatis 中的查询也最为复杂,因此本篇以SqlSession#selectList(String, Object, RowBounds)作为入口进行分析,中间穿插 SqlSession 的其他 API 介绍,重要的组件将在后面的章节中单独列出。


public interface SqlSession extends Closeable {
    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);  
}


#selectList 的方法定义如上所示,该方法查询数据库然后将结果转换为用户需要的类型。各参数的具体含义如下。


statement:表示 SQL 语句的标识,Mapper 接口中的方法调用时会使用 接口全限定名.方法名,对应 Mapper xml 配置中mapper节点namespace.select节点id。

parameter:MyBatis SQL 语句中的参数,可以是原生类型或原生类型的包装类,可以是 Map ,也可以是其他的 Object,如果 Mapper 接口方法中包含多个参数将转换为 Map,MyBatis 取 Map 或 Object 中的字段值替换 Mapper xml 文件中的 ${paramName}或将#{paramName} 指定的 SQL 参数设置为对应的字段值。

rowBounds:分页信息,包含 offset 和 limit ,MyBatis 在内存中对返回的结果进行分页。

了解方法的功能后,我们再看方法的实现,MyBatis 中 SqlSession 默认的实现为 DefaultSqlSession,跟踪源码。


public class DefaultSqlSession implements SqlSession {
    // Mybatis 配置
    private final Configuration configuration;
    // 执行器
    private final Executor executor;
    @Override
    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();
        }
    }
}


SqlSession 进行数据库查询时先从配置中获取表示 SQL 语句的 MappedStatement,然后使用执行器 Executor 进行执行。调用的 Executor#query 方法定义如下。


public interface Executor {
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
}


Executor 执行查询时多了一个新的参数 ResultHandler,它用于处理每一行数据库记录对应的 Java 对象,例如可以将结果保存到 List 或者 Map 中。Executor 作为接口具有多个实现,CachingExecutor 和其他 Executor 相比仅多了 Statement 级别缓存的支持,因此我们跟踪 BaseExecutor#query 方法的实现。


public abstract class BaseExecutor implements Executor {
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
}


BaseExecutor 先使用 MappedStatement 获取到 BoundSql 的实例,然后创建了表示当前查询的 CacheKey,最后调用了另一个 #query 方法。


BoundSql:MappedStatement 包含 MyBatis 执行 SQL 需要的完整元数据,如结果映射、参数映射、动态 SQL 等,MappedStatement 将动态 SQL 解析后生成 BoundSql,BoundSql 仅包含最终执行的 SQL 及参数信息。


CacheKey:MyBatis 可以将每个 SQL 的查询结果缓存下来,CacheKey 就是用来表示缓存的 key 值,它由 SQL、参数、分页等构成,当下次使用相同的条件查询数据库时可以优先从缓存获取到查询结果。


了解完这些参数后再看调用的 #query 方法。


public abstract class BaseExecutor implements Executor {
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ... 省略校验及缓存处理代码
        List<E> list;
        try {
            queryStack++;
            // 优先从缓存获取
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
              // 处理存储过程 OUT 参数
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 缓存中没有数据,从数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        ... 省略缓存处理相关代码
        return list;
    }
}


为了将重点放在主要流程,上面的代码省略了部分缓存处理的方法,关于缓存处理将在后面单独分析。#query 方法优先从缓存中获取查询结果,如果没有获取到则会从数据库进行查询,再看数据库查询的代码。


public abstract class BaseExecutor implements Executor {
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
          // 执行数据库查询
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        // 将查询结果缓存
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
        throws SQLException;    
}


#queryFromDatabase 调用 #doQuery 方法进行数据库查询,然后将查询结果缓存下来。#doQuery 是一个抽象方法,我们看其在默认使用的 SimpleExecutor 中的实现。


public class SimpleExecutor extends BaseExecutor {
    @Override
    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();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            // 创建 Statement,设置 SQL 参数
            stmt = prepareStatement(handler, ms.getStatementLog());
            // 使用 StatementHandler 执行查询
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
    // 准备好 Statement
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        // 创建 Statement
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 设置 SQL 参数
        handler.parameterize(stmt);
        return stmt;
    }
}


到了这里,终于看到了熟悉的 JDBC API。#doQuery 方法先使用配置创建了一个 StatementHandler,使用 StatementHandler 创建 Statement 并设置了 SQL 的参数后就开始调用 #StatementHandler#query 执行数据库查询。StatementHandler 用于创建 StatementHandler、设置参数、执行 SQL,如果没有指定则使用


PreparedStatementHandler。


先看 StatementHandler 创建 Statement 的方法 #prepared。


public abstract class BaseStatementHandler implements StatementHandler {
    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        ErrorContext.instance().sql(boundSql.getSql());
        Statement statement = null;
        try {
          // 实例化 Statement
            statement = instantiateStatement(connection);
            // 设置 Statement 的数据库参数
            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);
        }
    }
  // 实例化 Statement
    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
}


StatementHandler#prepared 方法由基类 BaseStatementHandler 实现,先调用了模板方法 #instantiateStatement 实例化 Statement,然后设置 Statement,模板方法由具体的子类实现,如 PreparedStatementHandler 会实例化出 PreparedStatement。


再跟踪 StatementHandler 的实现 PreparedStatementHandler 设置参数的 #parameterize 方法。


public class PreparedStatementHandler extends BaseStatementHandler {
    @Override
    public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
    }
}


由于 PreparedStatement 需要设置参数,因此这里 PreparedStatementHandler 将设置参数的动作委托给 ParameterHandler 进行处理。


再看 StatementHandler 查询数据的方法StatementHandler#query 的实现。


public class PreparedStatementHandler extends BaseStatementHandler {
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
    }
}


PreparedStatementHandler 先调用 PreparedStatement#execute 方法执行 SQL,然后使用 ResultSetHandler 将查询结果转换为需要的类型。ResultSetHandler 会根据 resultMap 将数据库记录转换为 Mapper 接口方法返回的对象,由于其内部实现比较复杂,这里暂不进行分析。


MappedStatement


概念理解:根据 CURD 四种 SQL 类型,Mapper xml 文件中操作数据库的节点分为 insert、update、select、delete 四种,对应到 Mapper 接口方法上可以使用的注解则为 @Insert、@Update、@Select、@Delete,MappedStatement 表示这四种语句的元数据,MyBatis 将其保存在 Configuration 中。


解析存储:MyBatis 解析 Mapper xml 文件时会使用 xml 节点中的元数据构建 MappedStatement 实例然后添加到 Configuration,也可以直接将 Mapper 接口直接添加到 Configuration,此时会使用 Mapper 接口中的注解信息构建 MappedStatement 然后添加到 Configuration。


源码位置:参见 XMLMapperBuilder#buildStatementFromContext(List<XNode>)、MapperAnnotationBuilder#parseStatement


Executor


概念理解:Executor 接口是 SQL 的执行器,它根据 SQL 语句的抽象 MappedStatement 及参数操作数据库,返回操作结果,如果进行数据库查询还可以将结果转换为用户期望的类型。


配置 Executor:通常情况下在 MyBatis 中不需要显式指定具体 Executor,如果需要指定则有以下两种方式。


xml 配置文件中 /configuration/settings 节点下指定 defaultExecutorType 的值来配置默认的 Executor。

通过 Configuration#newExecutor(Transaction, ExecutorType) 方法创建 Executor 的实例。

Executor 接口定义:Executor 接口定义如下。


public interface Executor {
  // 数据库新增、修改、或删除
    int update(MappedStatement ms, Object parameter) throws SQLException;
    // 数据库查询
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  // 支持批处理的 Executor 批量执行 SQL
    List<BatchResult> flushStatements() throws SQLException;
    // 事务管理
    void commit(boolean required) throws SQLException;
    void rollback(boolean required) throws SQLException;
    Transaction getTransaction();    
    // 从缓存中加载对象的属性值,或记录要从缓存中获取的属性
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
    // 连接管理
    void close(boolean forceRollback);
    boolean isClosed();
    // 设置当前 Executor 的包装器
    void setExecutorWrapper(Executor executor);   
}


Executor 实现:Executor 作为一个接口在 MyBatis 中有多种实现,类图如下。


image.png


BaseExecutor:Executor 的基类。


SimpleExecutor:MyBatis 中默认的 Executor。

ReuseExecutor:复用 Statement 的 Executor,对于相同的 SQL,使用的是同一个 Statement。

BatchExecutor:支持批处理的 Executor,每次执行 #update 方法时会将 SQL 添加到 Statement 中,直到调用#flushStatements方法开始提交到数据库执行。

CachingExecutor:作为其他 Executor 的包装器,支持 Statement 级别的缓存,其他 Executor 仅支持 Session 级别的缓存。

ResultHandler

ResultHandler 用于处理 MyBatis 将某一条数据库记录转换成的 Java 对象,通常会将转换结果保存到其内部,待使用时再获取。其接口定义如下。


public interface ResultHandler<T> {
     // 处理每一行对应的值
    void handleResult(ResultContext<? extends T> resultContext);
}


接口中只有一个方法,根据结果(即数据库单行记录对应的 Java 对象)的上下文处理结果。ResultHandler 在 MyBatis 中的实现有两个,具体如下。


DefaultResultHandler:将结果存储至内部的 List,Mapper 接口方法返回类型为 List 时使用。

DefaultMapResultHandler:将结果存储至内部的 Map 中,Mapper 接口方法返回类型为 Map 时使用,此时需要在 Mapper 方法上使用 @MapKey 注解指定 key 使用结果的哪个属性。

StatementHandler

StatementHandler 表示 JDBC 中 Statement 的处理器,用于创建 Statement、设置 Statement 中的 SQL 参数、执行 SQL,接口定义如下。


public interface StatementHandler {
  // 创建 Statement,并设置数据库相关参数
    Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
    // 设置 Statement 中的 SQL 参数
    void parameterize(Statement statement) throws SQLException;  
    // 将 SQL 添加到批量执行列表中
    void batch(Statement statement) throws SQLException; 
    // 执行 添加、更新、删除 SQL
    int update(Statement statement) throws SQLException;  
    // 执行查询
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;         
    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;  
  // 获取 SQL 信息
    BoundSql getBoundSql();
    // 获取参数处理器
    ParameterHandler getParameterHandler();            
}


根据不同的 Statement 类型,StatementHandler 具有不同的实现,其设计和 Executor 很类似,具体如下。


image.png


BaseStatementHandler:抽象的 StatementHandler 基类,提供子类的通用实现。

SimpleStatementHandler:简单 StatementHandler,处理普通的 Statement。

PreparedStatementHandler:支持预处理的 StatementHandler,处理 PreparedStatement。

CalableStatementHandler:支持存储过程的 StatementHandler,处理 CallableStatement。

RoutingStatementHandler:其他 StatementHandler 的装饰器,根据 Statement 的类型委托给其他 StatementHandler 做具体的处理。


ParameterHandler


ParameterHandler 是 SQL 参数的处理器,用于设置 SQL 中的参数。其定义比较简单,具体如下,它只有一个默认的实现 DefaultParameterHandler。


public interface ParameterHandler {
  // 获取参数对象,该对象包含 SQL 中可用的参数
    Object getParameterObject();
  // 设置 SQL 参数
    void setParameters(PreparedStatement ps) throws SQLException;
}


ResultSetHandler


ResultSetHandler 是 ResultSet 的处理器,当 Statement 执行 SQL 之后,就会使用 ResultSetHandler 处理产生的 ResultSet,ResultHandler 会根据 Mapper xml 文件中定义的 resultMap 或 resultType 将数据库记录转换为 Mapper 接口方法的返回值类型。对于每一行转换为的 Java 对象,使用 ResultHandler 进行处理。该接口默认的实现是 DefaultResultSetHandler ,该接口定义如下。


public interface ResultSetHandler {
  // 处理 ResultSet 为 List
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    // 处理 ResultSet 为 Cursor
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    // 处理存储过程 OUT 参数
    void handleOutputParameters(CallableStatement cs) throws SQLException;    
}


总结

前面以 SqlSession 的方法为入口,分析了 SQL 在 MyBatis 内部执行的代码,并对重要的 API 进行了介绍,这里使用文字的方式进行总结整个过程。


首先我们会使用 SqlSessionFactoryBuilder 构建 SqlSessionFactory。期间 MyBatis 会解析 Mapper xml 文件或注解,生成 SQL 语句元数据 MappedStatement,并保存到 Configuration 中。

使用 SqlSessionFactory 获取 SqlSession,默认的 SqlSession 是 DefaultSqlSession。

使用 SqlSession 获取 Mapper 接口实现,或直接执行 SqlSession 其他方法操作数据库。

SqlSession 根据语句的标识从 Configuration 中获取 MappedStatement,然后委托 Executor 操作数据库。

Executor 优先从缓存中获取数据,如果缓存中没有则执行数据库查询,对于 update 操作会先刷新缓存。

Executor 使用 Configuration 创建出 Statement 的处理器 StatementHandler,委托 StatementHandler 执行数据库操作。

StatementHandler 先实例化出 Statement,然后使用 ParameterHandler 设置 SQL 的参数,最后执行 Statement。

StatementHandler 委托 ResultSetHandler 处理结果集。

ResultSetHandler 处理结果集,根据 resultMap 或 resultType 将每一行数据库的记录转换为 Java 对象,然后将 Java 对象交由 ResultHandler 处理,最后转换为 Mapper 接口方法的返回类型。


目录
相关文章
|
5月前
|
SQL Java 测试技术
3、Mybatis-Plus 自定义sql语句
这篇文章介绍了如何在Mybatis-Plus框架中使用自定义SQL语句进行数据库操作。内容包括文档结构、编写mapper文件、mapper.xml文件的解释说明、在mapper接口中定义方法、在mapper.xml文件中实现接口方法的SQL语句,以及如何在单元测试中测试自定义的SQL语句,并展示了测试结果。
3、Mybatis-Plus 自定义sql语句
|
18天前
|
SQL XML Java
mybatis实现动态sql
MyBatis的动态SQL功能为开发人员提供了强大的工具来应对复杂的查询需求。通过使用 `<if>`、`<choose>`、`<foreach>`等标签,可以根据不同的条件动态生成SQL语句,从而提高代码的灵活性和可维护性。本文详细介绍了动态SQL的基本用法和实际应用示例,希望对您在实际项目中使用MyBatis有所帮助。
47 11
|
2月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
2月前
|
SQL Java 数据库连接
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
|
3月前
|
SQL Java 数据库连接
mybatis使用四:dao接口参数与mapper 接口中SQL的对应和对应方式的总结,MyBatis的parameterType传入参数类型
这篇文章是关于MyBatis中DAO接口参数与Mapper接口中SQL的对应关系,以及如何使用parameterType传入参数类型的详细总结。
61 10
|
4月前
|
SQL XML Java
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
文章介绍了MyBatis中动态SQL的用法,包括if、choose、where、set和trim标签,以及foreach标签的详细使用。通过实际代码示例,展示了如何根据条件动态构建查询、更新和批量插入操作的SQL语句。
mybatis复习03,动态SQL,if,choose,where,set,trim标签及foreach标签的用法
|
5月前
|
SQL Java 数据库连接
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
文章讲述了在使用Mybatis时遇到的资源文件找不到的问题,并提供了通过修改Maven配置来解决资源文件编译到target目录下的方法。
Mybatis系列之 Error parsing SQL Mapper Configuration. Could not find resource com/zyz/mybatis/mapper/
|
4月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
75 1
|
5月前
|
SQL Java 数据库连接
Mybatis系列之 动态SQL
文章详细介绍了Mybatis中的动态SQL用法,包括`<if>`、`<choose>`、`<when>`、`<otherwise>`、`<trim>`和`<foreach>`等元素的应用,并通过实际代码示例展示了如何根据不同条件动态生成SQL语句。
|
5月前
|
SQL 关系型数据库 MySQL
解决:Mybatis-plus向数据库插入数据的时候 报You have an error in your SQL syntax
该博客文章讨论了在使用Mybatis-Plus向数据库插入数据时遇到的一个常见问题:SQL语法错误。作者发现错误是由于数据库字段中使用了MySQL的关键字,导致SQL语句执行失败。解决方法是将这些关键字替换为其他字段名称,以避免语法错误。文章通过截图展示了具体的操作步骤。