Mybatis-flex代替繁瑣的JPA

张学涛發表於2024-10-06

1. 前言

最近在新的SpringBoot專案中採用JPA來作為資料庫的持久層。剛開始得益於Spring框架自帶,IDEA也有豐富的支援;可以自行匹配資料庫欄位,介面中方法可以直接提示,支援JPQL,原生SQL等方式。寫起來也是非常順手。但是當業務中有一些複雜一點的需求,在JPA中實現就非常麻煩,且不直觀。

本文不是批判JPA和Mybatis-plus的不足,也不會來對比他們的寫法優劣。主要是介紹從編碼生產力方面解決我日常寫程式碼的一些問題和帶來哪些便利。

2. 使用痛點

2.1. JpaRepository(優點)

JPA的Mapper操作類是透過繼承JpaRepository來進行CRUD操作,同時有一個非常便捷的操作,我們只需要在介面中透過一定的規則定義方法名稱,我們就可以執行對應的SQL。

interface AppRealtimeRecordRepository: JpaRepository<AppRealtimeRecord, Int> {

    fun findByCardNo(cardNo: String?): AppRealtimeRecord?

    @Query("select count(1) from AppRealtimeRecord arr where " +
            "arr.deviceSn = :#{#orderQueryRequest.deviceSn} AND " +
            "arr.createTime BETWEEN COALESCE(:#{#orderQueryRequest.payStartTime}, arr.createTime) AND " +
            "COALESCE(:#{#orderQueryRequest.payEndTime}, arr.createTime)"
    )
    fun summaryQuery(orderQueryRequest: OrderQueryRequest): Long
}

 

findByCardNo可以直接生成透過卡號查詢實體的語句,非常方便。但是如果我們查詢條件比較多,用這種方式就會生成非常長的方法名稱,這個時候就需要透過@Query進行引數指定查詢。

2.2. 條件查詢

經常有一個需求,需要判斷某一個欄位存在時,在進行SQL查詢。但是這種情況使用JPA會感覺非常麻煩。無論是用JPQL還是原生的SQL感覺很難比較好的實現。

上面提供了一個示例是判斷開始時間和結束時間是否為空,在進行比較判斷。利用的資料庫的判空邏輯來進行實現,感覺非常不直觀,也不利於SQL除錯。

2.2.1. Mybatis-Flex

我們Mybatis-Flex中就非常優雅的可以實現。像寫SQL一樣非常直觀的實現SQL的編寫。

QueryWrapper query = QueryWrapper.create()
        .where(EMPLOYEE.LAST_NAME.like(searchWord)) //條件為null時自動忽略
        .and(EMPLOYEE.GENDER.eq(1))
        .and(EMPLOYEE.AGE.gt(24));
List<Employee> employees = employeeMapper.selectListByQuery(query);

 

不需要判斷條件,預設條件為空時,自動忽略。

2.3. 查詢部分欄位

這個又是JPA一個比較麻煩的點,JPA是以物件的形式來操作的SQL,所以每次都是查詢出來全部的資料。有兩種方式可以解決。

  • 查詢出資料集,用List<Object[]>陣列來接收,在轉化成對應我們需要的部分資料。

  • 重新構建一個Model資料,裡面只放置我們需要的資料欄位,透過new Model(ParamA, ParamB)的方式來解決。

無論哪一種方式都會感覺非常彆扭,只是一個很簡單的需求。實現起來異常的麻煩。

2.3.1. Mybatis-Flex

在Mybatis-Flex中實現非常簡單。使用select方法即可,不傳引數值就是查詢所有欄位。可以定義自己想要查詢的欄位即可。

// 查詢所有資料
QueryWrapper queryWrapper = QueryWrapper.create()
        .select()
        .where(ACCOUNT.AGE.eq(18));
Account account = accountMapper.selectOneByQuery(queryWrapper);

// 查詢部分欄位,也可以使用Lambda表示式
select(QueryColumn("id"), QueryColumn("name"), QueryColumn("category_id"))

 

2.4. kotlin支援

我同步也有一個Kotlin的專案用的JPA,我也一起改成了flex版本。花了不多時間就改造完成,感覺程式碼整個都看起來非常優雅。

2.4.1. 分頁查詢
fun detailList(fairyDetailParam: FairyDetailParam): List<FairyDetail> {
    val paginateWith = paginate<FairyDetail>(pageNumber = fairyDetailParam.current,
        pageSize = fairyDetailParam.size,
        init = {
            select(QueryColumn("id"), QueryColumn("name"), QueryColumn("category_id"))
            whereWith {
                FairyDetail::categoryId eq fairyDetailParam.categoryId
            }
        }
    )

    return paginateWith.records
}

 

定義分頁物件和查詢條件即可,init引數傳入的是QueryScope,可以自由匹配需要查詢的引數。flex有一個特別好的點是,寫程式碼和寫SQL的感覺保持一致性。先寫select,再寫where,orderBy這些。

// 無需註冊Mapper與APT/KSP即可查詢操作
val accountList: List<Account> = query {
    select(Account::id, Account::userName)
    whereWith {
        Account::age.isNotNull and (Account::age ge 17)
    }
    orderBy(-Account::id)
}

 
2.4.2. 更新使用者
// 更新使用者頭像和暱稱
update<User> {
    User::avatarUrl set user.avatarUrl
    whereWith {
        User::openId eq user.openId
    }
}

 

