乐趣区

关于mybatis:Mybatis-手撸专栏第7章SQL执行器的定义和实现

作者:小傅哥
博客:https://bugstack.cn – 《手写 Mybatis 系列》

一、前言

为什么,要读框架源码?

因为手里的业务工程代码太拉胯了!通常作为业务研发,所开发进去的代码,大部分都是一连串的流程化解决,短少性能逻辑的解耦,有着迭代频繁但可迭代性差的特点。所以这样的代码通常只能学习业务逻辑,却很难排汇到大型零碎设计和性能逻辑实现的成功经验,往往都是失败的教训。

而所有零碎的设计和实现,外围都在于如何解耦,如果解耦不清晰最初间接导致的就是再持续迭代性能时,会让整个零碎的实现越来越臃肿,稳定性越来越差。而对于解耦的实际在各类框架的源码中都有十分不错的设计实现,所以浏览这部分源码,就是在排汇胜利的教训。把解耦的思维逐渐使用到理论的业务开发中,才会让咱们写出更加优良的代码构造。

二、指标

在上一章节咱们实现了有 / 无连接池的数据源,能够在调用执行 SQL 的时候,通过咱们实现池化技术实现数据库的操作。

那么对于池化数据源的调用、执行和后果封装,目前咱们还都只是在 DefaultSqlSession 中进行发动 如图 7-1 所示。那么这样的把代码流程写死的形式必定不适合于咱们扩大应用,也不利于 SqlSession 中每一个新增定义的办法对池化数据源的调用。

  • 解耦 DefaultSqlSession#selectOne 办法中对于对数据源的调用、执行和后果封装,提供新的功能模块代替这部分硬编码的逻辑解决。
  • 只有提供了独自的执行办法入口,咱们能力更好的扩大和应答这部分内容里的需要变动,包含了各类入参、后果封装、执行器类型、批处理等,来满足不同款式的用户需要,也就是配置到 Mapper.xml 中的具体信息。

三、设计

从咱们对 ORM 框架渐进式的开发过程上,能够分出的执行动作包含,解析配置、代理对象、映射办法等,直至咱们后面章节对数据源的包装和应用,只不过咱们把数据源的操作硬捆绑到了 DefaultSqlSession 的执行办法上了。

那么当初为理解耦这块的解决,则须要独自提出一块执行器的服务性能,之后将执行器的性能随着 DefaultSqlSession 创立时传入执行器性能,之后具体的办法调用就能够调用执行器来解决了,从而解耦这部分功能模块。如图 7-2 所示。

  • 首先咱们要提取出执行器的接口,定义出执行办法、事务获取和相应提交、回滚、敞开的定义,同时因为执行器是一种规范的执行过程,所以能够由抽象类进行实现,对过程内容进行模板模式的过程包装。在包装过程中定义抽象类,由具体的子类来实现。这一部分在下文的代码中会体现到 SimpleExecutor 简略执行器实现中。
  • 之后是对 SQL 的解决,咱们都晓得在应用 JDBC 执行 SQL 的时候,分为了简略解决和预处理,预处理中包含筹备语句、参数化传递、执行查问,以及最初的后果封装和返回。所以咱们这里也须要把 JDBC 这部分的步骤,分为结构化的类过程来实现,便于性能的拓展。具体代码次要体现在语句处理器 StatementHandler 的接口实现中。

四、实现

1. 工程构造

mybatis-step-06
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           ├── datasource
    │           ├── executor
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultHandler.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

工程源码 公众号「bugstack 虫洞栈」,回复:手写 Mybatis,获取残缺源码

