最近火起的 Bean Searcher 與 MyBatis Plus 到底有啥區別?

上篇: 我這樣寫代碼,比直接使用 MyBatis 效率提高了 100 倍

歡迎公衆號轉載,但請轉 當前最新版 並在顯眼處 標明作者 與 註明出處。如果你喜歡本文也歡迎轉發分享 ^_^

Bean Searcher 號稱 任何複雜的查詢都可以 一行代碼搞定,但 Mybatis Plus 似乎也有類似的動態查詢功能,它們有怎樣的區別呢?

區別一(基本)

Mybatis Plus 依賴 MyBatis, 功能 CRUD 都有,而 Bean Seracher 不依賴任何 ORM,只專注高級查詢。

只有使用 MyBatis 的項目纔會用 Mybatis Plus,而使用 Hibernate,Data Jdbc 等其它 ORM 的人則無法使用 Mybatis Plus。但是這些項目都可以使用 Bean Searcher(可與任何 ORM 配合使用,也可單獨使用)。

使用 Mybatis Plus 需要編寫實體類 和 Mapper 接口,而 Bean Searcher 只需編寫 實體類,無需編寫任何接口。

這個區別意義其實不大,因爲如果你用 Mybatis Plus,在增刪改的時候還是需要定義 Mapper 接口。

區別二(高級查詢)

Mybatis Plus 的 字段運算符 是靜態的,而 Bean Searcher 的是動態的。

字段運算符指的是某字段參與條件時用的是 => 亦或是 like 這些條件類型。
不只 Mybatis Plus,一般的傳統 ORM 的字段運算符都是靜態的,包括 Hibernate、Spring data jdbc、JOOQ 等。

下面舉例說明。對於只有三個字段的簡單實體類:

 
java
複製代碼
public class User {
    private long id;
    private String name;
    private int age;
    // 省略 Getter Setter
}

1)使用 MyBatis Plus 查詢:

依賴:

 
groovy
複製代碼
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.5'

首先要寫一個 Mapper 接口:

 
java
複製代碼
public interface UserMapper extends BaseMapper<User> {
}

然後在 Controller 裏寫查詢接口:

 
java
複製代碼
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/mp")
    public List<User> mp(User user) {
        return userMapper.selectList(new QueryWrapper<>(user));
    }
    
}

此時這個接口可以支持 三個檢索參數,id, name, age,例如:

  • GET /user/mp? name=Jack 查詢 name 等於 Jack 的數據
  • GET /user/mp? age=20 查詢 age 等於 20 的數據

但是他們所能表達的關係都是 等於,如果你還想查詢 age > 20 的數據,則無能爲力了,除非在實體類的 age 字段上加上一條註解:

 
java
複製代碼
@TableField(condition = "%s>#{%s}")
private int age;

但加了註解後,age 就 只能 表達 大於 的關係了,不再可以表達 等於了。所以說,MyBatit Plus 的 字段運算符 是 靜態 的,不能由參數動態指定。

當然我們可以在 Controller 里根據參數調用 QueryWrapper 的不同方法讓它支持,但這樣代碼就不只一行了,檢索的需求越複雜,需要編寫的代碼就越多了。

2)使用 Bean Searcher 查詢:

依賴:

 
groovy
複製代碼
implementation 'cn.zhxu:bean-searcher-boot-starter:4.2.7'

不用編寫任何接口,複用同一個實體類,直接進行查詢:

 
java
複製代碼
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private BeanSearcher beanSearcher;

    @GetMapping("/bs")
    public List<User> bs(@RequestParam Map<String, Object> params) {
        // 你是否對入參 Map 有偏見?如果有,請耐心往下看,有方案
        return beanSearcher.searchList(User.class, params);
    }
    
}

