对一个 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 :
<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
检索器了。
{
"dataList": [ // 用户列表,默认返回第 0 页,默认分页大小为 15 (可配置)
{ "id": 1, "name": "Jack", "age": 25, "roleId": 1, "role": "普通用户" },
{ "id": 2, "name": "Tom", "age": 26, "roleId": 1, "role": "普通用户" },
...
],
"totalCount": 100 // 用户总数
}
复制代码
参数名
size
和page
可自定义,page
默认从0
开始,同样可自定义,并且可与其它参数组合使用
参数名
sort
和order
可自定义,可与其它参数组合使用
{
"dataList": [ // 用户列表,默认返回第 0 页(只包含 id,name,role 字段)
{ "id": 1, "name": "Jack", "role": "普通用户" },
{ "id": 2, "name": "Tom", "role": "普通用户" },
...
],
"totalCount": 100 // 用户总数
}
复制代码
参数名
onlySelect
和selectExclude
可自定义,可与其它参数组合使用
参数
age-op = eq
表示age
的 字段运算符 是eq
(Equal
的缩写),表示参数age
与参数值20
之间的关系是Equal
,由于Equal
是一个默认的关系,所以age-op = eq
也可以省略
参数名 age-op
的后缀 -op
可自定义,且可与其它字段参数 和 上文所列的参数(分页、排序、指定字段)组合使用,下文所列的字段参数也是一样,不再复述。
ne
是 NotEqual
的缩写)ge
是 GreateEqual
的缩写)le
是 LessEqual
的缩写)gt
是 GreateThan
的缩写)lt
是 LessThan
的缩写)bt
是 Between
的缩写)参数
age-0 = 20
表示age
的第 0 个参数值是20
。上述提到的age = 20
实际上是age-0 = 20
的简写形式。另:参数名age-0
与age-1
中的连字符-
可自定义。
mv
是 MultiValue
的缩写,表示有多个值的意思)in
是 Include
的缩写)sw
是 StartWith
的缩写)sw
是 EndWith
的缩写)ey
是 Empty
的缩写)ny
是 NotEmpty
的缩写)ic
是 IgnoreCase
的缩写)参数名
name-ic
中的后缀-ic
可自定义,该参数可与其它的参数组合使用,比如这里检索的是 name 等于 Jack 时忽略大小写,但同样适用于检索 name 以 Jack 开头或结尾时忽略大小写。
查询 name 以 Jack (忽略大小写) 开头,且 roleId = 1,结果以 id 字段排序,每页加载 10 条,查询第 2 页:
OK,效果看完了,/user/index
接口里我们确实只写了一行代码,它便可以支持这么多种的检索方式,有没有觉得现在 你写的一行代码 就可以 干过别人的一百行 呢?
本例中,我们只使用了 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)
查询指定条件下 所有 数据 列表另外,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
只能有两种检索方式,如果再严格一点,只允许 精确匹配,那其实有两种写法。
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {
@DbField(onlyOn = Operator.Equal)
private String name;
// 为减少篇幅,省略其它字段...
}
复制代码
@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; // 返回过滤后的检索参数
}
};
}
复制代码
-
,不喜欢 op
、ic
后缀,完全可以自定义,参考这篇文档:https://searcher.ejlchina.com/guide/latest/params.html#字段参数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;
// 为减少篇幅,省略其它字段...
}
复制代码
其实,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 在复杂列表检索领域的超强能力,但由于篇幅所限,本文所述仍只是冰山一角,比如它还:
https://github.com/ejlchina/bean-searcher