SQL 办法执行器外围类关系,如图 7-3 所示

  • 以 Executor 接口定义为执行器入口,确定出事务和操作和 SQL 执行的对立标准接口。并以执行器接口定义实现抽象类,也就是用抽象类解决对立共用的事务和执行 SQL 的规范流程,也就是这里定义的执行 SQL 的形象接口由子类实现。
  • 在具体的简略 SQL 执行器实现类中,解决 doQuery 办法的具体操作过程。这个过程中则会引入进来 SQL 语句处理器的创立,创立过程仍有 configuration 配置项提供。你会发现很多这样的生成解决,都来自于配置项
  • 当执行器开发实现当前,接下来就是交给 DefaultSqlSessionFactory 开启 openSession 的时候随着构造函数参数传递给 DefaultSqlSession 中,这样在执行 DefaultSqlSession#selectOne 的时候就能够调用执行器进行解决了。也就由此实现解耦操作了。

2. 执行器的定义和实现

执行器分为接口、抽象类、简略执行器实现类三局部,通常在框架的源码中对于一些规范流程的解决,都会有抽象类的存在。它负责提供共性功能逻辑,以及对接口办法的执行过程进行定义和解决,并提取形象接口交由子类实现。这种设计模式也被定义为模板模式。

2.1 Executor

源码详见cn.bugstack.mybatis.executor.Executor

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    <E> List<E> query(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql);

    Transaction getTransaction();

    void commit(boolean required) throws SQLException;

    void rollback(boolean required) throws SQLException;

    void close(boolean forceRollback);

}
  • 在执行器中定义的接口包含事务相干的解决办法和执行 SQL 查问的操作,随着后续性能的迭代还会持续补充其余的办法。

2.2 BaseExecutor 形象基类

源码详见cn.bugstack.mybatis.executor.BaseExecutor

public abstract class BaseExecutor implements Executor {

    protected Configuration configuration;
    protected Transaction transaction;
    protected Executor wrapper;

    private boolean closed;

    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.wrapper = this;
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql) {if (closed) {throw new RuntimeException("Executor was closed.");
        }
        return doQuery(ms, parameter, resultHandler, boundSql);
    }

    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql);

    @Override
    public void commit(boolean required) throws SQLException {if (closed) {throw new RuntimeException("Cannot commit, transaction is already closed");
        }
        if (required) {transaction.commit();
        }
    }

}
  • 在形象基类中封装了执行器的全副接口,这样具体的子类继承抽象类后,就不必在解决这些共性的办法。与此同时在 query 查询方法中,封装一些必要的流程解决,如果检测敞开等,在 Mybatis 源码中还有一些缓存的操作,这里临时剔除掉,以外围流程为主。读者搭档在学习的过程中能够与源码进行对照学习。

2.3 SimpleExecutor 简略执行器实现

源码详见cn.bugstack.mybatis.executor.SimpleExecutor

public class SimpleExecutor extends BaseExecutor {public SimpleExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);
    }

    @Override
    protected <E> List<E> doQuery(MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql) {
        try {Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, resultHandler, boundSql);
            Connection connection = transaction.getConnection();
            Statement stmt = handler.prepare(connection);
            handler.parameterize(stmt);
            return handler.query(stmt, resultHandler);
        } catch (SQLException e) {e.printStackTrace();
            return null;
        }
    }

}
  • 简略执行器 SimpleExecutor 继承形象基类,实现形象办法 doQuery,在这个办法中包装数据源的获取、语句处理器的创立,以及对 Statement 的实例化和相干参数设置。最初执行 SQL 的解决和后果的返回操作。
  • 对于 StatementHandler 语句处理器的实现,接下来介绍。

3. 语句处理器

语句处理器是 SQL 执行器中依赖的局部,SQL 执行器封装事务、连贯和检测环境等,而语句处理器则是筹备语句、参数化传递、执行 SQL、封装后果的解决。

3.1 StatementHandler

源码详见cn.bugstack.mybatis.executor.statement.StatementHandler

public interface StatementHandler {

    /** 筹备语句 */
    Statement prepare(Connection connection) throws SQLException;

    /** 参数化 */
    void parameterize(Statement statement) throws SQLException;

    /** 执行查问 */
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

}
  • 语句处理器的外围包含了;筹备语句、参数化传递参数、执行查问的操作,这里对应的 Mybatis 源码中还包含了 update、批处理、获取参数处理器等。