此時這個接口可以支持的檢索參數就非常多了:

  • GET /user/bs? name=Jack 查詢 name 等於 Jack 的數據
  • GET /user/bs? name=Jack & name-ic=true 查詢 name 等於 Jack 時 忽略大小寫
  • GET /user/bs? name=Jack & name-op=ct 查詢 name 包含 Jack 的數據
  • GET /user/bs? age=20 查詢 age 等於 20 的數據
  • GET /user/bs? age=20 & age-op=gt 查詢 age 大於 20 的數據
  • 等等...

可以看出,Bean Searcher 對每個字段使用的 運算符 都可以由參數指定,它們是 動態 的。

無論查詢需求簡單還是複雜,Controller 裏都只需一行代碼。
參數 xxx-op 可以傳哪些值?參閱這裏:bs.zhxu.cn/guide/lates…

看到這裏,如果看的明白,應該有一半的讀者開始感慨:好傢伙,這不是把後端組裝查詢條件的過程都甩給了前端?誰用了這個框架,不會被前端打死嗎?

哈哈,我是不是道出了你現在心裏的想法?如果你真的如此想,請仔細回看我們正在討論的主題:【高級查詢】! 如果 不能理解什麼是高級查詢,我再貼個圖助你思考

76c821cac11375cea6298c0c94a8fad.png

當然也並不是所有的檢索需求都如此複雜,當前端不需要控制檢索方式時,xxx-op 參數 可以省略,省略時,默認表達的是 等於,如果你想表達 其它方式,只需一個註解即可,例如:

 
java
複製代碼
@DbField(onlyOn = GreaterThan.class)
private int age;

這時,當前端只傳一個 age 參數時,執行的 SQL 條件就是 age > ? 了,並且即使前端多傳一個 age-op 參數,也不再起作用了。

這其實是條件約束,下文會繼續講到。

區別三(邏輯分組)

就上文所例的代碼,除卻運算符 動靜 的區別,Mybatis Plus 對接收到的參數生成的條件 都是且的關係,而 Bean Searcher 默認也是且,但支持 邏輯分組。

再舉例說明,假設查詢條件爲:

( name = Jack 並且 age = 20  )  或者  ( age = 30  )

此時,MyBatis Plus 的一行代碼就無能爲力了,但 Bean Searcher 的一行代碼仍然管用,只需這樣傳參即可:

  • GET /user/bs? a.name=Jack & a.age=20 & b.age=30 & gexpr=a|b

這裏 Bean Searcher 將參數分爲 a, b 兩組,並用新參數 gexpr 來表達這兩組之間的關係(ab)。

實際傳參時gexpr的值需要 URLEncode 編碼一下: URLEncode('a|b') => 'a%7Cb',因爲 HTTP 規定參數在 URL 上不可以出現 | 這種特殊字符。當然如果你喜歡 POST, 可以將它放在報文體裏。

什麼場景下適合使用此功能? 當遇見類似下圖中的需求時,它將助你一招制敵:

image.png

分組功能非常強大,但如此複雜的檢索需求也確實罕見,這裏不再細述,詳情可閱:bs.zhxu.cn/guide/lates…

區別四(多表聯查)

在不寫 SQL 的情況下,Mybatis Plus 的動態查詢 僅限於 單表,而 Bean Searcher 單表 和 多表 都支持的一樣好。

這也是很重要的一點區別,因爲 大多數高級查詢 場景都是 需要聯表 的。

當然有些人堅持用單表做查詢,爲了避免聯表,從而在主表中冗餘了很多字段,這不僅造成了 數據庫存儲空間壓力急劇增加,還讓項目更加難以維護。因爲源數據一但變化,你必須同時更新這些冗餘的字段,只要漏了一處,BUG 就跳出來了。

