《Mybatis 手擼專欄》第7章:SQL執行器的定義和實現

小傅哥發表於2022-05-05

作者:小傅哥
部落格:https://bugstack.cn - 《手寫Mybatis系列》

一、前言

為什麼,要讀框架原始碼?

因為手裡的業務工程程式碼太拉胯了!通常作為業務研發,所開發出來的程式碼,大部分都是一連串的流程化處理,缺少功能邏輯的解耦,有著迭代頻繁但可迭代性差的特點。所以這樣的程式碼通常只能學習業務邏輯,卻很難吸收到大型系統設計和功能邏輯實現的成功經驗,往往都是失敗的教訓。

而所有系統的設計和實現,核心都在於如何解耦,如果解耦不清晰最後直接導致的就是再繼續迭代功能時,會讓整個系統的實現越來越臃腫,穩定性越來越差。而關於解耦的實踐在各類框架的原始碼中都有非常不錯的設計實現,所以閱讀這部分原始碼,就是在吸收成功的經驗。把解耦的思想逐步運用到實際的業務開發中,才會讓我們寫出更加優秀的程式碼結構。

二、目標

在上一章節我們實現了有/無連線池的資料來源,可以在呼叫執行SQL的時候,通過我們實現池化技術完成資料庫的操作。

那麼關於池化資料來源的呼叫、執行和結果封裝,目前我們還都只是在 DefaultSqlSession 中進行發起 如圖 7-1 所示。那麼這樣的把程式碼流程寫死的方式肯定不合適於我們擴充套件使用,也不利於 SqlSession 中每一個新增定義的方法對池化資料來源的呼叫。

圖 7-1 DefaultSqlSession 呼叫資料來源

  • 解耦 DefaultSqlSession#selectOne 方法中關於對資料來源的呼叫、執行和結果封裝,提供新的功能模組替代這部分硬編碼的邏輯處理。
  • 只有提供了單獨的執行方法入口,我們才能更好的擴充套件和應對這部分內容裡的需求變化,包括了各類入參、結果封裝、執行器型別、批處理等,來滿足不同樣式的使用者需求,也就是配置到 Mapper.xml 中的具體資訊。

三、設計

從我們對 ORM 框架漸進式的開發過程上,可以分出的執行動作包括,解析配置、代理物件、對映方法等,直至我們前面章節對資料來源的包裝和使用,只不過我們把資料來源的操作硬捆綁到了 DefaultSqlSession 的執行方法上了。

那麼現在為了解耦這塊的處理,則需要單獨提出一塊執行器的服務功能,之後將執行器的功能隨著 DefaultSqlSession 建立時傳入執行器功能,之後具體的方法呼叫就可以呼叫執行器來處理了,從而解耦這部分功能模組。如圖 7-2 所示。

圖 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

SQL方法執行器核心類關係,如圖 7-3 所示

圖 7-3 SQL方法執行器核心類關係

  • 以 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一遍就完事了,否則也就失去了跟著學習原始碼的意義。

七、系列推薦

相關文章