3.2 BaseStatementHandler 形象基类

源码详见cn.bugstack.mybatis.executor.statement.BaseStatementHandler

public abstract class BaseStatementHandler implements StatementHandler {

    protected final Configuration configuration;
    protected final Executor executor;
    protected final MappedStatement mappedStatement;

    protected final Object parameterObject;
    protected final ResultSetHandler resultSetHandler;

    protected BoundSql boundSql;

    public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, ResultHandler resultHandler, BoundSql boundSql) {this.configuration = mappedStatement.getConfiguration();
        this.executor = executor;
        this.mappedStatement = mappedStatement;
        this.boundSql = boundSql;
                
                // 参数和后果集
        this.parameterObject = parameterObject;
        this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, boundSql);
    }

    @Override
    public Statement prepare(Connection connection) throws SQLException {
        Statement statement = null;
        try {
            // 实例化 Statement
            statement = instantiateStatement(connection);
            // 参数设置,能够被抽取,提供配置
            statement.setQueryTimeout(350);
            statement.setFetchSize(10000);
            return statement;
        } catch (Exception e) {throw new RuntimeException("Error preparing statement.  Cause:" + e, e);
        }
    }

    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

}
  • 在语句处理器基类中,将参数信息、后果信息进行封装解决。不过临时这里咱们还不会做过多的参数解决,包含 JDBC 字段类型转换等。这部分内容随着咱们整个执行器的构造建设结束后,再进行迭代开发。
  • 之后是对 BaseStatementHandler#prepare 办法的解决,包含定义实例化形象办法,这个办法交由各个具体的实现子类进行解决。包含;SimpleStatementHandler 简略语句处理器和 PreparedStatementHandler 预处理语句处理器。

    • 简略语句处理器只是对 SQL 的最根本执行,没有参数的设置。
    • 预处理语句处理器则是咱们在 JDBC 中应用的最多的操作形式,PreparedStatement 设置 SQL,传递参数的设置过程。

3.3 PreparedStatementHandler 预处理语句处理器

源码详见cn.bugstack.mybatis.executor.statement.PreparedStatementHandler

public class PreparedStatementHandler extends BaseStatementHandler{

    @Override
    protected Statement instantiateStatement(Connection connection) throws SQLException {String sql = boundSql.getSql();
        return connection.prepareStatement(sql);
    }

    @Override
    public void parameterize(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
        ps.setLong(1, Long.parseLong(((Object[]) parameterObject)[0].toString()));
    }

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

}
  • 在预处理语句处理器中包含 instantiateStatement 预处理 SQL、parameterize 设置参数,以及 query 查问的执行的操作。
  • 这里须要留神 parameterize 设置参数中还是写死的解决,后续这部分再进行欠缺。
  • query 办法则是执行查问和对后果的封装,后果的封装目前也是比较简单的解决,只是把咱们后面章节中对象的内容摘取进去进行封装,这部分临时没有扭转。都放在后续进行欠缺解决。

4. 执行器创立和应用

执行器开发实现当前,则须要在串联到 DefaultSqlSession 中进行应用,那么这个串联过程就须要在 创立 DefaultSqlSession 的时候,构建出执行器并作为参数传递进去。那么这块就波及到 DefaultSqlSessionFactory#openSession 的解决。

4.1 开启执行器

源码详见cn.bugstack.mybatis.session.defaults.DefaultSqlSessionFactory

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}

    @Override
    public SqlSession openSession() {
        Transaction tx = null;
        try {final Environment environment = configuration.getEnvironment();
            TransactionFactory transactionFactory = environment.getTransactionFactory();
            tx = transactionFactory.newTransaction(configuration.getEnvironment().getDataSource(), TransactionIsolationLevel.READ_COMMITTED, false);
            // 创立执行器
            final Executor executor = configuration.newExecutor(tx);
            // 创立 DefaultSqlSession
            return new DefaultSqlSession(configuration, executor);
        } catch (Exception e) {
            try {
                assert tx != null;
                tx.close();} catch (SQLException ignore) { }
            throw new RuntimeException("Error opening session.  Cause:" + e);
        }
    }

}
  • 在 openSession 中开启事务传递给执行器的创立,对于执行器的创立具体能够参考 configuration.newExecutor 代码,这部分没有太多简单的逻辑。读者能够参考源码进行学习。
  • 在执行器创立结束后,则是把参数传递给 DefaultSqlSession,这样就把整个过程串联起来了。