還是舉個例子,某訂單列表需要展示 訂單號,訂單金額,店鋪名,買家名 等信息,用 Bean Searcher 實體類可以這麼寫:

 
java
複製代碼
@SearchBean(
    tables = "order o, shop s, user u",  // 三表關聯
    where = "o.shop_id = s.id and o.buyer_id = u.id",  // 關聯關係
    autoMapTo = "o"  // 未被 @DbField 註解的字段都映射到 order 表
)
public class OrderVO {
    private long id;         // 訂單ID   o.id
    private String orderNo;  // 訂單號   o.order_no
    private long amount;     // 訂單金額 o.amount
    @DbField("s.name")
    private String shop;     // 店鋪名   s.name
    @DbField("u.name")
    private String buyer;    // 買家名   u.name
    // 省略 Getter Setter
}

有心的同學會注意到,這個實體類的命名並不是 Order, 而是 OrderVO。這裏只是一個建議的命名,因爲它是本質上就是一個 VO(View Object),作用只是一個視圖實體類,所以建議將它和普通的單表實體類放在不同的 package 下(這只是一個規範)。

然後我們的 Controller 中仍然只需一行代碼:

 
java
複製代碼
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private BeanSearcher beanSearcher;

    @GetMapping("/index")
    public SearchResult<OrderVO> index(@RequestParam Map<String, Object> params) {
        // search 方法同時會返回滿足條件的總條數
        return beanSearcher.search(OrderVO.class, params);
    }

}

這就實現了一個支持高級查詢的 訂單接口,它同樣支持在上文 區別二 與 區別三 中所展示的各種檢索方式。

從本例可以看出,Bean Searcher 的檢索結果是 VO 對象,而非普通的單表實體類(DTO),這 省去了 DTO 向 VO 的轉換過程,它可以直接返回給前端。

區別五(使用場景)

在事務性的接口用推薦使用 MyBatis Plus, 非事務的檢索接口中推薦使用 Bean Searcher

  • 例如 創建訂單接口,在這個接口內部同樣有很多查詢,比如你需要查詢 店鋪的是否已經打烊,商品的庫存是否還足夠等,這些查詢場景,推薦依然使用 原有的 MyBatis Plus 或其它 ORM 就好,不必再用 Bean Seracher 了。

  • 再如 訂單列表接口,純查詢,可能需要分頁、排序、過濾等功能,此時就可用 Bean Seracher 了。

網友疑問

1)這貌似開放很大的檢索能力,風險可控嗎?

Bean Searcher 默認對實體類中的每個字段都支持了很多種檢索方式,但是我們也可以對它進行約束。

條件約束

例如,User 實體類的 name 字段只允許 精確匹配 與 後模糊 查詢,則在 name 字段上添加一個註解即可:

 
java
複製代碼
@DbField(onlyOn = {Equal.class, StartWith.class})
private String name;

再如:不允許 age 字段參與 where 條件,則可以:

 
java
複製代碼
@DbField(conditional = false)
private int age;

參考:bs.zhxu.cn/guide/lates…

排序約束

Bean Searcher 默認允許按所有字段排序,但可以在實體類裏進行約束。例如,只允許按 age 字段降序排序:

 
kotlin
複製代碼
@SearchBean(orderBy = "age desc", sortType = SortType.ONLY_ENTITY)
public class User {
    // ...
}

或者,禁止使用排序:

 
kotlin
複製代碼
@SearchBean(sortType = SortType.ONLY_ENTITY)
public class User {
    // ...
}

參考:bs.zhxu.cn/guide/lates…

2)使用 Bean Searcher 後 Controller 的入參必須是 Map 類型?

:這 並不是必須的,只是 Bean Searcher 的檢索方法接受這個類型的參數而已。如果你在 Controller 入參那裏 用一個 POJO 來接收也是可以的,只需要再用一個工具類把它轉換爲 Map 即可,只不過 平白多寫了一個類 而已,例如:

 
java
複製代碼
@GetMapping("/bs")
public List<User> bs(UserQuery query) {
    // 將 UserQuery 對象轉換爲 Map 再傳入進行檢索
    return beanSearcher.searchList(User.class, Utils.toMap(query));
}

