《手寫Mybatis》第5章:資料來源的解析、建立和使用

小傅哥發表於2022-04-18

作者:小傅哥
<br/>部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

管你吃幾碗粉,有流量就行!

現在我們每天所接收的資訊量越來越多,但很多的個人卻沒有多少分辨知識的能力。很多知識資訊也只是蹭熱點的泛知識,但泛知識只是一種空泛、不成系統、甚至可能是錯誤的資訊群,不過就是這樣的資訊卻給內容消費者一種“成功獲取了知識”吃飽的幻覺,卻喪失了對知識層次的把控。

而作為一個本身就很理科的程式設計師來說,如果都是被泛知識充斥,花費著自己的精力和時間,沒有經過足夠的腦力思考所吸收的泛技術內容,長期以往是很難有所成長的。

以為我個人的成長經歷來看,我更願意花很多的實際來解決一個問題,而不是一片問題。當一個問題解決的足夠透徹、清晰、明確以後,再結合著這個知識點所需要的內容繼續擴充套件和深挖。很慶幸當年沒有那麼多的泛知識內容推送,否則可能我也會被弄的很焦慮!

二、目標

在上一章節我們解析 XML 中的 SQL 配置資訊,並在代理物件呼叫 DefaultSqlSession 中進行獲取和列印操作,從整個框架結構來看我們解決了物件的代理、Mapper的對映、SQL的初步解析,那麼接下來就應該是連庫和執行SQL語句並返回結果了。

那麼這部分內容就會涉及到解析 XML 中關於 dataSource 資料來源資訊配置,並建立事務管理和連線池的啟動和使用。並將這部分能力在 DefaultSqlSession 執行 SQL 語句時進行呼叫。但為了不至於在一個章節把整個工程撐大,這裡我們會把重點放到解析配置、建立事務框架和引入 DRUID 連線池,以及初步完成 SQL 的執行和結果簡單包裝上。便於讀者先熟悉整個框架結構,在後續章節再陸續迭代和完善框架細節。

三、設計

建立資料來源連線池和 JDBC 事務工廠操作,並以 xml 配置資料來源資訊為入口,在 XMLConfigBuilder 中新增資料來源解析和構建操作,向配置類configuration新增 JDBC 操作環境資訊。以便在 DefaultSqlSession 完成對 JDBC 執行 SQL 的操作。

圖 5-1 資料來源的解析和使用

  • 在 parse 中解析 XML DB 連結配置資訊,並完成事務工廠和連線池的註冊環境到配置類的操作。
  • 與上一章節改造 selectOne 方法的處理,不再是列印 SQL 語句,而是把 SQL 語句放到 DB 連線池中進行執行,以及完成簡單的結果封裝。

四、實現

1. 工程結構

mybatis-step-04
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           │   ├── xml
    │           │   │   └── XMLConfigBuilder.java
    │           │   └── BaseBuilder.java
    │           ├── datasource
    │           │   ├── druid
    │           │   │   └── DruidDataSourceFactory.java
    │           │   └── DataSourceFactory.java
    │           ├── io
    │           │   └── Resources.java
    │           ├── mapping
    │           │   ├── BoundSql.java
    │           │   ├── Environment.java
    │           │   ├── MappedStatement.java
    │           │   ├── ParameterMapping.java
    │           │   └── SqlCommandType.java
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java  
    │           ├── transaction
    │           │   ├── jdbc
    │           │   │   ├── JdbcTransaction.java
    │           │   │   └── JdbcTransactionFactory.java
    │           │   ├── Transaction.java
    │           │   └── TransactionFactory.java
    │           └── type
    │               ├── JdbcType.java
    │               └── TypeAliasRegistry.java
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

原始碼地址:https://t.zsxq.com/bmqNFQ7

資料來源的解析和使用核心類關係,如圖 5-2 所示

圖 5-2 資料來源的解析和使用核心類關係

  • 以事務介面 Transaction 和事務工廠 TransactionFactory 的實現,包裝資料來源 DruidDataSourceFactory 的功能。這裡的資料來源連線池我們採用的是阿里的 Druid,暫時還沒有實現 Mybatis 的 JNDI 和 Pooled 連線池,這部分可以後續專門以資料來源連線池的專項來開發。
  • 當所有的資料來源相關功能準備好後,就是在 XMLConfigBuilder 解析 XML 配置操作中,對資料來源的配置進行解析以及建立出相應的服務,存放到 Configuration 的環境配置中。
  • 最後在 DefaultSqlSession#selectOne 方法中完成 SQL 的執行和結果封裝,最終就把整個 Mybatis 核心脈絡串聯出來了。

2. 事務管理

一次資料庫的操作應該具有事務管理能力,而不是通過 JDBC 獲取連結後直接執行即可。還應該把控連結、提交、回滾和關閉的操作處理。所以這裡我們結合 JDBC 的能力封裝事務管理。

