最近火起的 Bean Searcher 與 MyBatis Plus 倒底有啥區(qū)別怎爵?

image.png

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

歡迎公眾號轉(zhuǎn)載盅蝗,但請轉(zhuǎn) 當(dāng)前最新版 并在顯眼處 標(biāo)明作者注明出處鳖链。如果你喜歡本文也歡迎轉(zhuǎn)發(fā)分享 _

Bean Searcher 號稱 任何復(fù)雜的查詢都可以 一行代碼搞定,但 Mybatis Plus 似乎也有類似的動態(tài)查詢功能,它們有怎樣的區(qū)別呢芙委?

區(qū)別一(基本)

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

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

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

這個區(qū)別意義其實不大哪审,因為如果你用 Mybatis Plus蛾魄,在增刪改的時候還是需要定義 Mapper 接口。

區(qū)別二(高級查詢)

Mybatis Plus 的 字段運算符 是靜態(tài)的湿滓,而 Bean Searcher 的是動態(tài)的蠕搜。

字段運算符指的是某字段參與條件時用的是 =复亏、> 亦或是 like 這些條件類型党远。
不只 Mybatis Plus卿吐,一般的傳統(tǒng) ORM 的字段運算符都是靜態(tài)的,包括 Hibernate而线、Spring data jdbc铭污、JOOQ 等。

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

public class User {
    private long id;
    private String name;
    private int age;
    // 省略 Getter Setter
}

1)使用 MyBatis Plus 查詢:

依賴:

implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.1

首先要寫一個 Mapper 接口:

public interface UserMapper extends BaseMapper<User> {
}

然后在 Controller 里寫查詢接口:

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserMapper userMapper;

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

此時這個接口可以支持 三個檢索參數(shù)嘹狞,id, name, age,例如:

  • GET /user/mp? name=Jack 查詢 name 等于 Jack 的數(shù)據(jù)
  • GET /user/mp? age=20 查詢 age 等于 20 的數(shù)據(jù)

但是他們所能表達的關(guān)系都是 等于誓竿,如果你還想查詢 age > 20 的數(shù)據(jù)磅网,則無能為力了,除非在實體類的 age 字段上加上一條注解:

@TableField(condition = "%s>#{%s}")
private int age;

但加了注解后筷屡,age 就 只能 表達 大于 的關(guān)系了涧偷,不再可以表達 等于了。所以說毙死,MyBatit Plus 的 字段運算符靜態(tài) 的燎潮,不能由參數(shù)動態(tài)指定。

當(dāng)然我們可以在 Controller 里增加代碼讓它支持扼倘,但這樣代碼就不只一行了确封,檢索的需求越復(fù)雜,需要編寫的代碼就越多了。

2)使用 Bean Searcher 查詢:

依賴:

implementation 'cn.zhxu:bean-searcher-boot-starter:4.1.2'

不用編寫任何接口爪喘,復(fù)用同一個實體類颜曾,直接進行查詢:

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private BeanSearcher beanSearcher;

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

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

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

可以看出,Bean Searcher 對每個字段使用的 運算符 都可以由參數(shù)指定侦鹏,它們是 動態(tài) 的候址。

無論查詢需求簡單還是復(fù)雜,Controller 里都只需一行代碼种柑。
參數(shù) xxx-op 可以傳哪些值?參閱這里:https://bs.zhxu.cn/guide/latest/params.html#%E5%AD%97%E6%AE%B5%E8%BF%90%E7%AE%97%E7%AC%A6

看到這里匹耕,如果看的明白聚请,應(yīng)該有一半的讀者開始感慨:好家伙,這不是把后端組裝查詢條件的過程都甩給了前端稳其?誰用了這個框架驶赏,不會被前端打死嗎?

哈哈既鞠,我是不是道出了你現(xiàn)在心里的想法煤傍?如果你真的如此想,請仔細回看我們正在討論的主題:【高級查詢】嘱蛋!
如果 不能理解什么是高級查詢蚯姆,我再貼個圖助你思考