這裏爲什麼不直接使用 User 實體類來接收呢? 因爲 Bean Searcher 默認支持很多參數,而原有的 User 實體類中的字段不夠多,用它來接收的話會有很多參數接收不到。如果咱們的檢索需求比較簡單,不需要前端指定那些參數,則可以直接使用 User 實體類來接收。

這裏的 UserQuery 可以這麼定義:

 
java
複製代碼
// 繼承 User 裏的字段
public class UserQuery extends User {
    // 附加:排序參數
    private String order;
    private String sort;
    // 附加:分頁參數
    private Integer page;
    private Integer size;
    // 附加:字段衍生參數
    private String id_op;   // 由於字段命名不能有中劃線,這裏有下劃線替代
    private String name_op; // 前端傳參的時候就不能傳 name-op,而是 name_op 了
    private String name_ic;
    private String age_op;
    // 省略其它附加字段...
    
    // 省略 Getter Setter 方法
}

然後 Utils 工具類的 toMap 方法可以這樣寫(這個工具類是通用的):

 
java
複製代碼
public static Map<String, Object> toMap(Object bean) {
    Map<String, Object> map = new HashMap<>();
    Class<?> beanClass = bean.getClass();
    while (beanClass != Object.class) {
        for (Field field : beanClass.getDeclaredFields()) {
            field.setAccessible(true);
            try {
                // 將下劃線轉換爲中劃線
                Strubg name = field.getName().replace('_', '-');
                map.put(name, field.get(bean));
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        beanClass = beanClass.getSuperclass();
    }
    return map;
}

這樣就可以了,該接口依然可以支持很多種檢索方式:

  • GET /user/bs? name=Jack 查詢 name 等於 Jack 的數據
  • GET /user/bs? name=Jack & name_ic=true 查詢 name 等於 Jack 時 忽略大小寫
  • GET /user/bs? name=Jack & name_op=ct 查詢 name 包含 Jack 的數據
  • GET /user/bs? age=20 查詢 age 等於 20 的數據
  • GET /user/bs? age=20 & age_op=gt 查詢 age 大於 20 的數據
  • 等等...

注意使用參數是 name_op,不再是 name-op

以上的方式應該滿足了一些強迫症患者的期望,但是這樣的代價是多寫一個 UserQuery 類,這不禁讓我們細想:這樣做值得嗎?

當然,寫成這樣是有一些好處的:

  • 便於參數校驗
  • 便於生成接口文檔

但是:

  • 這是一個 非事務性 的檢索接口,參數校驗真的那麼必要嗎?本來就可以無參請求,參數傳錯了系統自動忽略它是不是也可以?
  • 如果瞭解了 Bean Searcher 參數規則,是不是不用這個 UserQuery 類也可以生成文檔,或者在文檔中一句話概括 該接口是 Bean Searcher 檢索接口,請按照規則傳遞參數,是不是也行呢?

所以,我的建議是:一切以真實需求爲準則,不要爲了規範而去規範,莫名徒增代碼。

看到這裏,你可能已經在心裏準備了一大堆的話想要反駁我,別急,我們先回顧一下前面的 [多表聯查] 章節所提到的:

  • Bean Searcher 中的實體類(SearchBean),實際上是一個可以 直接與 DB 有跨表映射關係 的 VO(View Ojbect),它代表一種檢索業務,在概念上它與傳統 ORM 的實體類(Entity)或 域類(Domain)有着本質的區別!

這一句話,道出了 Bean Searcher 的靈魂。它看似簡單,實則難以悟透。如果您不是那種千古罕見先天通靈的奇才,強烈建議閱讀官方文檔的 介紹 > 設計思想(出發點) 章節,那裏有對這一句話的詳細解釋。

3)想手動添加或修改參數,只能向 Mapput 嗎?有沒有優雅點寫法?

答:當然有。Bean Searcher 提供了一個 參數構建器,可讓後端人員想手動添加或修改檢索參數時使用。例如:

 
java
複製代碼
@GetMapping("/bs")
public List<User> bs(@RequestParam Map<String, Object> params) {
    params = MapUtils.builder(params)  // 在原有參數基礎之上
        .field(User::getAge, 20, 30).op(Between.class) // 添加一個年齡區間條件
        .field(User::getName).op(StartWith.class) // 修改 name 字段的運算符爲 StartWith,參數值還是用前端傳來的參數
        .build();
    return beanSearcher.searchList(User.class, params);
}

4)前端亂傳參數的話,存在 SQL 注入風險嗎?

