更便捷的Mybatis增強外掛——EasyMybatis

左羽發表於2022-01-05

easy-mybatis是一個對Mybatis的增強框架(外掛)。在Spring整合Mybatis的基礎上,將專案開發中對資料庫的常用操作統一化。使用本框架可以很便捷的對資料庫進行操作,提高開發效率,減少機械行為。

專案主頁https://mybatis.zuoyu.top

API文件地址https://mybatis.zuoyu.top/doc/index.html

GitHub地址https://github.com/zuoyuip/easy-mybatis

Gitee地址https://gitee.com/zuoyuip/easy-mybatis

Maven依賴引入


<!-- https://mvnrepository.com/artifact/top.zuoyu.mybatis/easy-mybatis-spring-boot-starter -->
<dependency>
    <groupId>top.zuoyu.mybatis</groupId>
    <artifactId>easy-mybatis-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle依賴引入


// https://mvnrepository.com/artifact/top.zuoyu.mybatis/easy-mybatis-spring-boot-starter
implementation 'top.zuoyu.mybatis:easy-mybatis-spring-boot-starter:1.0.0'


框架初衷

這個框架的初衷是,減少Java程式設計師千篇一律的資料庫操作。

對於開發人員來說:

  • 精力應該花費在業務邏輯上,而非機械式的“技術”上。
  • 專案中減少無關痛癢的程式碼,從抽象的角度看實現。
  • 各司其職,各勞其力,追求專案角度的服務流水線。

服務分離的時代

如今已很難看到單體架構的專案(感興趣的可以檢視我對架構演變的描述《淺談微服務》),目前的專案大都是通過RESTfulMQSocket的方式(協議)進行資料傳輸。

這讓我開始質疑傳統JavaWeb專案中的資料庫操作模式——即Model(DTO)存在的意義。理論上,資料庫設計是不可能完全遵循檢視模型的,這就導致“正確”的做法是在專案中引入VO,由多個DTO來組裝。

那麼,為什麼不能用靈活的Map來替代呢?

對一個Map的方法進行擴充,增加其對Json的解析能力,那麼是不是就可以擺脫POJO的各種麻煩組裝。

思考框架設計

我在思考如何設計這個框架的時候,被需要考慮的方方面面給阻擋住了。

因為一個資料庫框架需要考慮的東西實在太多了,比如:

  1. 事務機制
  2. 型別轉換
  3. 會話管理

···

思來想去,發現自己方向跑偏了,我只是希望統一資料庫操作的介面 + 擺脫Model,沒必要重新平地起牆,完全可以在一個現有的框架基礎上進行封裝。

那麼,對這個現有框架的選擇就尤為重要了。

現有框架的選擇

目前Java中主流的資料庫操作框架:

  • Spring JDBC
  • Spring Data JPA
  • Mybatis
  • Hibernate

選擇現有框架有一個原則——“統一資料庫操作的介面 + 擺脫Model”是對該框架的加強,而非變異;不能因為“統一資料庫操作的介面 + 擺脫Model”而無法使用原框架的部分功能。

擺脫Model”這個特點,首先就要排除重度ORM框架,也就是支援JPA操作的資料庫——Spring Data JPAHibernate;原因很簡單,這兩個框架的強大之處恰恰就在它完全面向Model操作。

剩下的就只有兩個框架了,Spring JDBCMybatis。其中,Spring JDBC留給了開發人員大量的可操作空間,更加自由,但恰恰是這種自由使得它更加繁瑣。而Mybatis是一個輕量ORM框架,準確來說Mybatis不能稱為ORM框架,因為它並不是面向Model運算元據庫,僅僅是將資料庫欄位與Model欄位互相賦值,並沒有做到ORM定義的關係對映。

抉擇

由以上各框架的特點,結合國內Java語言中資料庫操作框架的熱度,毫無疑問的選擇了Mybatis

考慮到SpringBootMybatis優秀的支援級別,我決定基於mybatis-spring-boot-starter開發這款框架,準備來說應該稱其為“外掛”。

框架特性

  • 更便捷

摒棄傳統mybatismodelxmldao資料庫操作模式,避繁就簡,快速開發。

  • 更高效

採用預編譯SQL,拒絕執行期間反射生成SQL,效能更高效。

  • 無侵入

只是對Mybatis-Spring的增強外掛,對已有工程不做任何修改,仍可使用原生框架的功能,僅僅是簡化了開發階段對資料庫的操作。

  • 統一操作介面

對資料庫的所有操作共用一個介面,降低使用門檻,輕鬆運算元據庫。

  • 統一操作物件