image.png

當(dāng)然也并不是所有的檢索需求都如此復(fù)雜,當(dāng)前端不需要控制檢索方式時洒敏,xxx-op 參數(shù) 可以省略龄恋,省略時,默認表達的是 等于凶伙,如果你想表達 其它方式郭毕,只需一個注解即可,例如:

@DbField(onlyOn = GreaterThan.class)
private int age;

這時函荣,當(dāng)前端只傳一個 age 參數(shù)時显押,執(zhí)行的 SQL 條件就是 age > ? 了,并且即使前端多傳一個 age-op 參數(shù)傻挂,也不再起作用了乘碑。

這其實是條件約束,下文會繼續(xù)講到踊谋。

區(qū)別三(邏輯分組)

就上文所例的代碼蝉仇,除卻運算符 動靜 的區(qū)別,Mybatis Plus 對接收到的參數(shù)生成的條件 都是且的關(guān)系,而 Bean Searcher 默認也是且轿衔,但支持 邏輯分組沉迹。

再舉例說明,假設(shè)查詢條件為:

( 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 將參數(shù)分為 a, b 兩組宛官,并用新參數(shù) gexpr 來表達這兩組之間的關(guān)系(ab)葫松。

實際傳參時gexpr的值需要 URLEncode 編碼一下: URLEncode('a|b') => 'a%7Cb',因為 HTTP 規(guī)定參數(shù)在 URL 上不可以出現(xiàn) | 這種特殊字符底洗。當(dāng)然如果你喜歡 POST, 可以將它放在報文體里腋么。

分組功能非常強大,但如此復(fù)雜的檢索需求也確實罕見亥揖,這里不再細述珊擂,詳情可閱:https://bs.zhxu.cn/guide/latest/params.html#%E9%80%BB%E8%BE%91%E5%88%86%E7%BB%84%EF%BC%88since-v3-5%EF%BC%89

區(qū)別四(多表聯(lián)查)

在不寫 SQL 的情況下,Mybatis Plus 的動態(tài)查詢 僅限于 單表费变,而 Bean Searcher 單表 和 多表 都支持的一樣好摧扇。

這也是很重要的一點區(qū)別,因為 大多數(shù)高級查詢 場景都是 需要聯(lián)表 的挚歧。

當(dāng)然有些人堅持用 Mybatis Plus 的動態(tài)查詢扛稽,為了避免聯(lián)表,從而在主表中冗余了很多字段滑负,這不僅造成了 數(shù)據(jù)庫存儲空間壓力急劇增加在张,還讓項目更加難以維護。因為源數(shù)據(jù)一但變化矮慕,你必須同時更新這些冗余的字段瞧掺,只要漏了一處,BUG 就跳出來了凡傅。

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

@SearchBean(
    tables = "order o, shop s, user u",  // 三表關(guān)聯(lián)
    joinCond = "o.shop_id = s.id and o.buyer_id = u.id",  // 關(guān)聯(lián)關(guān)系
    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
}

有心的同學(xué)會注意到槽华,這個實體類的命名并不是 Order, 而是 OrderVO壹蔓。這里只是一個建議的命名,因為它是本質(zhì)上就是一個 VO猫态,作用只是一個視圖實體類佣蓉,所以建議將它和普通的單表實體類放在不同的 package 下(這只是一個規(guī)范)披摄。

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

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private BeanSearcher beanSearcher;

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

}

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

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

區(qū)別五(使用場景)

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

例如璧函,創(chuàng)建訂單接口傀蚌,在這個接口內(nèi)部同樣有很多查詢,比如你需要查詢 店鋪的是否已經(jīng)打烊蘸吓,商品的庫存是否還足夠善炫,這些查詢場景,推薦直接使用 原有的 MyBatis Plus 就好库继,不必再用 Bean Seracher 了销部。

疑問

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

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

條件約束

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

@DbField(onlyOn = {Equal.class, StartWith.class})
private String name;

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

