我这样写代码,比直接使用 MyBatis 效率提高了100倍!
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
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 :
<dependency>
<groupId>com.ejlchinagroupId>
<artifactId>bean-searcher-boot-starterartifactId>
<version>3.0.1version>
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
上述代码中的
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
接口里我们确实只写了一行代码,它便可以支持这么多种的检索方式,有没有觉得现在 你写的一行代码 就可以 干过别人的一百行 呢?
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 params = MapUtils.builder()
.field(User::getName, "Jack").op(Operator.StartWith).ic()
.field(User::getRoleId, 1)
.orderBy(User::getId, "asc")
.page(2, 10)
.build()
List 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
条件约束
该同学又:哎呀!我的数据量还是很大,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 Map doFilter(BeanMeta beanMeta, Map paraMap) {
// beanMeta 是正在检索的实体类的元信息, paraMap 是当前的检索参数
// TODO: 这里可以添加一下自定义的参数过滤规则
return paraMap; // 返回过滤后的检索参数
}
};
}
复制代码
某同学问
参数咋这么怪,这么多呢,和前端有仇呀
参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线 -
,不喜欢op
、ic
后缀,完全可以自定义,参考这篇文档:https://searcher.ejlchina.com/guide/latest/params.html#字段参数
参数个数的多少,其实是和需求的复杂程度相关,如果需求很简单,其实很多参数没必要让前端传,后端直接塞进去就好,比如: 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
类型的参数,至于这个参数是怎么来的,和 Bean Searcher 并没有直接关系。前文之所以从 request
里取,只是因为这样代码看起来简洁,如果你喜欢声明参数,完全可以把代码写成这样:
@GetMapping("/index")
public SearchResult
结语
本文介绍了 Bean Searcher 在复杂列表检索领域的超强能力,但由于篇幅所限,本文所述仍只是冰山一角,比如它还:
支持 嵌入参数 支持 字段转换器 支持 Sql 拦截器 支持 多数据源 支持 自定义注解 等等
项目 GitHub 地址
https://github.com/ejlchina/bean-searcher
作者:乔伊酱
来源:https://juejin.cn/post/7027733039299952676
加锋哥微信: java1239 围观锋哥朋友圈,每天推送Java干货!