使用JsonObject為資料物件,提供一系列操作方法,方便從持久化物件組裝為檢視物件。

  • 易上手

整個框架只提供了一個介面、一個註解、兩個物件,僅僅一行配置便可完成對資料庫進行常用操作。

  • ...

安利

  • mybatis-spring-boot環境下,使用該框架(外掛),可以減少傳統Mybatis使用中對modelxmldao的機械式開發。

  • 所有的資料庫操作均使用MapperRepository介面,通過註解@Magic("xxx")標記介面的資料表歸屬,即可直接使用。

  • 該框架(外掛)不妨礙同時使用傳統Mybatismodelxmldao的資料庫開發方式。

快速上手

安裝

  • 安裝mybatis-spring-boot環境

mybatis-spring-boot的Maven依賴

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>${spring-boot.version}</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot.version}</version>
        </dependency>
    </dependencies>

mybatis-spring-boot的Gradle依賴

    plugins {
    id 'org.springframework.boot' version '${springBootVersion}'
    id 'io.spring.dependency-management' version '${springManagementVersion}'
    id 'java'
    }

    dependencies {
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisSpringVersion}'
    }
  • 安裝本框架(外掛)

Maven依賴引入


<!-- https://mvnrepository.com/artifact/top.zuoyu.mybatis/easy-mybatis-spring-boot-starter -->
<dependency>
    <groupId>top.zuoyu.mybatis</groupId>
    <artifactId>easy-mybatis-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle依賴引入


// https://mvnrepository.com/artifact/top.zuoyu.mybatis/easy-mybatis-spring-boot-starter
implementation 'top.zuoyu.mybatis:easy-mybatis-spring-boot-starter:1.0.0'

配置

這裡以MySQL資料庫為例,Oracle資料庫配置請參考配置說明

  1. 配置spring-boot-jdbc資料庫

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://172.0.0.1:3306/xxxx
    username: xxxx
    password: xxxx

關於springBoot的配置,這裡不多贅述,更多移步springBoot官網

  1. 配置easy-mybatis支援的表名(例子)

easy-mybatis:
  table-names: teacher, student

這裡的table-names配置,表示需要easy-mybatis框架支援的資料表名,多個表名使用逗號隔開。

即可使用easy-mybatis框架操作teacherstudent兩個資料表,如果需要支援其他資料表,需要在此配置

運算元據庫(例子)


@SpringBootTest
class DemoApplicationTests {

    // 表示該介面用來操作名稱為'teacher'的資料表
    @Magic("teacher")
    private MapperRepository teacherRepository;

    // 表示該介面用來操作名稱為'student'的資料表
    @Magic("student")
    private MapperRepository studentRepository;


    // 查詢teacher表下所有資料
    @Test
    void teacherTest() {
        teachertRepository.selectList().forEach(System.out::println);
    }

    // 查詢student表下符合特定條件的資料
    @Test
    void studentTest() {
        studentRepository.selectListByExample(
          new JsonObject().put("birthday", "2009/12/12 12:12:12")
          ).forEach(System.out::println);
    }

}

使用MapperRepository介面對資料庫進行操作,需要使用@Magic("表名稱")標記該介面的資料表歸屬。

在本例中,@Magic("teacher")表示該MapperRepository"teacher"資料表的操作介面,可以通過teacherRepository呼叫一系列方法完成對"teacher"資料表的操作。

配置說明

該框架(外掛)的全部相關配置


#easy-mybatis的相關配置
easy-mybatis:
  #表示需要載入的資料庫表
  table-names: SYS_USER, SYS_DEPT
  #預設的日期字串格式
  data-format: yyyy-MM-dd HH:mm:ss
  #當資料庫為Oracle時,資料表對應的主鍵SEQ名稱
  sequences:
    SYS_USER: SEQ_SYS_USER,
    SYS_DEPT: SEQ_SYS_DEPT
  #當資料庫為Oracle時,資料表的日期型別欄位格式
  oracle-date-format: yyyy-mm-dd hh24:mi:ss

詳細說明

  1. table-names

此配置表示需要載入的資料表,多個表名由逗號隔開,只有在此處配置了的資料表,才能在專案中使用@Magic("表名稱")標記MapperRepository介面的資料表歸屬。

  1. data-format

此配置表示在JsonObject中的日期存在格式,JsonObject中對日期的儲存型別為String型別,可通過此配置自定義儲存格式,預設格式為yyyy-MM-dd HH:mm:ss