@DbField(conditional = false)
private int age;

參考:https://bs.zhxu.cn/guide/latest/advance.html#%E6%A3%80%E7%B4%A2%E7%BA%A6%E6%9D%9F

排序約束

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

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

或者恢暖,禁止使用排序:

@SearchBean(sortType = SortType.ONLY_ENTITY)
public class User {
    // ...
}

參考:https://bs.zhxu.cn/guide/latest/bean.html#%E9%BB%98%E8%AE%A4%E6%8E%92%E5%BA%8F%EF%BC%88since-v3-6-0%EF%BC%89

2)使用 Bean Searcher 后 Controller 的入?yún)⒈仨毷?Map 類型?

:這 并不是必須的排监,只是 Bean Searcher 的檢索方法接受這個類型的參數(shù)而已。如果你在 Controller 入?yún)⒛抢?用一個 POJO 來接收也是可以的杰捂,只需要再用一個工具類把它轉(zhuǎn)換為 Map 即可舆床,只不過 平白多寫了一個類 而已,例如:

@GetMapping("/bs")
public List<User> bs(UserQuery query) {
    // 將 UserQuery 對象轉(zhuǎn)換為 Map 再傳入進行檢索
    return beanSearcher.searchList(User.class, Utils.toMap(query));
}

這里為什么不使用 User 實體類類接收呢嫁佳? 因為 Bean Searcher 默認支持很多參數(shù)挨队,而原有的 User 實體類中的字段不夠多,用它來接收的話會有很多參數(shù)無法接收蒿往。如果咱就是不想要哪些額外的參數(shù)可以直接使用 User 實體類盛垦。

這里的 UserQuery 可以這么定義:

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

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

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 {
                // 將下劃線轉(zhuǎn)換為中劃線
                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 的數(shù)據(jù)
  • GET /user/bs? name=Jack & name_ic=true 查詢 name 等于 Jack 時 忽略大小寫
  • GET /user/bs? name=Jack & name_op=ct 查詢 name 包含 Jack 的數(shù)據(jù)
  • GET /user/bs? age=20 查詢 age 等于 20 的數(shù)據(jù)
  • GET /user/bs? age=20 & age_op=gt 查詢 age 大于 20 的數(shù)據(jù)
  • 等等...

注意使用參數(shù)是 name_op颊埃,不再是 name-op

以上的方式應(yīng)該滿足了一些強迫癥患者的期望,但是這樣的代價是多寫一個 UserQuery 類蝶俱,這不禁讓我們細想:這樣做值得嗎班利?

當(dāng)然,寫成這樣是有一些好處的:

  • 便于參數(shù)校驗
  • 便于生成接口文檔

但是:

  • 這是一個 非事務(wù)性 的檢索接口跷乐,參數(shù)校驗真的那么必要嗎肥败?本來就可以無參請求,參數(shù)傳錯了系統(tǒng)自動忽略它是不是也可以愕提?
  • 如果了解了 Bean Searcher 參數(shù)規(guī)則馒稍,是不是不用這個 UserQuery 類也可以生成文檔,或者在文檔中一句話概括 該接口是 Bean Searcher 檢索接口浅侨,請按照規(guī)則傳遞參數(shù)纽谒,是不是也行呢?

所以如输,我的建議是:一切以真實需求為準則鼓黔,不要為了規(guī)范而去規(guī)范,莫名徒增代碼不见。

3)前端亂傳參數(shù)的話澳化,存在 SQL 注入風(fēng)險嗎?

答:不存在的稳吮,Bean Searcher 是一個 只讀 ORM缎谷,它也存在 對象關(guān)系映射,所傳參數(shù)都是實體類內(nèi)定義的 Java 屬性名灶似,而非數(shù)據(jù)庫表里的字段名(當(dāng)前端傳遞實體類未定義的字段參數(shù)時列林,會被自動忽略)。

也可以說:檢索參數(shù)與數(shù)據(jù)庫表是解耦的酪惭。