答:不存在的,Bean Searcher 是一個 只讀 ORM,它也存在 對象關係映射,所傳參數都是實體類內定義的 Java 屬性名,而非數據庫表裏的字段名(當前端傳遞實體類未定義的字段參數時,會被自動忽略)。

也可以說:檢索參數與數據庫表是解耦的

5)可以隨意傳參,會讓用戶獲取本不該看到的數據嗎?

答:不會的,因爲用戶 可獲取數據最多的請求就是無參請求,用戶嘗試的任何參數,都只會縮小數據範圍,不可能擴大

如果想做 數據權限,根據不同的用戶返回不同的數據:可在 參數過濾器 裏爲權限字段統一注入條件(前提是 實體類中得有一個數據權限字段,可以在基類中定義)。

6)效率雖有提高,但性能如何呢?

前段時間又不少朋友看了這篇文章私下問我 Bean Searcher 的性能如何,這個週末我就在家做了下對比測試,結果如下:

  • 比 Spring Data Jdbc 高 5 ~ 10
  • 比 Spring Data JPA 高 2 ~ 3
  • 比 原生 MyBatis 高 1 ~ 2
  • 比 MyBatis Plus 高 2 ~ 5

完整報告:

以上測試是基於 H2 內存數據庫進行

測試源碼地址,大家可自行測試對比:

7)支持哪些數據庫呢?

只要支持正常的 SQL 語法,都是支持的,另外 Bean Searcher 內置了四個方言實現:

  • 分頁語法和 MySQL 一樣的數據庫,默認支持
  • 分頁語法和 PostgreSql 一樣的數據庫,選用 PostgreSql 方言 即可
  • 分頁語法和 Oracle 一樣的數據庫,選用 Oracle 方言 即可
  • 分頁語法和 SqlServer(v2012+)一樣的數據庫,選用 SqlServer 方言 即可

如果分頁語法獨創的,自定義一個方言,只需實現一個方法即可,參考:高級 > SQL 方言 章節。

總結

上文所述的各種區別,並不是說 MyBatis Plus 和 Bean Searcher 哪個好哪個不好,而是它們 專注的領域 確實不一樣(BS 也不會替代 MP)。

Bean Searcher 在剛誕生的時候是專門用來處理那種特別複雜的檢索需求(如上文中的例圖所示),一般都用在 管理後臺 系統裏。
但用着用着,我們發現,對檢索需求沒那麼複雜的普通分頁查詢接口,Bean Searcher 也非常好用
代碼寫起來比用傳統的 ORM 要簡潔的多,只需一個實體類和 Controller 裏的幾行代碼,ServiceDao 什麼的全都消失了,而且它返回的結果就是 VO, 也 不需要再做進一步的轉換 了,可以直接返回給前端。

在項目中配合使用它們,事務中使用 MyBatis Plus,列表檢索場景使用 Bean Searcher,你將 如虎添翼

實際上,在舊項目中集成 Bean Searcher 更加容易,已有的單表實體類都能直接複用,而多表關聯的 VO 對象類也只需添加相應註解即可擁有強大的檢索能力。
無論項目原來 ORM 用的是 MyBatis, MP, 還是 Hibernate,Data Jdbc 等,也無論 Web 框架是 Spring Boot, Spring MVC 還是 Grails 或 Jfinal 等,只要是 java 項目, 都可以用它,爲系統賦能高級查詢。

最後附上 Bean Searcher 的相關連接:

如果覺得文本不錯,動手點個贊吧 ^_^


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章