2.1 事務介面

詳見原始碼cn.bugstack.mybatis.transaction.Transaction

public interface Transaction {

    Connection getConnection() throws SQLException;

    void commit() throws SQLException;

    void rollback() throws SQLException;

    void close() throws SQLException;

}
  • 定義標準的事務介面,連結、提交、回滾、關閉,具體可以由不同的事務方式進行實現,包括:JDBC和託管事務,託管事務是交給 Spring 這樣的容器來管理。

詳見原始碼cn.bugstack.mybatis.transaction.jdbc.JdbcTransaction

public class JdbcTransaction implements Transaction {

    protected Connection connection;
    protected DataSource dataSource;
    protected TransactionIsolationLevel level = TransactionIsolationLevel.NONE;
    protected boolean autoCommit;

    public JdbcTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        this.dataSource = dataSource;
        this.level = level;
        this.autoCommit = autoCommit;
    }

    @Override
    public Connection getConnection() throws SQLException {
        connection = dataSource.getConnection();
        connection.setTransactionIsolation(level.getLevel());
        connection.setAutoCommit(autoCommit);
        return connection;
    }

    @Override
    public void commit() throws SQLException {
        if (connection != null && !connection.getAutoCommit()) {
            connection.commit();
        }
    }
    
    //...

}
  • 在 JDBC 事務實現類中,封裝了獲取連結、提交事務等操作,其實使用的也就是 JDBC 本身提供的能力。

2.2 事務工廠

詳見原始碼cn.bugstack.mybatis.transaction.TransactionFactory

public interface TransactionFactory {

    /**
     * 根據 Connection 建立 Transaction
     * @param conn Existing database connection
     * @return Transaction
     */
    Transaction newTransaction(Connection conn);

    /**
     * 根據資料來源和事務隔離級別建立 Transaction
     * @param dataSource DataSource to take the connection from
     * @param level Desired isolation level
     * @param autoCommit Desired autocommit
     * @return Transaction
     */
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

}
  • 以工廠方法模式包裝 JDBC 事務實現,為每一個事務實現都提供一個對應的工廠。與簡單工廠的介面包裝不同。

3. 型別別名註冊器

在 Mybatis 框架中我們所需要的基本型別、陣列型別以及自己定義的事務實現和事務工廠都需要註冊到型別別名的註冊器中進行管理,在我們需要使用的時候可以從註冊器中獲取到具體的物件型別,之後在進行例項化的方式進行使用。

3.1 基礎註冊器

詳見原始碼cn.bugstack.mybatis.type.TypeAliasRegistry

public class TypeAliasRegistry {

    private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();

    public TypeAliasRegistry() {
        // 建構函式裡註冊系統內建的型別別名
        registerAlias("string", String.class);

        // 基本包裝型別
        registerAlias("byte", Byte.class);
        registerAlias("long", Long.class);
        registerAlias("short", Short.class);
        registerAlias("int", Integer.class);
        registerAlias("integer", Integer.class);
        registerAlias("double", Double.class);
        registerAlias("float", Float.class);
        registerAlias("boolean", Boolean.class);
    }

    public void registerAlias(String alias, Class<?> value) {
        String key = alias.toLowerCase(Locale.ENGLISH);
        TYPE_ALIASES.put(key, value);
    }

    public <T> Class<T> resolveAlias(String string) {
        String key = string.toLowerCase(Locale.ENGLISH);
        return (Class<T>) TYPE_ALIASES.get(key);
    }

}
  • 在 TypeAliasRegistry 型別別名註冊器中先做了一些基本的型別註冊,以及提供 registerAlias 註冊方法和 resolveAlias 獲取方法。

3.2 註冊事務

詳見原始碼cn.bugstack.mybatis.session.Configuration

public class Configuration {

    //環境
    protected Environment environment;

    // 對映序號產生器
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);

    // 對映的語句,存在Map裡
    protected final Map<String, MappedStatement> mappedStatements = new HashMap<>();

    // 型別別名序號產生器
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
        typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
    }
    
    //...
}
  • 在 Configuration 配置選項類中,新增型別別名序號產生器,通過建構函式新增 JDBC、DRUID 註冊操作。
  • 讀者應該注意到,整個 Mybatis 的操作都是使用 Configuration 配置項進行串聯流程,所以所有內容都會在 Configuration 中進行連結。

4. 解析資料來源配置

通過在 XML 解析器 XMLConfigBuilder 中,擴充套件對環境資訊的解析,我們這裡把資料來源、事務類內容稱為操作 SQL 的環境。解析後把配置資訊寫入到 Configuration 配置項中,便於後續使用。

詳見原始碼cn.bugstack.mybatis.builder.xml.XMLConfigBuilder