4.2 应用执行器

源码详见cn.bugstack.mybatis.session.defaults.DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {MappedStatement ms = configuration.getMappedStatement(statement);
        List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getBoundSql());
        return list.get(0);
    }

}
  • 好了,通过下面执行器的所有实现实现后,接下来就是解耦后的调用了。在 DefaultSqlSession#selectOne 中获取 MappedStatement 映射语句类后,则传递给执行器进行解决,那么当初这个类通过设计思维的解耦后,就变得更加连忙整洁了,也就是易于保护和扩大了。

五、测试

1. 当时筹备

1.1 创立库表

创立一个数据库名称为 mybatis 并在库中创立表 user 以及增加测试数据,如下:

CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID',
        userId VARCHAR(9) COMMENT '用户 ID',
        userHead VARCHAR(16) COMMENT '用户头像',
        createTime TIMESTAMP NULL COMMENT '创立工夫',
        updateTime TIMESTAMP NULL COMMENT '更新工夫',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');    

1.2 配置数据源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
  • 通过 mybatis-config-datasource.xml 配置数据源信息,包含:driver、url、username、password
  • 在这里 dataSource 能够按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。

1.3 配置 Mapper

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>
  • 这部分临时不须要调整,目前还只是一个入参的类型的参数,后续咱们全副欠缺这部分内容当前,则再提供更多的其余参数进行验证。

2. 单元测试

@Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 从 SqlSessionFactory 中获取 SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
   
    // 2. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 测试验证
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试后果:{}", JSON.toJSONString(user));
}
  • 在单元测试中没有什么变动,只是咱们仍旧是传递一个 1L 的 long 类型参数,进行办法的调用解决。通过单元测试验证执行器的处理过程,读者在学习的过程中能够进行断点测试,学习每个过程的解决内容。

测试后果

22:16:25.770 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
22:16:26.076 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 540642172.
22:16:26.198 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试后果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}

Process finished with exit code 0

  • 从测试后果看咱们曾经能够把 DefaultSqlSession#selectOne 中的调用,换成执行器实现整个过程的解决了,解耦了这部分的逻辑操作,也能不便咱们后续的扩大。

六、总结

  • 整个章节的实现都是在处了解耦这件事件,从 DefaultSqlSession#selectOne 对数据源的处了解耦到执行器中进行操作。而执行器中又包含了对 JDBC 解决的拆解,链接、筹备语句、封装参数、处理结果,所有的这些过程通过解耦后的类和办法,就都能够在当前的性能迭代中十分不便的实现扩大了。
  • 本章节也为咱们后续扩大参数的解决、后果集的封装预留出了扩大点,以及对于不同的语句处理器抉择的问题,都须要在后续进行欠缺和补充。目前咱们串联进去的是最外围的骨架构造,随着后续的渐进式开发陆续迭代欠缺。
  • 对于源码的学习,读者要经验看、写、思考、利用等几个步骤的过程,能力更好的排汇这外面的思维,不只是照着 CP 一遍就完事了,否则也就失去了跟着学习源码的意义。

七、系列举荐

  • 《Spring 手撸专栏》第 1 章:开篇介绍,我要带你撸 Spring 啦!
  • 基于 JavaAgent 的全链路监控
  • 方案设计:基于 IDEA 插件开发和字节码插桩技术,实现研发交付品质主动剖析
  • 面经手册 · 开篇《面试官都问我啥》
  • Lottery 抽奖零碎 – 基于畛域驱动设计的四层架构实际
退出移动版