⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7027733039299952676 「乔伊酱」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

对一个 Java 后端程序员来说,mybatishibernatedata-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用户名年龄 等任意字段排序

后端接口该怎么写呢?

试想一下,对于这种要求的查询,后端接口里的代码如果用 mybatishibernatedata-jdbc 直接来写的话,100 行代码 能实现吗?

反正我是没这个信心,算了,我还是直接坦白,面对这种需求后端如何 只用一行代码搞定 吧(有兴趣的同学可以 mybatis 等写个试试,最后可以对比一下)

手把手:只一行代码实现以上需求

首先,重点人物出场啦:Bean Searcher, 它就是专门来对付这种列表检索的,无论简单的还是复杂的,统统一行代码搞定!而且它还非常轻量,Jar 包体积仅不到 100KB,无第三方依赖。

假设我们项目使用的框架是 Spring Boot(当然 Bean Searcher 对框架没有要求,但在 Spring Boot 中使用更加方便)

添加依赖

Maven :

<dependency>
<groupId>com.ejlchina</groupId>
<artifactId>bean-searcher-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>

Gradle :

implementation 'com.ejlchina:bean-searcher-boot-starter:3.0.1'

然后写个实体类来承载查询的结果

@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 int 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 页)

参数名 sizepage 可自定义, page 默认从 0 开始,同样可自定义,并且可与其它参数组合使用

(3)数据排序(sort | order)

  • GET /user/index? sort = age & order = desc
  • 返回结果:结构同 (1)(只是 dataList 数据列表以 age 字段降序输出)

参数名 sortorder 可自定义,可与其它参数组合使用

(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 // 用户总数
}

参数名 onlySelectselectExclude 可自定义,可与其它参数组合使用

(5)字段过滤(op = eq)

  • GET /user/index? age=20
  • GET /user/index? age=20 & age-op=eq
  • 返回结果:结构同 (1)(但只返回 age = 20 的数据)

参数 age-op = eq 表示 age字段运算符eqEqual 的缩写),表示参数 age 与参数值 20 之间的关系是 Equal,由于 Equal 是一个默认的关系,所以 age-op = eq 也可以省略

参数名 age-op 的后缀 -op 可自定义,且可与其它字段参数 和 上文所列的参数(分页、排序、指定字段)组合使用,下文所列的字段参数也是一样,不再复述。

(6)字段过滤(op = ne)

  • GET /user/index? age=20 & age-op=ne
  • 返回结果:结构同 (1)(但只返回 age != 20 的数据,neNotEqual 的缩写)

(7)字段过滤(op = ge)

  • GET /user/index? age=20 & age-op=ge
  • 返回结果:结构同 (1)(但只返回 age >= 20 的数据,geGreateEqual 的缩写)

(8)字段过滤(op = le)

  • GET /user/index? age=20 & age-op=le
  • 返回结果:结构同 (1)(但只返回 age <= 20 的数据,leLessEqual 的缩写)

(9)字段过滤(op = gt)

  • GET /user/index? age=20 & age-op=gt
  • 返回结果:结构同 (1)(但只返回 age > 20 的数据,gtGreateThan 的缩写)

(10)字段过滤(op = lt)

  • GET /user/index? age=20 & age-op=lt
  • 返回结果:结构同 (1)(但只返回 age < 20 的数据,ltLessThan 的缩写)

(11)字段过滤(op = bt)

  • GET /user/index? age-0=20 & age-1=30 & age-op=bt
  • 返回结果:结构同 (1)(但只返回 20 <= age <= 30 的数据,btBetween 的缩写)

参数 age-0 = 20 表示 age 的第 0 个参数值是 20。上述提到的 age = 20 实际上是 age-0 = 20 的简写形式。另:参数名 age-0age-1 中的连字符 - 可自定义。

(12)字段过滤(op = mv)

  • GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=mv
  • 返回结果:结构同 (1)(但只返回 age in (20, 30, 40) 的数据,mvMultiValue 的缩写,表示有多个值的意思)

(13)字段过滤(op = in)

  • GET /user/index? name=Jack & name-op=in
  • 返回结果:结构同 (1)(但只返回 name 包含 Jack 的数据,inInclude 的缩写)

(14)字段过滤(op = sw)

  • GET /user/index? name=Jack & name-op=sw
  • 返回结果:结构同 (1)(但只返回 name 以 Jack 开头的数据,swStartWith 的缩写)