注意
data-format的格式修改,請務必將oracle-date-format的格式與之匹配。

  1. sequences

此配置表示當資料庫為Oracle時,每個資料表的主鍵生成器名稱,以鍵值對的格式進行引數配置。若此處沒有對資料表的主鍵生成器進行配置,那麼在做該資料表的資料插入行為時需自定義填充主鍵數值。

  1. oracle-date-format

此配置表示當資料庫為Oracle時,傳入日期格式欄位時的引數格式,即TO_DATE(field, '$dateFormat')

注意
oracle-date-format的格式修改,請務必將data-format的格式與之匹配。

操作介面約定

本框架(外掛)的資料庫操作唯一介面——top.zuoyu.mybatis.service.MapperRepository

  1. ByExample為結尾的方法

此類是根據JsonObject型別的引數example中鍵值對,對符合要求的資料進行操作,例如:


    // 構建example,這裡指欄位"birthday"為"2009/12/12 12:12:12"
    JsonObject example = new JsonObject().put("birthday", "2009/12/12 12:12:12");
    // 查詢所有欄位"birthday"為"2009/12/12 12:12:12"的學生資料
    List<JsonObject> studentList = studentRepository.selectListByExample(example);

這裡的selectListByExample方法就是根據引數example中鍵值對查詢符合要求的資料。
同理, countByExample方法就是獲取符合引數example中鍵值對的資料總量...

  1. By為結尾的方法

此類是根據String型別的引數suffixSql對方法追加引數中SQL語句的內容,例如:


    // 查詢"is_delete"欄位為0的資料,並以年齡進行排序
    List<JsonObject> teachers = teacherRepository.selectListBy("WHERE is_delete = 0 ORDER BY age");

    // 將"birthday"欄位大於"1990/1/1 00:00:00"的資料,"salary"欄位改為20000。 (也就是出生日期在1990年之前的老師,工資改為2W)
    JsonObject example = new JsonObject().put("salary", 20000);
    int count = teacherRepository.updateBy(example, "WHERE birthday < '1990/1/1 00:00:00'");

這裡的selectListBy方法就是查詢出符合引數中SQL語句要求的內容,updateBy方法就是將符合引數中SQL語句要求的資料修改為example定義的數值。

同理,countBy方法就是獲取符合引數中SQL語句要求的資料總量...

  1. insertinsertBatch方法

當資料庫為MySQL時,如果資料表主鍵自增,則無需定義主鍵數值;若主鍵為非自增,則需定義主鍵數值。

當資料庫為Oracle時,如果在配置中定義了資料表的主鍵生成器,則無需定義主鍵數值;反之,則需定義主鍵數值。

  • MapperRepository提供了豐富的方法,以方便對資料庫的CURD操作,更多關於MapperRepository的使用說明請移步MapperRepository說明MapperRepository的API文件

操作物件約定

資料庫介面的操作物件主要為JsonObject型別和JsonArray型別。

  1. 其中top.zuoyu.mybatis.json.JsonObject為鍵值對容器,以鍵值的形式裝載,資料結構為:

{
    'field1' : 'value1',
    'field2' : 'value2',
    'field3' : 'value3',
    ...
}

鍵的型別為java.lang.String型別,值的型別可以為任何型別。

JsonObject提供了豐富的方法,以方便操作鍵值對和型別轉換,更多關於JsonObject的使用說明請移步JsonObject說明JsonObject的API文件

  1. 其中top.zuoyu.mybatis.json.JsonArray為陣列容器,以多個單位的形式裝載,資料結構為:

[
    object1,
    object2,
    object3,
    ...
]

每個單位可以為任何型別,且各單位型別允許與其他單位型別不同。

JsonArray提供了豐富的方法,以方便操作鍵值對和型別轉換,更多關於JsonArray的使用說明請移步JsonArray說明JsonArray的API文件


具體方法可參考API文件

MapperRepository說明

查詢所有selectList

    /**
     * 查詢所有
     *
     * @return 所有資料
     */
    List<JsonObject> selectList();

查詢符合樣例的資料selectListByExample

    /**
     * 根據已有鍵值查詢
     *
     * @param example - 已有鍵值
     * @return 符合要求的資料集合
     */
    List<JsonObject> selectListByExample(JsonObject example);

根據主鍵查詢唯一物件selectByPrimaryKey

    /**
     * 根據主鍵查詢唯一物件
     *
     * @param primaryKey - 主鍵
     * @return 唯一物件
     */
    JsonObject selectByPrimaryKey(Serializable primaryKey);