更新使用者頭像地址,透過OpenId。寫起來非常絲滑。

2.5. 程式碼自動生成

Gradle中需要新增annotationProcessor 'com.mybatis-flex:mybatis-flex-processor:1.9.3'註解來進行程式碼生成。Maven的使用者可以自行在官網查詢如何配置。

目前我做的是半自動的資料生成。先手動建立實體類,在透過實體類生成對應的操作物件。

2.5.1. 建立實體
package cn.db101.jcc.entity;

import java.io.Serializable;
import java.util.Date;

import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import lombok.Data;

/**
 * 
 * @TableName t_banner
 */
@Table("t_banner")
@Data
public class Banner {
    /**
     * 
     */
    @Id(keyType = KeyType.Auto)
    private Integer id;

    /**
     * 
     */
    private String url;

    /**
     * 排序,越小越靠前
     */
    private Integer sort;

    /**
     * 
     */
    private Date createTime;

}

 
2.5.2. 編譯專案

會在target或者build目錄中生成對應的實體互操作類。

預設生成的實體類和欄位都是大寫來進行體現。

/**
 * 透過使用者查詢收藏列表
 * @param userId
 * @return
 */
public List<Lineup> lineUpList(int userId) {
    // 查詢收藏的Id
    List<Favorites> favoritesList = favoritesMapper.selectListByQuery(QueryWrapper.create()
            .select(Favorites::getLineupId)
            .from(Favorites.class)
            .where(FAVORITES.USER_ID.eq(userId)));

    return lineupService.listFromId(favoritesList.stream().map(Favorites::getLineupId).collect(Collectors.toList()));
}

/**
 * 刪除收藏
 * @param favorites
 */
public void deleteFavorites(Favorites favorites) {

    favoritesMapper.deleteByQuery(QueryWrapper.create()
            .where(FAVORITES.USER_ID.eq(favorites.getUserId()))
            .and(FAVORITES.LINEUP_ID.eq(favorites.getLineupId())));
}

public Favorites selectOne(Favorites favorites) {

    return favoritesMapper.selectOneByQuery(QueryWrapper.create()
            .where(FAVORITES.USER_ID.eq(favorites.getUserId()))
            .and(FAVORITES.LINEUP_ID.eq(favorites.getLineupId())));
}

 
2.5.3. 全自動生成

透過連線資料庫查詢對應的表生成對應的實體,實體互操作 類,Mapper,Controller等。

public class Codegen {

    public static void main(String[] args) {
        //配置資料來源
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/your-database?characterEncoding=utf-8");
        dataSource.setUsername("root");
        dataSource.setPassword("******");

        //建立配置內容,兩種風格都可以。
        GlobalConfig globalConfig = createGlobalConfigUseStyle1();
        //GlobalConfig globalConfig = createGlobalConfigUseStyle2();

        //透過 datasource 和 globalConfig 建立程式碼生成器
        Generator generator = new Generator(dataSource, globalConfig);

        //生成程式碼
        generator.generate();
    }

    public static GlobalConfig createGlobalConfigUseStyle1() {
        //建立配置內容
        GlobalConfig globalConfig = new GlobalConfig();

        //設定根包
        globalConfig.setBasePackage("com.test");

        //設定表字首和只生成哪些表
        globalConfig.setTablePrefix("tb_");
        globalConfig.setGenerateTable("tb_account", "tb_account_session");

        //設定生成 entity 並啟用 Lombok
        globalConfig.setEntityGenerateEnable(true);
        globalConfig.setEntityWithLombok(true);
        //設定專案的JDK版本,專案的JDK為14及以上時建議設定該項,小於14則可以不設定
        globalConfig.setJdkVersion(17);

        //設定生成 mapper
        globalConfig.setMapperGenerateEnable(true);

        //可以單獨配置某個列
        ColumnConfig columnConfig = new ColumnConfig();
        columnConfig.setColumnName("tenant_id");
        columnConfig.setLarge(true);
        columnConfig.setVersion(true);
        globalConfig.setColumnConfig("tb_account", columnConfig);

        return globalConfig;
    }

    public static GlobalConfig createGlobalConfigUseStyle2() {
        //建立配置內容
        GlobalConfig globalConfig = new GlobalConfig();

        //設定根包
        globalConfig.getPackageConfig()
                .setBasePackage("com.test");

        //設定表字首和只生成哪些表,setGenerateTable 未配置時,生成所有表
        globalConfig.getStrategyConfig()
                .setTablePrefix("tb_")
                .setGenerateTable("tb_account", "tb_account_session");

        //設定生成 entity 並啟用 Lombok
        globalConfig.enableEntity()
                .setWithLombok(true)
                .setJdkVersion(17);

        //設定生成 mapper
        globalConfig.enableMapper();

        //可以單獨配置某個列
        ColumnConfig columnConfig = new ColumnConfig();
        columnConfig.setColumnName("tenant_id");
        columnConfig.setLarge(true);
        columnConfig.setVersion(true);
        globalConfig.getStrategyConfig()
                .setColumnConfig("tb_account", columnConfig);

        return globalConfig;
    }
}

 

Copy記錄僅供自己下次使用方便

相關文章