作者:喬伊醬
鏈接:https://juejin.cn/post/7027733039299952676
對一個 Java 后端程式員來說,MyBatis、Hibernate、Data Jdbc 等都是我們常用的 ORM 框架,它們有時候很好用,比如簡單的 CRUD,事務的支持都非常棒,
但有時候用起來也非常繁瑣,比如接下來我們要聊到的一個常見的開發需求,而對這類需求,本文會給出一個比直接使用這些 ORM 開發效率至少會提高 100 倍的方法(絕無夸張),

首先資料庫有兩張表
用戶表(user):(簡單起見,假設只有 4 個欄位)
| 欄位名 | 型別 | 含義 |
|---|---|---|
| id | bitint | 用戶 ID |
| name | varchar(45) | 用戶名 |
| age | int | 年齡 |
| role_id | int | 角色 ID |
角色表(role):(簡單起見,假設只有 2 個欄位)
| 欄位名 | 型別 | 含義 |
|---|---|---|
| id | int | 角色 ID |
| name | varchar(45) | 角色名 |
接下來我們要實作一個用戶查詢的功能
這個查詢有點復雜,它的要求如下:
-
可按用戶名
欄位查詢,要求:
- 可精確匹配(等于某個值)
- 可全模糊匹配(包含給定的值)
- 可后模糊查詢(以...開頭)
- 可前模糊查詢(以.. 結尾)
- 可指定以上四種匹配是否可以忽略大小寫
-
可按年齡
欄位查詢,要求:
- 可精確匹配(等于某個年齡)
- 可大于匹配(大于某個值)
- 可小于匹配(小于某個值)
- 可區間匹配(某個區間范圍)
-
可按
角色ID查詢,要求:精確匹配 -
可按
用戶ID查詢,要求:同年齡欄位 -
可指定只輸出哪些列(例如,只查詢
ID與用戶名列) -
支持分頁(每次查詢后,頁面都要顯示滿足條件的用戶總數)
-
查詢時可選擇按
ID、用戶名、年齡等任意欄位排序
后端介面該怎么寫呢?
試想一下,對于這種要求的查詢,后端介面里的代碼如果用 MyBatis、Hibernate、Data Jdbc 直接來寫的話,100 行代碼 能實作嗎?
反正我是沒這個信心,算了,我還是直接坦白,面對這種需求后端如何 只用一行代碼搞定 吧(有興趣的同學可以 MyBatis 等寫個試試,最后可以對比一下)
手把手:只一行代碼實作以上需求
首先,重點人物出場啦:Bean Searcher, 它就是專門來對付這種串列檢索的,無論簡單的還是復雜的,統統一行代碼搞定!而且它還非常輕量,Jar 包體積僅不到 100KB,無第三方依賴,
假設我們專案使用的框架是 Spring Boot(當然 Bean Searcher 對框架沒有要求,但在 Spring Boot 中使用更加方便)
Spring Boot 基礎就不介紹了,推薦下這個實戰教程:
https://github.com/javastacks/spring-boot-best-practice
添加依賴
Maven :
<dependency>
<groupId>com.ejlchina</groupId>
<artifactId>bean-searcher-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
Gradle :
implementation 'com.ejlchina:bean-searcher-boot-starter:3.1.2'
然后寫個物體類來承載查詢的結果
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
private Long id; // 用戶ID(u.id)
private String name; // 用戶名(u.name)
private int age; // 年齡(u.age)
private int roleId; // 角色ID(u.role_id)
@DbField("r.name") // 指明這個屬性來自 role 表的 name 欄位
private String role; // 角色名(r.name)
// Getter and Setter ...
}
接著就可以寫用戶查詢介面了
介面路徑就叫 /user/index 吧:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private MapSearcher mapSearcher; // 注入檢索器(由 bean-searcher-boot-starter 提供)
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {
// 這里咱們只寫一行代碼
return mapSearcher.search(User.class, MapUtils.flat(request.getParameterMap()));
}
}
上述代碼中的
MapUtils是 Bean Searcher 提供的一個工具類,MapUtils.flat(request.getParameterMap())只是為了把前端傳來的請求引數統一收集起來,然后剩下的,就全部交給MapSearcher檢索器了,
這樣就完了?那我們來測一下這個介面,看看效果吧
(1)無參請求
- GET /user/index
- 回傳結果:
{
"dataList": [ // 用戶串列,默認回傳第 0 頁,默認分頁大小為 15 (可配置)
{ "id": 1, "name": "Jack", "age": 25, "roleId": 1, "role": "普通用戶" },
{ "id": 2, "name": "Tom", "age": 26, "roleId": 1, "role": "普通用戶" },
...
],
"totalCount": 100 // 用戶總數
}
(2)分頁請求(page | size)
- GET /user/index? page=2 & size=10
- 回傳結果:結構同 (1)(只是每頁 10 條,回傳第 2 頁)
引數名
size和page可自定義,page默認從0開始,同樣可自定義,并且可與其它引陣列合使用
(3)資料排序(sort | order)
- GET /user/index? sort=age & order=desc
- 回傳結果:結構同 (1)(只是 dataList 資料串列以 age 欄位降序輸出)
引數名
sort和order可自定義,可與其它引陣列合使用
(4)指定(排除)欄位(onlySelect | selectExclude)
- GET /user/index? onlySelect=id,name,role
- GET /user/index? selectExclude=age,roleId
- 回傳結果:( 串列只含 id,name 與 role 三個欄位)
{
"dataList": [ // 用戶串列,默認回傳第 0 頁(只包含 id,name,role 欄位)
{ "id": 1, "name": "Jack", "role": "普通用戶" },
{ "id": 2, "name": "Tom", "role": "普通用戶" },
...
],
"totalCount": 100 // 用戶總數
}
引數名
onlySelect和selectExclude可自定義,可與其它引陣列合使用
(5)欄位過濾(op = eq)
- GET /user/index? age=20
- GET /user/index? age=20 & age-op=eq
- 回傳結果:結構同 (1)(但只回傳 age = 20 的資料)
引數
age-op = eq表示age的 欄位運算子 是eq(Equal的縮寫),表示引數age與引數值20之間的關系是Equal,由于Equal是一個默認的關系,所以age-op = eq也可以省略
引數名 age-op 的后綴 -op 可自定義,且可與其它欄位引數 和 上文所列的引數(分頁、排序、指定欄位)組合使用,下文所列的欄位引數也是一樣,不再復述,
(6)欄位過濾(op = ne)
- GET /user/index? age=20 & age-op=ne
- 回傳結果:結構同 (1)(但只回傳 age != 20 的資料,
ne是NotEqual的縮寫)
(7)欄位過濾(op = ge)
- GET /user/index? age=20 & age-op=ge
- 回傳結果:結構同 (1)(但只回傳 age >= 20 的資料,
ge是GreateEqual的縮寫)
(8)欄位過濾(op = le)
- GET /user/index? age=20 & age-op=le
- 回傳結果:結構同 (1)(但只回傳 age <= 20 的資料,
le是LessEqual的縮寫)
(9)欄位過濾(op = gt)
- GET /user/index? age=20 & age-op=gt
- 回傳結果:結構同 (1)(但只回傳 age > 20 的資料,
gt是GreateThan的縮寫)
(10)欄位過濾(op = lt)
- GET /user/index? age=20 & age-op=lt
- 回傳結果:結構同 (1)(但只回傳 age < 20 的資料,
lt是LessThan的縮寫)
(11)欄位過濾(op = bt)
- GET /user/index? age-0=20 & age-1=30 & age-op=bt
- GET /user/index? age=[20,30] & age-op=bt(簡化版,[20,30] 需要 UrlEncode, 參考下文)
- 回傳結果:結構同 (1)(但只回傳 20 <= age <= 30 的資料,
bt是Between的縮寫)
引數
age-0 = 20表示age的第 0 個引數值是20,上述提到的age = 20實際上是age-0 = 20的簡寫形式,另:引數名age-0與age-1中的連字符-可自定義,
(12)欄位過濾(op = mv)
- GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=mv
- GET /user/index? age=[20,30,40] & age-op=mv(簡化版,[20,30,40] 需要 UrlEncode, 參考下文)
- 回傳結果:結構同 (1)(但只回傳 age in (20, 30, 40) 的資料,
mv是MultiValue的縮寫,表示有多個值的意思)
(13)欄位過濾(op = in)
- GET /user/index? name=Jack & name-op=in
- 回傳結果:結構同 (1)(但只回傳 name 包含 Jack 的資料,
in是Include的縮寫)
(14)欄位過濾(op = sw)
- GET /user/index? name=Jack & name-op=sw
- 回傳結果:結構同 (1)(但只回傳 name 以 Jack 開頭的資料,
sw是StartWith的縮寫)
(15)欄位過濾(op = ew)
- GET /user/index? name=Jack & name-op=ew
- 回傳結果:結構同 (1)(但只回傳 name 以 Jack 結尾的資料,
sw是EndWith的縮寫)
(16)欄位過濾(op = ey)
- GET /user/index? name-op=ey
- 回傳結果:結構同 (1)(但只回傳 name 為空 或為 null 的資料,
ey是Empty的縮寫)
(17)欄位過濾(op = ny)
- GET /user/index? name-op=ny
- 回傳結果:結構同 (1)(但只回傳 name 非空 的資料,
ny是NotEmpty的縮寫)
(18)忽略大小寫(ic = true)
- GET /user/index? name=Jack & name-ic=true
- 回傳結果:結構同 (1)(但只回傳 name 等于 Jack (忽略大小寫) 的資料,
ic是IgnoreCase的縮寫)
引數名
name-ic中的后綴-ic可自定義,該引數可與其它的引陣列合使用,比如這里檢索的是 name 等于 Jack 時忽略大小寫,但同樣適用于檢索 name 以 Jack 開頭或結尾時忽略大小寫,
當然,以上各種條件都可以組合,例如
查詢 name 以 Jack (忽略大小寫) 開頭,且 roleId = 1,結果以 id 欄位排序,每頁加載 10 條,查詢第 2 頁:
- GET /user/index? name=Jack & name-op=sw & name-ic=true & roleId=1 & sort=id & size=10 & page=2
- 回傳結果:結構同 (1)
OK,效果看完了,/user/index 介面里我們確實只寫了一行代碼,它便可以支持這么多種的檢索方式,有沒有覺得現在 你寫的一行代碼 就可以 干過別人的一百行 呢?