查詢符合條件的資料selectListBy

    /**
     * 查詢符合條件的資料
     *
     * @param suffixSql - 條件語句(例如:where field = xxx)
     * @return 資料數量
     */
    List<JsonObject> selectListBy(String suffixSql);

查詢特定的欄位或結果selectFields

    /**
     * 查詢特定的欄位或結果
     * (例如:"fieldA, fieldB, fieldC")
     * (例如:"COUNT(field)")
     *
     * @param fields - 特定的欄位或結果
     * @return 特定的欄位或結果
     */
    JsonArray selectFields(String fields);

根據樣例查詢特定的欄位或結果selectFieldsByExample

    /**
     * 根據已有鍵值查詢特定的欄位或結果
     * (例如:"fieldA, fieldB, fieldC")
     * (例如:"COUNT(field)")
     *
     * @param fields  - 特定的欄位或結果
     * @param example - 已有鍵值
     * @return 特定的欄位或結果
     */
    JsonArray selectFieldsByExample(String fields, JsonObject example);

根據主鍵查詢特定的欄位或結果selectFieldsByPrimaryKey

    /**
     * 根據主鍵查詢特定的欄位或結果
     *
     * @param fields     - 特定的欄位或結果(例如:"fieldA, fieldB, fieldC")
     * @param primaryKey - 主鍵
     * @return 特定的欄位或結果
     */
    JsonArray selectFieldsByPrimaryKey(String fields, Serializable primaryKey);

根據主鍵集合查詢特定的欄位或結果selectFieldsByPrimaryKeys

    /**
     * 根據主鍵集合查詢特定的欄位或結果
     *
     * @param fields      - 特定的欄位或結果(例如:"fieldA, fieldB, fieldC")
     * @param primaryKeys - 主鍵集合
     * @return 特定的欄位或結果
     */
    JsonArray selectFieldsByPrimaryKeys(String fields, Serializable[] primaryKeys);

查詢符合條件的資料selectFieldsBy

    /**
     * 查詢符合條件的資料
     *
     * @param fields    - 特定的欄位或結果(例如:"fieldA, fieldB, fieldC")
     * @param suffixSql - 條件語句(例如:where field = xxx)
     * @return 特定的欄位或結果
     */
    JsonArray selectFieldsBy(String fields, String suffixSql);

查詢符合條件的資料數量countBy

    /**
     * 查詢符合條件的資料數量
     *
     * @param suffixSql - 條件語句(例如:where field = xxx)
     * @return 資料數量
     */
    long countBy(String suffixSql);

查詢是否存在符合樣例的資料數量countByExample

    /**
     * 根據已有鍵值查詢是否存在符合條件的資料數量
     *
     * @param example - 已有鍵值
     * @return 資料數量
     */
    long countByExample(JsonObject example);

是否存在符合條件的資料existsBy

    /**
     * 是否存在符合條件的資料
     *
     * @param suffixSql - 條件語句(例如:where field = xxx)
     * @return 是否存在
     */
    boolean existsBy(String suffixSql);

是否存在符合樣例的資料existsByExample

    /**
     * 根據已有鍵值查詢是否存在符合條件的資料
     *
     * @param example - 已有鍵值
     * @return 是否存在
     */
    boolean existsByExample(JsonObject example);

新增物件insert

    /**
     * 新增物件
     *
     * @param jsonObject - 物件鍵值
     * @return 變動資料的數量
     */
    int insert(JsonObject jsonObject);

批量新增物件insertBatch

    /**
     * 批量新增物件
     *
     * @param jsonObjects - 物件鍵值集合
     * @return 變動資料的數量
     */
    int insertBatch(List<JsonObject> jsonObjects);

根據主鍵修改物件屬性updateByPrimaryKey

    /**
     * 根據主鍵修改物件屬性
     *
     * @param jsonObject - 包含主鍵物件鍵值
     * @return 變動資料的數量
     */
    int updateByPrimaryKey(JsonObject jsonObject);

修改特定條件的物件屬性updateBy

    /**
     * 修改特定條件的物件屬性
     *
     * @param jsonObject - 要修改的鍵值
     * @param suffixSql  - 條件語句(例如:where field = xxx)
     * @return 變動資料的數量
     */
    int updateBy(JsonObject jsonObject, String suffixSql);

批量根據主鍵修改物件屬性updateByPrimaryKeyBatch

    /**
     * 批量根據主鍵修改物件屬性
     *
     * @param jsonObjects - 物件鍵值集合
     * @return 變動資料的數量
     */
    int updateByPrimaryKeyBatch(List<JsonObject> jsonObjects);