4)可以隨意傳參希痴,會讓用戶獲取本不該看到的數(shù)據(jù)嗎?

答:不會的春感,因為用戶 可獲取數(shù)據(jù)最多的請求就是無參請求砌创,用戶嘗試的任何參數(shù),都只會縮小數(shù)據(jù)范圍鲫懒,不可能擴大纺铭。

如果想做 數(shù)據(jù)權(quán)限,根據(jù)不同的用戶返回不同的數(shù)據(jù):可在 Controller 層向檢索參數(shù)中手動添加一個參數(shù)即可刀疙,或者在 ControllerAdvise 里注入限制參數(shù)也行舶赔。

總結(jié)

上文所述的各種區(qū)別,并不是要說 MyBatis Plus 和 Bean Searcher 那個好哪個不好谦秧,而是它們 專注的領(lǐng)域 確實不一樣竟纳。

Bean Searcher 在剛誕生的時候是專門用來處理那種特別復(fù)雜的檢索需求(如上文中的例圖所示)撵溃,一般都用在管理后臺系統(tǒng)里。
但用著用著锥累,我們發(fā)現(xiàn)缘挑,對檢索需求沒那么復(fù)雜的普通分頁查詢接口,Bean Searcher 也非常好用桶略。
代碼寫起來比用傳統(tǒng)的 ORM 要簡潔的多语淘,只需一個實體類和 Controller 里的幾行代碼,ServiceDao 什么的全都消失了际歼,而且它返回的結(jié)果就是 VO, 也 不需要再做進一步的轉(zhuǎn)換 了惶翻,可以直接返回給前端。

在項目中配合使用它們鹅心,事務(wù)中使用 MyBatis Plus吕粗,列表檢索場景使用 Bean Searcher,你將 如虎添翼旭愧。

實際上颅筋,在舊項目中集成 Bean Searcher 更加容易,已有的單表實體類都能直接復(fù)用输枯,而多表關(guān)聯(lián)的 VO 對象類也只需添加相應(yīng)注解即可擁有強大的檢索能力议泵。
無論項目原來 ORM 用的是 MyBatis, MP, 還是 Hibernate,Data Jdbc 等桃熄,也無論 Web 框架是 Spring Boot, Spring MVC 還是 Grails 或 Jfinal 等先口,只要是 java 項目, 都可以用它,為系統(tǒng)賦能高級查詢蜻拨。

最后附上 Bean Searcher 的相關(guān)連接:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缎讼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子坑匠,更是在濱河造成了極大的恐慌血崭,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厘灼,死亡現(xiàn)場離奇詭異夹纫,居然都是意外死亡,警方通過查閱死者的電腦和手機设凹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門舰讹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闪朱,你說我怎么就攤上這事月匣∽耆鳎” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵锄开,是天一觀的道長素标。 經(jīng)常有香客問我,道長萍悴,這世上最難降的妖魔是什么头遭? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮癣诱,結(jié)果婚禮上计维,老公的妹妹穿的比我還像新娘。我一直安慰自己狡刘,他們只是感情好享潜,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嗅蔬,像睡著了一般剑按。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上澜术,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天艺蝴,我揣著相機與錄音,去河邊找鬼鸟废。 笑死猜敢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盒延。 我是一名探鬼主播缩擂,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼添寺!你這毒婦竟也來了胯盯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤计露,失蹤者是張志新(化名)和其女友劉穎博脑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體票罐,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡叉趣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了该押。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疗杉。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蚕礼,靈堂內(nèi)的尸體忽然破棺而出乡数,到底是詐尸還是另有隱情椭蹄,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布净赴,位于F島的核電站绳矩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏玖翅。R本人自食惡果不足惜翼馆,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望金度。 院中可真熱鬧应媚,春花似錦、人聲如沸猜极。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跟伏。三九已至丢胚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間受扳,已是汗流浹背携龟。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留勘高,地道東北人峡蟋。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像华望,于是被迫代替她去往敵國和親蕊蝗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355