一、引入
对于一个 Java后端程序员来说,mybatis、hibernate、data-jdbc 等都是我们常用的 ORM 框架。它们有时候很好用,比如简单的 CRUD,事务的支持都非常棒。但有时候用起来也非常繁琐,比如接下来我们要聊到的一个常见的开发需求,最后本文给出一个比直接试用这些ORM 开发效率至少会 提高 100 倍数的方法。
天然支持多表关联:searcher.ejlchina.com/guide/lates… 。
二、数据库有两张表
(1)用户表
| 字段名 | 类型 | 含义 |
|---|---|---|
| id | bitint | 用户 ID |
| name | varchar(45) | 用户名 |
| age | int | 年龄 |
| role_id | int | 角色 ID |
(2)角色表
| 字段名 | 类型 | 含义 |
|---|---|---|
| id | int | 角色 ID |
| name | varchar(45) | 角色名 |
三、适配多样的查询需求
(1)根据 用户名 字段查询,做到:
- 可精确匹配(等于某个值)
- 可全模糊匹配(包含给定的值)
- 可后模糊查询(以……开头)
- 可前模糊查询(以……结尾)
- 可指定以上四种匹配是否可以忽略大小写。
(2)根据 年龄 字段查询,做到:
- 可精确匹配(等于 某个年龄)
- 可大小匹配(大于 某个值)
- 可小于匹配(小于 某个值)
- 可区间匹配(某个区间范围)
(3)根据 角色ID 查询,做到:精确匹配
(4)根据 用户ID 查询,做到:同年龄字段
(5)可指定只输出哪些列(例如,只查询 ID 与 用户名 列)
(6)支持分页(每次查询后,页面都要显示满足条件的用户总数)
(7)查询时可选择 根据 ID、用户名、年龄 等任意字段排序
四、实现需求
(1)添加 BeanSearcher 依赖
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'
(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 int role; // 角色名(r.name)
// Getter and Setter ...
}
(3)示例:书写用户查询接口
@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
- 返回结果:结构同 (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 开头或结尾时忽略大小写。
(19)当然,以上各种条件都可以组合,例如
查询 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 方法。
(1)检索方法
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)查询指定条件下 所有 数据 列表
(2)MapSearcher 与 BeanSearcher
另外,Bean Searcher 除了提供了 MapSearcher 检索器外,还提供了 BeanSearcher 检索器,它同样拥有 MapSearcher 拥有的方法,只是它返回的单条数据不是 Map,而是一个 泛型 对象。
(3)参数构建工具
另外,如果你是在 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) 方法。
(3)运算符约束
上文我们看到,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 只能有两种检索方式,如果再严格一点,只允许 精确匹配,那其实有两种写法。
① 还是使用运算符约束
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(onlyOn = Operator.Equal)
private String name;
// 为减少篇幅,省略其它字段...
}
② 在 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);
}
(4)条件约束
该同学又:哎呀!我的数据量还是很大,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 都不搭理。
(6)参数过滤器
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)参数命名怪,前端不好理解
-
参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线
-,不喜欢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) .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; // 为减少篇幅,省略其它字段... }
(2)入参是 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.com/ejlchina/be…
【注意:本质上工作量没有变化,后端书写了一个接口,将后端参数的限制转移给了前端按照框架内的传参要求进行对接。】
附录
- 项目 Github 地址:github.com/ejlchina/be…
- 天然支持多表关联:searcher.ejlchina.com/guide/lates…