根據主鍵刪除物件deleteByPrimaryKey

    /**
     * 根據主鍵刪除物件
     *
     * @param primaryKey - 主鍵
     * @return 變動資料的數量
     */
    int deleteByPrimaryKey(Serializable primaryKey);

刪除符合條件的資料deleteBy

    /**
     * 刪除符合條件的資料
     *
     * @param suffixSql - 條件語句(例如:where field = xxx)
     * @return 變動資料的數量
     */
    int deleteBy(String suffixSql);

批量根據主鍵刪除物件deleteByPrimaryKeys

    /**
     * 批量根據主鍵刪除物件
     *
     * @param primaryKeys - 主鍵組
     * @return 變動資料的數量
     */
    int deleteByPrimaryKeys(Serializable[] primaryKeys);

根據樣例刪除物件deleteByExample

    /**
     * 根據已有鍵值刪除物件
     *
     * @param example - 已有鍵值
     * @return 變動資料的數量
     */
    int deleteByExample(JsonObject example);

JsonObject說明

top.zuoyu.mybatis.json.JsonObject為鍵值對容器,以鍵值的形式裝載,資料結構為:

{
    'field1' : 'value1',
    'field2' : 'value2',
    'field3' : 'value3',
    ...
}

鍵的型別為java.lang.String型別,值的型別可以為任何型別。

型別轉換

內部預設已包含常規的型別轉換,預設支援以下型別:

  • java.math.BigInteger
  • java.math.BigDecimal
  • java.lang.Boolean
  • java.lang.Byte
  • java.lang.Character
  • java.util.Date
  • java.lang.Double
  • java.lang.Float
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Short
  • java.lang.String

如果預設的轉換器無法滿足需求,或者需要轉換為其他型別,可通過自定義轉換器ConvertClass<T>進行實現,介面方法:

public interface ConvertClass<T> {

    /**
     * 將給定的 value 物件轉換為 T 型別
     *
     * @param value - 給定物件
     * @return 目標轉換型別的物件
     */
    T convert(Object value);

    /**
     * 將給定的 value 物件轉換為 T 型別, 否則返回  defaultValue
     *
     * @param value        - 給定物件
     * @param defaultValue - 預設值
     * @return 目標轉換型別的物件
     */
    T convert(Object value, T defaultValue);
}

示例

    @Test
    void convertTest() {
        // 使用JSON字串初始化物件
        JsonObject student = new JsonObject(studentJSON);

        // 獲取學生的性別,如果無法轉換則返回"未知"
        String sex = student.getValue("sex", new SexConvert());

        // 獲取學生的性別,如果無法轉換則返回"未錄入"
        String sexOrDefault = student.getValue("sex", new SexConvert(), "未錄入");
    }

    static class SexConvert implements ConvertClass<String> {

        @Override
        public String convert(Object value) {
            if (value instanceof Integer) {
                if ((Integer) value == 1) {
                    return "男";
                } else if ((Integer)value == 0) {
                    return "女";
                }
            }
            return "未知";
        }

        @Override
        public String convert(Object value, String defaultValue) {
            if (value instanceof Integer) {
                if ((Integer) value == 1) {
                    return "男";
                } else if ((Integer)value == 0) {
                    return "女";
                }
            }
            return defaultValue;
        }
    }

本示例也可使用StringConvert轉換器,StringConvertConvertClass<T>的子介面。

也可以通過toClass方法,將JsonObject物件轉換為指定型別的實體類。

子介面:

  • BigDecimalConvert
  • BigIntegerConvert
  • BooleanConvert
  • ByteConvert
  • CharacterConvert
  • DateConvert
  • DoubleConvert
  • FloatConvert
  • IntegerConvert
  • LongConvert
  • ShortConvert
  • StringConvert

更多方法請移步JsonObject的API文件

JsonArray說明

top.zuoyu.mybatis.json.JsonArray為陣列容器,以多個單位的形式裝載,資料結構為:


[
    object1,
    object2,
    object3,
    ...
]

每個單位可以為任何型別,且各單位型別允許與其他單位型別不同。

型別轉換

具體使用方法與JsonObject在型別轉換操作一致,參考JsonObject的型別轉換操作


更多方法請移步JsonArray的API文件


開源地址

專案主頁https://mybatis.zuoyu.top

API文件地址https://mybatis.zuoyu.top/doc/index.html

GitHub地址https://github.com/zuoyuip/easy-mybatis

Gitee地址https://gitee.com/zuoyuip/easy-mybatis

相關文章