Bean Search 超级好用的搜索工具
1、引入依赖
<dependency>
<groupId>cn.zhxu</groupId>
<artifactId>bean-searcher-boot-starter</artifactId>
<version>4.1.2</version>
</dependency>
2、定义实体类
- autoMapTo: 若不指定别名,自动映射的表
- orderBy:排序字段,如果数据量大,不建议加,因为他是全表排序后再取页数
- JsonFormat:日期格式化
@SearchBean(tables = "staff_dict s left join dept_dict d on d.dept_code=s.dept_code",
autoMapTo = "s",
orderBy="name",
sortType = SortType.ALLOW_PARAM)
@Data
public class Staff {
private String empNo;
private String name;
private String userName;
@DbField("getpwd(password)")
private String password;
@DbField("d.dept_name")
private String deptName;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createDate;
}
3、controller
@RestController
@RequestMapping("emp")
public class TestController {
@Autowired
private MapSearcher mapSearcher;
@GetMapping("index")
public Object list(@RequestParam Map<String, Object> params){
// 组合检索、排序、分页 和 统计 都在这一句代码中实现了
return mapSearcher.search(Emp.class, params, Emp::getSal);
}
@GetMapping("staff")
public Object staff(@RequestParam Map<String, Object> params){
// 组合检索、排序、分页 和 统计 都在这一句代码中实现了
return mapSearcher.search(Staff.class, params,Staff::getEmpNo);
}
}
4、生成的sql
select *
from (select row_.*, rownum rownum_
from (select s.emp_no c_0,
s.name c_1,
s.user_name c_2,
getpwd(password) c_3,
d.dept_name c_4
from staff_dict s
left join dept_dict d
on d.dept_code = s.dept_code
order by name) row_
where rownum <= 30) table_
where table_.rownum_ > 10
5、返回值
{
"totalCount": 893,
"dataList": [
{
"empNo": "1012",
"name": "张三",
"userName": "1012",
"password": "1",
"deptName": "外科疗区",
"createDate": "2022-04-13 17:12:56"
}
],
"summaries": [
2322726
]
}
6、注意点
- 指定起始页,不配置默认为0,这里配置为1,是为了兼容element UI的分页组件
- 默认分页使用的是mysql,其他分页请指定方言
- 转换器只对MapSearcher有效
Bean Searcher
除了提供了MapSearcher
检索器外,还提供了BeanSearcher
检索器,它同样拥有MapSearcher
拥有的方法,只是它返回的单条数据不是Map
,而是一个泛型
对象。
bean-searcher:
params:
pagination:
# 起始页,不配置默认为0,这里配置为1,是为了兼容element UI的分页组件
start: 1
sql:
dialect: Oracle
field-convertor:
date-formats:
# 只对MapSearcher有效
cn.tjhis.domain.Staff: yyyy-MM-dd HH:mm:ss
7、请求简写说明
配置键名 | 含义 | 可选值 |
---|---|---|
bean-searcher.params.separator | 字段参数名分隔符 字符串 | - |
bean-searcher.params.operator-key | 字段运算符参数名后缀字符串 | op |
bean-searcher.params.ignore-case-key | 是否忽略大小写字段参数名后缀 字符串 | ic |
// 更多参考 https://bs.zhxu.cn/guide/latest/params.html#%E5%AD%97%E6%AE%B5%E8%BF%90%E7%AE%97%E7%AC%A6
numOps: [
{ key: 'eq', label: '等于',english:'Equal'},
{ key: 'ne', label: '不等于',english:'NotEqual'},
{ key: 'gt', label: '大于',english:'GreateThan'},
{ key: 'lt', label: '小于',english:'LessThan'},
{ key: 'ge', label: '大于等于',english:'GreateEqual'},
{ key: 'le', label: '小于等于',english:'LessEqual'},
{ key: 'bt', label: '区间',english:'Between'},
{ key: 'in', label: '包含',english:'in'},
{ key: 'ct', label: '包含',english:'Contain'},
{ key: 'ey', label: '空 或 null',english:'Empty'},
{ key: 'ny', label: '非空',english:'NotEmpty'},
{ key: 'sw', label: '以...开始',english:'StartWith'},
{ key: 'ew', label: '以...结束',english:'EndWith'}
],
8、请求格式
- GET /user/index:无参数,默认返回第 1 页,默认分页大小为 15 (可配置)
bean-searcher.params.pagination.page
和bean-searcher.params.pagination.size
- GET /user/index? page=2 & size=10 返回结果:结构同 (1)(只是每页 10 条,返回第 2 页)
- GET /user/index? sort=age & order=desc 返回结果:结构同 (1)(只是 dataList 数据列表以 age 字段降序输出)
- GET /user/index? age=20 & age-op=eq 返回结果:结构同 (1)(但只返回 age=20 的数据) age-op=eq 也可以省略
- GET /user/index? age=20 & age-op=ne 返回结果:结构同 (1)(但只返回 age != 20 的数据,ne 是 NotEqual 的缩写)
- GET /user/index? age-0=20 & age-1=30 & age-op=bt 返回结果:结构同 (1)(但只返回 20 <= age <= 30 的数据,bt 是 Between 的缩写)
- GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=il 返回结果:结构同 (1)(但只返回 age in (20, 30, 40) 的数据,il 是 InList 的缩写)
- GET /user/index? name-op=ey 返回结果:结构同 (1)(但只返回 name 为空 或为 null 的数据,ey 是 Empty 的缩写)
- GET /user/index? name=Jack & name-ic=true 返回结果:结构同 (1)(但只返回 name 等于 Jack (忽略大小写) 的数据,ic 是 IgnoreCase 的缩写)
- GET /user/bs? a.name=Jack & a.age=20 & b.age=30 & gexpr=a|b 实际传参时gexpr的值需要 URLEncode 编码一下: URLEncode(‘a|b’) => ‘a%7Cb’,因为 HTTP 规定参数在 URL 上不可以出现 | 这种特殊字符。当然如果你喜欢 POST, 可以将它放在报文体里。
9、打印日志
- 包名必须为:cn.zhxu.bs 不能修改
logging:
level:
cn.zhxu.bs: DEBUG
- 如果需要日志文件也打印,配置logback.xml
<logger name="cn.zhxu.bs" level="DEBUG" additivity="false">
<appender-ref ref="console" />
<appender-ref ref="file" />
</logger>
10、其他检索
- searchAll:所有数据 []
- searchCount:查询指定条件下的数据 总条数 纯数值
- searchSum:查询指定条件下的 某字段 的 统计值 纯数值
return mapSearcher.search(Staff.class, params,new String[] {"empNo","id"});
- search:带分页的查询(包含数据总数以及合计域)
- searchList:带分页的查询,返回值是数据集的数组 []
11、方法
-
Bean Searcher 本例中,我们只使用了 Bean Searcher 提供的 MapSearcher 检索器的一个 search 方法,其实,它有很多 search 方法。
-
检索方法
- searchCount(Class
beanClass, Map<String, Object> params) 查询指定条件下的数据 总条数 - searchSum(Class
beanClass, Map<String, Object> params, String field) 查询指定条件下的 某字段 的 统计值 - searchSum(Class
beanClass, Map<String, Object> params, String[] fields) 查询指定条件下的 多字段 的 统计值 - search(Class
beanClass, Map<String, Object> params) 分页 查询指定条件下数据 列表 与 总条数 - search(Class
beanClass, Map<String, Object> params, String[] summaryFields) 同上 + 多字段 统计 - searchFirst(Class
beanClass, Map<String, Object> params) 查询指定条件下的 第一条 数据 - searchList(Class
beanClass, Map<String, Object> params) 分页 查询指定条件下数据 列表 - searchAll(Class
beanClass, Map<String, Object> params) 查询指定条件下 所有 数据 列表 - searchFirst:返回满足条件的第一条,{}
- searchCount(Class
-
如果你是在
Service
里使用Bean Searcher
,那么直接使用Map<String, Object>
类型的参数可能不太优雅,为此,Bean Searcher
特意提供了一个参数构建工具。
@Service
public class StaffService {
@Autowired
BeanSearcher beanSearcher;
public List<Staff> getList(){
Map<String, Object> params = MapUtils.builder()
.field(Staff::getName, "崔")
.op("sw").ic()
.field(Staff::getPassword, 1)
.orderBy(Staff::getId, "asc")
.page(2, 10)
.build();
return beanSearcher.searchList(Staff.class, params);
}
}
- 参数个数的多少,其实是和需求的复杂程度相关的。如果需求很简单,那么很多参数没必要让前端传,后端直接塞进去就好。
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {
Map<String, Object> params = MapUtils.flatBuilder(request.getParameterMap())
.field(User::getName).op(Operator.StartWith)
.field(User::getAge).op(Operator.Between)
.build()
return mapSearcher.search(User.class, params);
}
12、DbField
onlyOn
:索方式太多了,我根本不需要这么多,通过onlyOn
限定,只接受FieldOp
的子类Class<? extends FieldOp>[] onlyOn() default {};
conditional
:设置为false,即便传了该参数,也是无效的,不会拼接sql
语句,直接忽略。 无论前端怎么传参,Bean Searcher 都不搭理。- Bean Searcher 默认允许按所有字段排序,但可以在实体类里进行约束。例如,只允许按 age 字段降序排序:
@DbField(onlyOn = {Equal.class, StartWith.class})
private String name;
@DbField(value ="d.dept_name",conditional = false)
private String deptName;
@SearchBean(orderBy = "age desc", sortType = SortType.ONLY_ENTITY)
public class User {
}
// 禁止使用排序
@SearchBean(sortType = SortType.ONLY_ENTITY)
public class User {
}
- 使用 Bean Searcher 后 Controller 的入参必须是 Map 类型? 这 并不是必须的,只是 Bean Searcher 的检索方法接受这个类型的参数而已。如果你在 Controller 入参那里 用一个 POJO 来接收也是可以的,只需要再用一个工具类把它转换为 Map 即可
@GetMapping("/bs")
public List<User> bs(UserQuery query) {
// 将 UserQuery 对象转换为 Map 再传入进行检索
return beanSearcher.searchList(User.class, Utils.toMap(query));
}
// 继承 User 里的字段 当然,写成这样是有一些好处的:1、便于参数校验 ; 2、便于生成接口文档;
public class UserQuery extends User {
// 附加:排序参数
private String order;
private String sort;
// 附加:分页参数
private Integer page;
private Integer size;
// 附加:字段衍生参数
private String id_op; // 由于字段命名不能有中划线,这里有下划线替代
private String name_op; // 前端传参的时候就不能传 name-op,而是 name_op 了
private String name_ic;
private String age_op;
// 省略其它附加字段...
// 省略 Getter Setter 方法
}
public static Map<String, Object> toMap(Object bean) {
Map<String, Object> map = new HashMap<>();
Class<?> beanClass = bean.getClass();
while (beanClass != Object.class) {
for (Field field : beanClass.getDeclaredFields()) {
field.setAccessible(true);
try {
// 将下划线转换为中划线
Strubg name = field.getName().replace('_', '-');
map.put(name, field.get(bean));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
beanClass = beanClass.getSuperclass();
}
return map;
}
13、结束语
Bean Searcher
在复杂列表检索领域的超强能力。它之所以可以极大提高这类需求的研发效率,根本上归功于它 独创 的 动态字段运算符 与 多表映射机制,这是传统 ORM 框架所没有的。Mybatis Plus
依赖MyBatis
, 功能CRUD
都有,而Bean Seracher
不依赖任何ORM
,只专注高级查询。Mybatis Plus
的 字段运算符 是静态的,而Bean Searcher
的是动态的。- 逻辑分组
Mybatis Plus
对接收到的参数生成的条件 都是且的关系,而Bean Searcher
默认也是且,但支持 逻辑分组。gexpr
Mybatis Plus
的动态查询 仅限于 单表,而Bean Searcher
单表 和 多表 都支持的一样好
// 这个实体类的命名并不是 Order, 而是 OrderVO。这里只是一个建议的命名,因为它是本质上就是一个 VO,作用只是一个视图实体类,所以建议将它和普通的单表实体类放在不同的 package 下(这只是一个规范)。
@SearchBean(
tables = "order o, shop s, user u", // 三表关联
joinCond = "o.shop_id = s.id and o.buyer_id = u.id", // 关联关系
autoMapTo = "o" // 未被 @DbField 注解的字段都映射到 order 表
)
public class OrderVO {
private long id; // 订单ID o.id
private String orderNo; // 订单号 o.order_no
private long amount; // 订单金额 o.amount
@DbField("s.name")
private String shop; // 店铺名 s.name
@DbField("u.name")
private String buyer; // 买家名 u.name
// 省略 Getter Setter
}
- 在事务性的接口用推荐使用
MyBatis Plus
, 非事务的检索接口中推荐使用Bean Searcher
- 前端乱传参数的话,存在 SQL 注入风险吗?不存在的,Bean Searcher 是一个 只读 ORM,它也存在 对象关系映射,所传参数都是实体类内定义的 Java 属性名,而非数据库表里的字段名(当前端传递实体类未定义的字段参数时,会被自动忽略)。也可以说:检索参数与数据库表是解耦的。
在项目中配合使用它们,事务中使用 MyBatis Plus
,列表检索场景使用 Bean Searcher
,你将 如虎添翼。
世界上没有什么事情是跑步解决不了的,如果有,那就再跑一会!