这样写代码,比直接使用 MyBatis 效率提高了 100 倍 !
Java之间
共 4181字,需浏览 9分钟
·
2021-12-20 06:14
1、五年工作经验总结 16 条的代码规范 2、突发!Apache Log4j2 报核弹级漏洞 3、都在建议你不要直接使用 @Async 注解,为什么? 4、面试官:抛开Spring来说,如何自己实现Spring AOP? 5、蓝绿发布、滚动发布、灰度发布,有什么区别?这下明白了
MyBatis
、Hibernate
、Data Jdbc
等都是我们常用的 ORM 框架。它们有时候很好用,比如简单的 CRUD,事务的支持都非常棒。首先数据库有两张表
接下来我们要实现一个用户查询的功能
可按用户名
字段查询,要求:
可精确匹配(等于某个值) 可全模糊匹配(包含给定的值) 可后模糊查询(以...开头) 可前模糊查询(以.. 结尾) 可指定以上四种匹配是否可以忽略大小写 可按年龄
字段查询,要求:
可精确匹配(等于某个年龄) 可大于匹配(大于某个值) 可小于匹配(小于某个值) 可区间匹配(某个区间范围) 可按
角色ID
查询,要求:精确匹配可按
用户ID
查询,要求:同年龄
字段可指定只输出哪些列(例如,只查询
ID
与用户名
列)支持分页(每次查询后,页面都要显示满足条件的用户总数)
查询时可选择按
ID
、用户名
、年龄
等任意字段排序
后端接口该怎么写呢?
MyBatis
、Hibernate
、Data Jdbc
直接来写的话,100 行代码 能实现吗?手把手:只一行代码实现以上需求
添加依赖
<dependency>
<groupId>com.ejlchinagroupId>
<artifactId>bean-searcher-boot-starterartifactId>
<version>3.1.2version>
dependency>
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 ...
}
接着就可以写用户查询接口了
@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 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 开头或结尾时忽略大小写。
当然,以上各种条件都可以组合,例如
GET /user/index? name=Jack & name-op=sw & name-ic=true & roleId=1 & sort=id & size=10 & page=2 返回结果:结构同 (1)
/user/index
接口里我们确实只写了一行代码,它便可以支持这么多种的检索方式,有没有觉得现在 你写的一行代码 就可以 干过别人的一百行 呢?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
MapSearcher
检索器外,还提供了 BeanSearcher
检索器,它同样拥有 MapSearcher
所有的方法,只是它返回的单条数据不是 Map
,而是一个 泛型 对象。参数构建工具
Map
类型的参数可能不太优雅,为此, Bean Searcher 特意提供了一个参数构建工具。Map params = MapUtils.builder()
.field(User::getName, "Jack").op(Operator.StartWith).ic()
.field(User::getRoleId, 1)
.orderBy(User::getId, "asc")
.page(2, 10)
.build()
Listusers = beanSearcher.searchList(User.class, params);
这里使用的是 BeanSearcher
检索器,以及它的searchList(Class
方法。beanClass, Map params)
运算符约束
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
只能适用与 精确匹配 和 后模糊查询,其它检索方式它将直接忽略。Spring Boot 学习笔记,这个分享给你。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
条件约束
@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
public ParamFilter myParamFilter() {
return new ParamFilter() {
@Override
publicMap doFilter(BeanMeta {beanMeta, Map paraMap)
// beanMeta 是正在检索的实体类的元信息, paraMap 是当前的检索参数
// TODO: 这里可以写一些自定义的参数过滤规则
return paraMap; // 返回过滤后的检索参数
}
};
}
某同学问
参数咋这么怪,这么多呢,和前端有仇么
参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线 -
,不喜欢op
、ic
后缀,完全可以自定义,参考这篇文档:
参数个数的多少,其实是和需求的复杂程度相关的。如果需求很简单,那么很多参数没必要让前端传,后端直接塞进去就好。比如: 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;
// 为减少篇幅,省略其它字段...
}
对于 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 文档不好渲染了呀
Map
类型的参数,至于这个参数是怎么来的,和 Bean Searcher 并没有直接关系。前文之所以从 request
里取,只是因为这样代码看起来简洁,如果你喜欢声明参数,完全可以把代码写成这样:@GetMapping("/index")
public SearchResult
字段参数之间的关系都是 “且” 呀,那 “或” 呢?“且” “或” 任意组合呢?
https://github.com/ejlchina/bean-searcher/issues/8
开发效率真的提高 100 倍了吗?
结语
支持 聚合查询 支持 Select|Where|From子查询 支持 实体类嵌入参数 支持 字段转换器 支持 Sql 拦截器 支持 数据库 Dialect 扩展 支持 多数据源 支持 自定义注解 等等
Bean Searcher 是我在工作中总结封装出来的一个小工具,公司内部使用了 4 年,经历大小项目三四十个,只是最近才着手完善文档分享给大家,如果你喜欢,一定去点个 Star 哦 ^_^。
最近热文阅读:
1、五年工作经验总结 16 条的代码规范 2、为什么我劝你放弃了Restful API? 3、Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合 4、公司规定所有接口都用 POST请求,这是为什么? 5、为什么阿里强制 boolean 类型变量不能使用 is 开头? 6、面试官:InnoDB中一棵B+树可以存放多少行数据? 7、MyBatis批量插入几千条数据,请慎用foreach 8、有了 for (;;) ,为什么还需要while (true) ?到底哪个更快? 9、名企公开挂“加班真好”标语,员工称一年被免费“白嫖”600多小时!网友看不下去了,稽查部门展开调查... 10、面试官:为什么 Java 不把基本类型放在堆中?我竟然答不上来。。 关注公众号,你想要的Java都在这里
评论