Bean Searcher
本例中,我們只使用了 Bean Searcher 提供的 MapSearcher 檢索器的一個檢索方法,其實,它還有很多檢索方法,
檢索方法
searchCount(Class<T> beanClass, Map<String, Object> params)查詢指定條件下的資料 總條數searchSum(Class<T> beanClass, Map<String, Object> params, String field)查詢指定條件下的 某欄位 的 統計值searchSum(Class<T> beanClass, Map<String, Object> params, String[] fields)查詢指定條件下的 多欄位 的 統計值search(Class<T> beanClass, Map<String, Object> params)分頁 查詢指定條件下資料 串列 與 總條數search(Class<T> beanClass, Map<String, Object> params, String[] summaryFields)同上 + 多欄位 統計searchFirst(Class<T> beanClass, Map<String, Object> params)查詢指定條件下的 第一條 資料searchList(Class<T> beanClass, Map<String, Object> params)分頁 查詢指定條件下資料 串列searchAll(Class<T> beanClass, Map<String, Object> params)查詢指定條件下 所有 資料 串列
MapSearcher 與 BeanSearcher
另外,Bean Searcher 除了提供了 MapSearcher 檢索器外,還提供了 BeanSearcher 檢索器,它同樣擁有 MapSearcher 所有的方法,只是它回傳的單條資料不是 Map,而是一個 泛型 物件,
引數構建工具
另外,如果你是在 Service 里使用 Bean Searcher,那么直接使用 Map<String, Object> 型別的引數可能不太優雅,為此, Bean Searcher 特意提供了一個引數構建工具,
例如,同樣查詢 name 以 Jack (忽略大小寫) 開頭,且 roleId = 1,結果以 id 欄位排序,每頁加載 10 條,加載第 2 頁,使用引數構建器,代碼可以這么寫:
Map<String, Object> params = MapUtils.builder()
.field(User::getName, "Jack").op(Operator.StartWith).ic()
.field(User::getRoleId, 1)
.orderBy(User::getId, "asc")
.page(2, 10)
.build()
List<User> users = beanSearcher.searchList(User.class, params);
這里使用的是
BeanSearcher檢索器,以及它的searchList(Class<T> beanClass, Map<String, Object> params)方法,
運算子約束
上文我們看到,Bean Searcher 對物體類中的每一個欄位,都直接支持了很多的檢索方式,
但某同學:哎呀!檢索方式太多了,我根本不需要這么多,我的資料量幾十億,用戶名欄位的前模糊查詢方式利用不到索引,萬一把我的資料庫查崩了怎么辦呀?
好辦,Bean Searcher 支持運算子的約束,物體類的用戶名 name 欄位只需要注解一下即可:
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(onlyOn = {Operator.Equal, Operator.StartWith})
private String name;
// 為減少篇幅,省略其它欄位...
}
如上,通過 @DbField 注解的 onlyOn 屬性,指定這個用戶名 name 只能適用與 精確匹配 和 后模糊查詢,其它檢索方式它將直接忽略,
上面的代碼是限制了 name 只能有兩種檢索方式,如果再嚴格一點,只允許 精確匹配,那其實有兩種寫法,
(1)還是使用運算子約束:
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(onlyOn = Operator.Equal)
private String name;
// 為減少篇幅,省略其它欄位...
}
(2)在 Controller 的介面方法里把運算子引數覆寫:
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {
Map<String, Object> params = MapUtils.flatBuilder(request.getParameterMap())
.field(User::getName).op(Operator.Equal) // 把 name 欄位的運算子直接覆寫為 Equal
.build()
return mapSearcher.search(User.class, params);
}
條件約束
該同學又:哎呀!我的資料量還是很大,age 欄位沒有索引,我不想讓它參與 where 條件,不然很可能就出現慢 SQL 啊!
不急,Bean Searcher 還支持條件的約束,讓這個欄位直接不能作為條件:
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(conditional = false)
private int age;
// 為減少篇幅,省略其它欄位...
}
如上,通過 @DbField 注解的 conditional 屬性, 就直接不允許 age 欄位參與條件了,無論前端怎么傳參,Bean Searcher 都不搭理,
引數過濾器
該同學仍:哎呀!哎呀 ...
別怕! Bean Searcher 還支持配置全域引數過濾器,可自定義任何引數過濾規則,在 Spring Boot 專案中,只需要配置一個 Bean:
@Bean
public ParamFilter myParamFilter() {
return new ParamFilter() {
@Override
public <T> Map<String, Object> doFilter(BeanMeta<T> beanMeta, Map<String, Object> paraMap) {
// beanMeta 是正在檢索的物體類的元資訊, paraMap 是當前的檢索引數
// TODO: 這里可以寫一些自定義的引數過濾規則
return paraMap; // 回傳過濾后的檢索引數
}
};
}
某同學問
引數咋這么怪,這么多呢,和前端有仇么
- 引數名是否奇怪,這其實看個人喜好,如果你不喜歡中劃線
-,不喜歡op、ic后綴,完全可以自定義,參考這篇檔案:
searcher.ejlchina.com/guide/lates…
- 引數個數的多少,其實是和需求的復雜程度相關的,如果需求很簡單,那么很多引數沒必要讓前端傳,后端直接塞進去就好,比如:
name只要求后模糊匹配,age只要求區間匹配,則可以:
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {
Map<String, Object> params = MapUtils.flatBuilder(request.getParameterMap())
.field(User::getName).op(Operator.StartWith)
.field(User::getAge).op(Operator.Between)
.build()
return mapSearcher.search(User.class, params);
}
這樣前端就不用傳 name-op 與 age-op 這兩個引數了,
其實還有一種更簡單的方法,那就是 運算子約束(當約束存在時,運算子默認就是 onlyOn 屬性中指定的第一個值,前端可以省略不傳):
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(onlyOn = Operator.StartWith)
private String name;
@DbField(onlyOn = Operator.Between)
private String age;
// 為減少篇幅,省略其它欄位...
}
- 對于 op=bt/mv 的多值引數傳遞,引數確實可以簡化,例如:
- 把
age-0=20 & age-1=30 & age-op=bt簡化為age=[20,30] & age-op=bt, - 把
age-0=20 & age-1=30 & age-2=40 & age-op=mv簡化為age=[20,30,40] & age-op=mv,
簡化方法:只需配置一個 ParamFilter(引數過濾器)即可,具體代碼可以參考這里:
https://github.com/ejlchina/bean-searcher/issues/10
入參是 request,我 swagger 檔案不好渲染了呀
其實,Bean Searcher 的檢索器只是需要一個 Map<String, Object> 型別的引數,至于這個引數是怎么來的,和 Bean Searcher 并沒有直接關系,前文之所以從 request 里取,只是因為這樣代碼看起來簡潔,如果你喜歡宣告引數,完全可以把代碼寫成這樣:
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(Integer page, Integer size,
String sort, String order, String name, Integer roleId,
@RequestParam(value = "https://www.cnblogs.com/javastack/archive/2021/12/07/name-op", required = false) String name_op,
@RequestParam(value = "https://www.cnblogs.com/javastack/archive/2021/12/07/name-ic", required = false) Boolean name_ic,
@RequestParam(value = "https://www.cnblogs.com/javastack/archive/2021/12/07/age-0", required = false) Integer age_0,
@RequestParam(value = "https://www.cnblogs.com/javastack/archive/2021/12/07/age-1", required = false) Integer age_1,
@RequestParam(value = "https://www.cnblogs.com/javastack/archive/2021/12/07/age-op", required = false) String age_op) {
Map<String, Object> params = MapUtils.builder()
.field(Employee::getName, name).op(name_op).ic(name_ic)
.field(Employee::getAge, age_0, age_1).op(age_op)
.field(Employee::getRoleId, roleId)
.orderBy(sort, order)
.page(page, size)
.build();
return mapSearcher.search(User.class, params);
}
欄位引數之間的關系都是 “且” 呀,那 “或” 呢? “且” “或” 任意組合呢?
上文所述的欄位引數之間確是都是 "且" 的關系,至于 “或”,雖然這種使用場景不太多,但 Bean Searcher 也是支持的,詳細可以參考這篇文章:
https://github.com/ejlchina/bean-searcher/issues/8
這里就不再復述了,
開發效率真的提高 100 倍了嗎?
從本例其實可以看出,效率提升的程度依賴于檢索需求的復雜度,需求越復雜,則效率提高倍數越多,反之則越少,如果需求超級復雜,則提高 1000 倍都有可能,
但即使我們日常開發中沒有如此復雜的需求,開發效率只提升了 5 到 10 倍,那是不是也非常可觀呢?
結語
本文介紹了 Bean Searcher 在復雜串列檢索領域的超強能力,它之所以可以極大提高這類需求的研發效率,根本上歸功于它 獨創 的 動態欄位運算子 與 多表映射機制,這是傳統 ORM 框架所沒有的,但由于篇幅所限,它的特性本文不能盡述,比如它還:
- 支持 聚合查詢
- 支持 Select|Where|From子查詢
- 支持 物體類嵌入引數
- 支持 欄位轉換器
- 支持 Sql 攔截器
- 支持 資料庫 Dialect 擴展
- 支持 多資料源
- 支持 自定義注解
- 等等
Bean Searcher 是我在作業中總結封裝出來的一個小工具,公司內部使用了 4 年,經歷大小專案三四十個,只是最近才著手完善檔案分享給大家,如果你喜歡,一定去點個 Star 哦 _,
再奉上 Bean Searcher 的詳細檔案:searcher.ejlchina.com/
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.別在再滿屏的 if/ else 了,試試策略模式,真香!!
3.臥槽!Java 中的 xx ≠ null 是什么新語法?
4.Spring Boot 2.6 正式發布,一大波新特性,,
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/375738.html
標籤:其他
上一篇:如何進行excel資料分析之后的可視化資料寫入保存!
下一篇:mybatis配置決議