(15)字段过滤(op = ew)

  • GET /user/index? name=Jack & name-op=ew
  • 返回结果:结构同 (1)(但只返回 name 以 Jack 结尾的数据,swEndWith 的缩写)

(16)字段过滤(op = ey)

  • GET /user/index? name-op=ey
  • 返回结果:结构同 (1)(但只返回 name 为空 或为 null 的数据,eyEmpty 的缩写)

(17)字段过滤(op = ny)

  • GET /user/index? name-op=ny
  • 返回结果:结构同 (1)(但只返回 name 非空 的数据,nyNotEmpty 的缩写)

(18)忽略大小写(ic = true)

  • GET /user/index? name=Jack & name-ic=true
  • 返回结果:结构同 (1)(但只返回 name 等于 Jack (忽略大小写) 的数据,icIgnoreCase 的缩写)

参数名 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 检索器的一个 search 方法,其实,它有很多 search 方法。

检索方法

  • 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) // 收集前端传来的参数
.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; // 返回过滤后的检索参数
}
};
}

某同学问

参数咋这么怪,这么多呢,和前端有仇呀

  1. 参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线 -,不喜欢 opic 后缀,完全可以自定义,参考这篇文档:

searcher.ejlchina.com/guide/lates…

  1. 参数个数的多少,其实是和需求的复杂程度相关,如果需求很简单,其实很多参数没必要让前端传,后端直接塞进去就好,比如:name 只要求后模糊匹配,age 只要求区间匹配,那可以这样:

@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {
Map<String, Object> params = MapUtils.flatBuilder(request)
.field(User::getName).op(Operator.StartWith)
.field(User::getAge).op(Operator.Between)
.build()
return mapSearcher.search(User.class, params);
}

这样前端就不用传 name-opage-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;

// 为减少篇幅,省略其它字段...
}

入参是 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 在复杂列表检索领域的超强能力,但由于篇幅所限,本文所述仍只是冰山一角,比如它还:

  • 支持 嵌入参数
  • 支持 字段转换器
  • 支持 Sql 拦截器
  • 支持 多数据源
  • 支持 自定义注解
  • 等等

项目 GitHub 地址

https://github.com/ejlchina/bean-searcher

文章目录
  1. 1. 首先数据库有两张表
  2. 2. 接下来我们要实现一个用户查询的功能
  3. 3. 后端接口该怎么写呢?
  4. 4. 手把手:只一行代码实现以上需求
    1. 4.1. 添加依赖
    2. 4.2. 然后写个实体类来承载查询的结果
    3. 4.3. 接着就可以写用户查询接口了
  5. 5. 这样就完了?那我们来测一下这个接口,看看效果吧
    1. 5.0.1. (1)无参请求
    2. 5.0.2. (2)分页请求(page | size)
    3. 5.0.3. (3)数据排序(sort | order)
    4. 5.0.4. (4)指定(排除)字段(onlySelect | selectExclude)
    5. 5.0.5. (5)字段过滤(op = eq)
    6. 5.0.6. (6)字段过滤(op = ne)
    7. 5.0.7. (7)字段过滤(op = ge)
    8. 5.0.8. (8)字段过滤(op = le)
    9. 5.0.9. (9)字段过滤(op = gt)
    10. 5.0.10. (10)字段过滤(op = lt)
    11. 5.0.11. (11)字段过滤(op = bt)
    12. 5.0.12. (12)字段过滤(op = mv)
    13. 5.0.13. (13)字段过滤(op = in)
    14. 5.0.14. (14)字段过滤(op = sw)
    15. 5.0.15. (15)字段过滤(op = ew)
    16. 5.0.16. (16)字段过滤(op = ey)
    17. 5.0.17. (17)字段过滤(op = ny)
    18. 5.0.18. (18)忽略大小写(ic = true)
    19. 5.0.19. 当然,以上各种条件都可以组合,例如
  • 6. Bean Searcher
    1. 6.1. 检索方法
    2. 6.2. MapSearcher 与 BeanSearcher
    3. 6.3. 参数构建工具
    4. 6.4. 运算符约束
      1. 6.4.0.1. (1)还是使用运算符约束:
      2. 6.4.0.2. (2)在 Controller 的接口方法里把运算符参数覆盖:
  • 6.5. 条件约束
  • 6.6. 参数过滤器
  • 7. 某同学问
    1. 7.1. 参数咋这么怪,这么多呢,和前端有仇呀
    2. 7.2. 入参是 request,我 swagger 文档不好渲染了呀
  • 8. 结语
  • 9. 项目 GitHub 地址