天天看點

這樣寫代碼,比直接使用 MyBatis 效率提高了 100 倍。。

對一個 Java 後端程式員來說,MyBatis、Hibernate、Data Jdbc 等都是我們常用的 ORM 架構。它們有時候很好用,比如簡單的 CRUD,事務的支援都非常棒。

但有時候用起來也非常繁瑣,比如接下來我們要聊到的一個常見的開發需求,而對這類需求,本文會給出一個比直接使用這些 ORM 開發效率至少會提高 100 倍的方法(絕無誇張)。

首先資料庫有兩張表

使用者表(user):(簡單起見,假設隻有 4 個字段)

這樣寫代碼,比直接使用 MyBatis 效率提高了 100 倍。。
角色表(role):(簡單起見,假設隻有 2 個字段)
這樣寫代碼,比直接使用 MyBatis 效率提高了 100 倍。。

接下來我們要實作一個使用者查詢的功能

這個查詢有點複雜,它的要求如下:

可按使用者名

字段查詢,要求:

可精确比對(等于某個值)

可全模糊比對(包含給定的值)

可後模糊查詢(以...開頭)

可前模糊查詢(以.. 結尾)

可指定以上四種比對是否可以忽略大小寫

可按年齡

可精确比對(等于某個年齡)

可大于比對(大于某個值)

可小于比對(小于某個值)

可區間比對(某個區間範圍)

可按角色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 beanClass, Map params) 查詢指定條件下的資料 總條數

searchSum(Class beanClass, Map params, String field) 查詢指定條件下的 某字段 的 統計值

searchSum(Class beanClass, Map params, String[] fields) 查詢指定條件下的 多字段 的 統計值

search(Class beanClass, Map params) 分頁 查詢指定條件下資料 清單 與 總條數

search(Class beanClass, Map params, String[] summaryFields) 同上 + 多字段 統計

searchFirst(Class beanClass, Map params) 查詢指定條件下的 第一條 資料

searchList(Class beanClass, Map params) 分頁 查詢指定條件下資料 清單

searchAll(Class beanClass, Map params) 查詢指定條件下 所有 資料 清單

MapSearcher 與 BeanSearcher

另外,Bean Searcher 除了提供了 MapSearcher 檢索器外,還提供了 BeanSearcher 檢索器,它同樣擁有 MapSearcher 所有的方法,隻是它傳回的單條資料不是 Map,而是一個 泛型 對象。

參數建構工具

另外,如果你是在 Service 裡使用 Bean Searcher,那麼直接使用 Map 類型的參數可能不太優雅,為此, 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 beanClass, Map 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 類型的參數,至于這個參數是怎麼來的,和 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 = "name-op", required = false) String name_op,
            @RequestParam(value = "name-ic", required = false) Boolean name_ic,
            @RequestParam(value = "age-0", required = false) Integer age_0,
            @RequestParam(value = "age-1", required = false) Integer age_1,
            @RequestParam(value = "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 擴充

支援 多資料源

支援 自定義注解

等等