对一个 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 中使用更加方便)
添加依赖
Maven :
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
上述代码中的 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
返回结果:结构同 (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
返回结果:结构同 (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 接口里我们确实只写了一行代码,它便可以支持这么多种的检索方式,有没有觉得现在 你写的一行代码 就可以 干过别人的一百行 呢?
编辑切换为居中
添加图片注释,不超过 140 字(可选)
Bean Searcher
本例中,我们只使用了 Bean Searcher 提供的 MapSearcher 检索器的一个 search 方法,其实,它有很多 search 方法。
检索方法
searchCount(Class
searchSum(Class
searchSum(Class
search(Class
search(Class
searchFirst(Class
searchList(Class
searchAll(Class
MapSearcher 与 BeanSearcher
另外,Bean Searcher 除了提供了 MapSearcher 检索器外,还提供了 BeanSearcher 检索器,它同样拥有 MapSearcher 拥有的方法,只是它返回的单条数据不是 Map,而是一个 泛型 对象。
参数构建工具
另外,如果你是在 Service 里使用 Bean Searcher,那么直接使用 Map
例如,同样查询 name 以 Jack (忽略大小写) 开头,且 roleId = 1,结果以 id 字段排序,每页加载 10 条,加载第 2 页,使用参数构建器,代码可以这么写:
Map
这里使用的是 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
条件约束
该同学又:哎呀!我的数据量还是很大,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
某同学问
参数咋这么怪,这么多呢,和前端有仇么
参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线 -,不喜欢 op、ic 后缀,完全可以自定义,参考这篇文档:
searcher.ejlchina.com/guide/lates…
参数个数的多少,其实是和需求的复杂程度相关的。如果需求很简单,那么很多参数没必要让前端传,后端直接塞进去就好。比如:name 只要求后模糊匹配,age 只要求区间匹配,则可以:
@GetMapping("/index") public SearchResult
这样前端就不用传 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; // 为减少篇幅,省略其它字段... }
入参是 request,我 swagger 文档不好渲染了呀
其实,Bean Searcher 的检索器只是需要一个 Map
@GetMapping("/index") public SearchResult
结语
本文介绍了 Bean Searcher 在复杂列表检索领域的超强能力。它之所以可以极大提高这类需求的研发效率,根本上归功于它 独创 的 动态字段运算符 与 多表映射机制,这是传统 ORM 框架所没有的。但由于篇幅所限,它的特性本文不能尽述,比如它还:
支持 聚合查询
支持 Select|Where|From子查询
支持 实体类嵌入参数
支持 字段转换器
支持 Sql 拦截器
支持 多数据源
支持 自定义注解
等等
要了解更多,先来点个 Star 吧 : Github 、Gitee。
Bean Searcher 是我在工作中总结封装出来的一个小工具,公司内部使用了 4 年,经历大小项目三四十个,只是最近才着手完善文档分享给大家,如果你喜欢,一定去点个 Star 哦 ^_^。
再奉上 Bean Searcher 的详细文档:searcher.ejlchina.com/
最后,再来个 Demo 地址:
Spring Boot 框架中使用 demo
github.com/ejlchina/be…
gitee.com/ejlchina-zh…
Grails 框架中使用 demo
github.com/ejlchina/be…
gitee.com/ejlchina-zh…
代码,也喜欢纯手工的,因为这样才能造出真正的艺术品。
资源获取:
大家点赞、收藏、关注、评论啦 、查看👇🏻👇🏻👇🏻微信公众号获取联系方式👇🏻👇🏻👇🏻
精彩专栏推荐订阅:在下方专栏👇🏻👇🏻👇🏻👇🏻