public class XMLConfigBuilder extends BaseBuilder {
         
  public Configuration parse() {
      try {
          // 環境
          environmentsElement(root.element("environments"));
          // 解析對映器
          mapperElement(root.element("mappers"));
      } catch (Exception e) {
          throw new RuntimeException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
      }
      return configuration;
  }
    
  private void environmentsElement(Element context) throws Exception {
      String environment = context.attributeValue("default");
      List<Element> environmentList = context.elements("environment");
      for (Element e : environmentList) {
          String id = e.attributeValue("id");
          if (environment.equals(id)) {
              // 事務管理器
              TransactionFactory txFactory = (TransactionFactory) typeAliasRegistry.resolveAlias(e.element("transactionManager").attributeValue("type")).newInstance();
              // 資料來源
              Element dataSourceElement = e.element("dataSource");
              DataSourceFactory dataSourceFactory = (DataSourceFactory) typeAliasRegistry.resolveAlias(dataSourceElement.attributeValue("type")).newInstance();
              List<Element> propertyList = dataSourceElement.elements("property");
              Properties props = new Properties();
              for (Element property : propertyList) {
                  props.setProperty(property.attributeValue("name"), property.attributeValue("value"));
              }
              dataSourceFactory.setProperties(props);
              DataSource dataSource = dataSourceFactory.getDataSource();
              // 構建環境
              Environment.Builder environmentBuilder = new Environment.Builder(id)
                      .transactionFactory(txFactory)
                      .dataSource(dataSource);
              configuration.setEnvironment(environmentBuilder.build());
          }
      }
  }

}
  • 以 XMLConfigBuilder#parse 解析擴充套件對資料來源解析操作,在 environmentsElement 方法中包括事務管理器解析和從型別註冊器中讀取到事務工程的實現類,同理資料來源也是從型別註冊器中獲取。
  • 最後把事務管理器和資料來源的處理,通過環境構建 Environment.Builder 存放到 Configuration 配置項中,也就可以通過 Configuration 存在的地方都可以獲取到資料來源了。

5. SQL執行和結果封裝

在上一章節中在 DefaultSqlSession#selectOne 只是列印了 XML 中配置的 SQL 語句,現在把資料來源的配置載入進來以後,就可以把 SQL 語句放到資料來源中進行執行以及結果封裝。

詳見原始碼cn.bugstack.mybatis.session.defaults.DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

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

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        try {
            MappedStatement mappedStatement = configuration.getMappedStatement(statement);
            Environment environment = configuration.getEnvironment();

            Connection connection = environment.getDataSource().getConnection();

            BoundSql boundSql = mappedStatement.getBoundSql();
            PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
            preparedStatement.setLong(1, Long.parseLong(((Object[]) parameter)[0].toString()));
            ResultSet resultSet = preparedStatement.executeQuery();

            List<T> objList = resultSet2Obj(resultSet, Class.forName(boundSql.getResultType()));
            return objList.get(0);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // ...

}
  • 在 selectOne 方法中獲取 Connection 資料來源連結,並簡單的執行 SQL 語句,並對執行的結果進行封裝處理。
  • 因為目前這部分主要是為了大家串聯出整個功能結構,所以關於 SQL 的執行、引數傳遞和結果封裝都是寫死的,後續我們進行擴充套件。

六、測試

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', '小傅哥');    

2. 配置資料來源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="DRUID">
            <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,因為我們實現的是這個資料來源的處理方式。

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>
  • Mapper 的配置內容在上一章節的解析學習中已經做了配置,本章節做了簡單的調整。

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));
}
  • 單元測試沒有什麼改變,仍是通過 SqlSessionFactory 中獲取 SqlSession 並獲得對映物件和執行方法呼叫。

測試結果

22:34:18.676 [main] INFO  c.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
22:34:19.286 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}

Process finished with exit code 0
  • 從現在的測試結果已經可以看出,通過我們對資料來源的解析、包裝和使用,已經可以對 SQL 語句進行執行和包裝返回的結果資訊了。
  • 讀者在學習的過程中可以除錯下程式碼,看看每一步都是如何完成執行步驟的,也在這個過程中進行學習 Mybatis 框架的設計技巧。

七、總結

  • 以解析 XML 配置解析為入口,新增資料來源的整合和包裝,引出事務工廠對 JDBC 事務的處理,並載入到環境配置中進行使用。
  • 那麼通過資料來源的引入就可以在 DefaultSqlSession 中從 Configuration 配置引入環境資訊,把對應的 SQL 語句提交給 JDBC 進行處理並簡單封裝結果資料。
  • 結合本章節建立起來的框架結構,資料來源、事務、簡單的SQL呼叫,下個章節將繼續這部分內容的擴充套件處理,讓整個功能模組逐漸完善。

相關文章