目錄
- 一、前言
- 二、目标
- 三、設計
- 四、實作
- 1. 工程結構
- 2. 執行器的定義和實作
- 3. 語句處理器
- 4. 執行器建立和使用
- 五、測試
- 1. 事先準備
- 2. 單元測試
- 六、總結
一、前言
為什麼,要讀架構源碼?
因為手裡的業務工程代碼太拉胯了!通常作為業務研發,所開發出來的代碼,大部分都是一連串的流程化處理,缺少功能邏輯的解耦,有着疊代頻繁但可疊代性差的特點。是以這樣的代碼通常隻能學習業務邏輯,卻很難吸收到大型系統設計和功能邏輯實作的成功經驗,往往都是失敗的教訓。
而所有系統的設計和實作,核心都在于如何解耦,如果解耦不清晰最後直接導緻的就是再繼續疊代功能時,會讓整個系統的實作越來越臃腫,穩定性越來越差。而關于解耦的實踐在各類架構的源碼中都有非常不錯的設計實作,是以閱讀這部分源碼,就是在吸收成功的經驗。把解耦的思想逐漸運用到實際的業務開發中,才會讓我們寫出更加優秀的代碼結構。
二、目标
在上一章節我們實作了有/無連接配接池的資料源,可以在調用執行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
工程源碼:公衆号「bugstack蟲洞棧」,回複:手寫Mybatis,擷取完整源碼
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 源碼中還有一些緩存的操作,這裡暫時剔除掉,以核心流程為主。讀者夥伴在學習的過程中可以與源碼進行對照學習。
嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和内容,導緻工資要不上去!
無償分享大家一個資料包,差不多150多G。裡面學習内容、面經、項目都比較新也比較全!某魚上買估計至少要好幾十。
點選這裡找小助理0元領取:加微信領取資料
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一遍就完事了,否則也就失去了跟着學習源碼的意義。
- END -
文章連結:https://mp.weixin.qq.com/s/MJYKOn0-jhnGurcbDxgWtA
轉載自:小傅哥 bugstack蟲洞棧
文章來源:《Mybatis 手撸專欄》第7章:SQL執行器的定義和實作
版權申明:本文來源于網絡,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯系我進行删除。