揭秘 Bean Searcher 与 MyBatis Plus 之争:谁才是真正的王者!
点击关注公众号,Java干货及时送达👇
Bean Searcher 号称 任何复杂的查询都可以 一行代码搞定,但 Mybatis Plus 似乎也有类似的动态查询功能,它们有怎样的区别呢?
区别一(基本)
Mybatis Plus 依赖 MyBatis, 功能 CRUD 都有,而 Bean Seracher 不依赖任何 ORM,只专注高级查询。
❝
只有使用 MyBatis 的项目才会用 Mybatis Plus,而使用 Hibernate,Data Jdbc 等其它 ORM 的人则无法使用 Mybatis Plus。但是这些项目都可以使用 Bean Searcher(可与任何 ORM 配合使用,也可单独使用)。
❞
使用 Mybatis Plus 需要编写实体类 和 Mapper 接口,而 Bean Searcher 只需编写 实体类,无需编写任何接口。
❝
这个区别意义其实不大,因为如果你用 Mybatis Plus,在增删改的时候还是需要定义 Mapper 接口。
❞
区别二(高级查询)
Mybatis Plus 的 字段运算符 是静态的,而 Bean Searcher 的是动态的。
❝
字段运算符指的是某字段参与条件时用的是 =、> 亦或是 like 这些条件类型。不只 Mybatis Plus,一般的传统 ORM 的字段运算符都是静态的,包括 Hibernate、Spring data jdbc、JOOQ 等。
❞
下面举例说明。对于只有三个字段的简单实体类:
public class User {
private long id;
private String name;
private int age;
// 省略 Getter Setter
}
1)使用MyBatisPlus查询:
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.2
首先要写一个 Mapper 接口:java
public interface UserMapper extends BaseMapper<User> {
}
然后在 Controller 里写查询接口:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/mp")
public List<User> mp(User user) {
return userMapper.selectList(new QueryWrapper<>(user));
}
}
此时这个接口可以支持 三个检索参数,id, name, age,例如:
GET /user/mp? name=Jack 查询 name 等于 Jack 的数据 GET /user/mp? age=20 查询 age 等于 20 的数据
但是他们所能表达的关系都是 等于,如果你还想查询 age > 20 的数据,则无能为力了,除非在实体类的 age 字段上加上一条注解:
@TableField(condition = "%s>#{%s}")
private int age;
但加了注解后,age 就 只能 表达 大于 的关系了,不再可以表达 等于了。所以说,MyBatit Plus 的 字段运算符 是 静态 的,不能由参数动态指定。
基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。
GitHub 地址:https://github.com/weiwosuoai/WeBlog Gitee 地址:https://gitee.com/AllenJiang/WeBlog
❝
当然我们可以在 Controller 里根据参数调用 QueryWrapper 的不同方法让它支持,但这样代码就不只一行了,检索的需求越复杂,需要编写的代码就越多了。
❞
2)使用BeanSearcher查询:
依赖:
implementation 'cn.zhxu:bean-searcher-boot-starter:4.1.2'
不用编写任何接口,复用同一个实体类,直接进行查询:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private BeanSearcher beanSearcher;
@GetMapping("/bs")
public List<User> bs(@RequestParam Map<String, Object> params) {
// 你是否对入参 Map 有偏见?如果有,请耐心往下看,有方案
return beanSearcher.searchList(User.class, params);
}
}
此时这个接口可以支持的检索参数就非常多了:
GET /user/bs? name=Jack 查询 name 等于 Jack 的数据 GET /user/bs? name=Jack & name-ic=true 查询 name 等于 Jack 时 忽略大小写 GET /user/bs? name=Jack & name-op=ct 查询 name 包含 Jack 的数据 GET /user/bs? age=20 查询 age 等于 20 的数据 GET /user/bs? age=20 & age-op=gt 查询 age 大于 20 的数据 等等...
可以看出,Bean Searcher 对每个字段使用的 运算符 都可以由参数指定,它们是 动态 的。
❝
无论查询需求简单还是复杂,Controller 里都只需一行代码。参数 xxx-op 可以传哪些值?参阅这里:bs.zhxu.cn/guide/lates…
❞
看到这里,如果看的明白,应该有一半的读者开始感慨:好家伙,这不是把后端组装查询条件的过程都甩给了前端?谁用了这个框架,不会被前端打死吗?
哈哈,我是不是道出了你现在心里的想法?如果你真的如此想,请仔细回看我们正在讨论的主题:【高级查询】!如果 不能理解什么是高级查询,我再贴个图助你思考:
当然也并不是所有的检索需求都如此复杂,当前端不需要控制检索方式时,xxx-op 参数 可以省略,省略时,默认表达的是 等于,如果你想表达 其它方式,只需一个注解即可,例如:
@DbField(onlyOn = GreaterThan.class)
private int age;
这时,当前端只传一个 age 参数时,执行的 SQL 条件就是 age > ? 了,并且即使前端多传一个 age-op 参数,也不再起作用了。
❝
这其实是条件约束,下文会继续讲到。
❞
区别三(逻辑分组)
就上文所例的代码,除却运算符 动静 的区别,Mybatis Plus 对接收到的参数生成的条件 都是且的关系,而 Bean Searcher 默认也是且,但支持 逻辑分组。
再举例说明,假设查询条件为:
❝
( name = Jack 并且 age = 20 ) 或者 ( age = 30 )
❞
此时,MyBatis Plus 的一行代码就无能为力了,但 Bean Searcher 的一行代码仍然管用,只需这样传参即可:
GET /user/bs? a.name=Jack & a.age=20 & b.age=30 & gexpr=a|b
这里 Bean Searcher 将参数分为 a, b 两组,并用新参数 gexpr 来表达这两组之间的关系(a 或 b)。
❝
实际传参时gexpr的值需要 URLEncode 编码一下:
URLEncode('a|b') => 'a%7Cb'
,因为 HTTP 规定参数在 URL 上不可以出现 | 这种特殊字符。当然如果你喜欢 POST, 可以将它放在报文体里。❞
什么场景下适合使用此功能?当遇见类似下图中的需求时,它将助你一招制敌:
分组功能非常强大,但如此复杂的检索需求也确实罕见,这里不再细述,详情可阅:bs.zhxu.cn/guide/lates…
区别四(多表联查)
在不写 SQL 的情况下,Mybatis Plus 的动态查询 仅限于 单表,而 Bean Searcher 单表 和 多表 都支持的一样好。
这也是很重要的一点区别,因为 大多数高级查询 场景都是 需要联表 的。
❝
当然有些人坚持用单表做查询,为了避免联表,从而在主表中冗余了很多字段,这不仅造成了 数据库存储空间压力急剧增加,还让项目更加难以维护。因为源数据一但变化,你必须同时更新这些冗余的字段,只要漏了一处,BUG 就跳出来了。
❞
还是举个例子,某订单列表需要展示 订单号,订单金额,店铺名,买家名 等信息,用 Bean Searcher 实体类可以这么写:
@SearchBean(
tables = "order o, shop s, user u", // 三表关联
where = "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
}
❝
有心的同学会注意到,这个实体类的命名并不是 Order, 而是 OrderVO。这里只是一个建议的命名,因为它是本质上就是一个 VO(View Object),作用只是一个视图实体类,所以建议将它和普通的单表实体类放在不同的 package 下(这只是一个规范)。
❞
然后我们的 Controller 中仍然只需一行代码:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private BeanSearcher beanSearcher;
@GetMapping("/index")
public SearchResult<OrderVO> index(@RequestParam Map<String, Object> params) {
// search 方法同时会返回满足条件的总条数
return beanSearcher.search(OrderVO.class, params);
}
}
这就实现了一个支持高级查询的 订单接口,它同样支持在上文 区别二 与 区别三 中所展示的各种检索方式。
❝
从本例可以看出,Bean Searcher 的检索结果是 VO 对象,而非普通的单表实体类(DTO),这 省去了 DTO 向 VO 的转换过程,它可以直接返回给前端。
❞
区别五(使用场景)
在事务性的接口用推荐使用 MyBatis Plus, 非事务的检索接口中推荐使用 Bean Searcher
例如 创建订单接口,在这个接口内部同样有很多查询,比如你需要查询 店铺的是否已经打烊,商品的库存是否还足够等,这些查询场景,推荐依然使用 原有的 MyBatis Plus 或其它 ORM 就好,不必再用 Bean Seracher 了。 再如 订单列表接口,纯查询,可能需要分页、排序、过滤等功能,此时就可用 Bean Seracher 了。 基于 Spring Boot + MyBatis Plus + Vue 3.2 + Vite + Element Plus 实现的前后端分离博客,包含后台管理系统,支持文章、分类、标签管理、仪表盘等功能。
GitHub 地址:https://github.com/weiwosuoai/WeBlog Gitee 地址:https://gitee.com/AllenJiang/WeBlog
网友质疑
1)这貌似开放很大的检索能力,风险可控吗?
Bean Searcher 默认对实体类中的每个字段都支持了很多种检索方式,但是我们也可以对它进行约束。
条件约束
例如,User 实体类的 name 字段只允许 精确匹配 与 后模糊 查询,则在 name 字段上添加一个注解即可:
@DbField(onlyOn = {Equal.class, StartWith.class})
private String name;
再如:不允许 age 字段参与 where 条件,则可以
@DbField(conditional = false)
private int age;
排序约束
Bean Searcher 默认允许按所有字段排序,但可以在实体类里进行约束。例如,只允许按 age 字段降序排序:
@SearchBean(orderBy = "age desc", sortType = SortType.ONLY_ENTITY)
public class User {
// ...
}
或者,禁止使用排序
@SearchBean(sortType = SortType.ONLY_ENTITY)
public class User {
// ...
}
2)使用 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 实体类来接收呢?因为 Bean Searcher 默认支持很多参数,而原有的 User 实体类中的字段不够多,用它来接收的话会有很多参数接收不到。如果咱们的检索需求比较简单,不需要前端指定那些参数,则可以直接使用 User 实体类来接收。
❞
这里的 UserQuery 可以这么定义:
// 继承 User 里的字段
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 方法
}
然后 Utils 工具类的 toMap 方法可以这样写(这个工具类是通用的):
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;
}
这样就可以了,该接口依然可以支持很多种检索方式:
GET /user/bs? name=Jack 查询 name 等于 Jack 的数据 GET /user/bs? name=Jack & name_ic=true 查询 name 等于 Jack 时 忽略大小写 GET /user/bs? name=Jack & name_op=ct 查询 name 包含 Jack 的数据 GET /user/bs? age=20 查询 age 等于 20 的数据 GET /user/bs? age=20 & age_op=gt 查询 age 大于 20 的数据 等等...
❝
注意使用参数是 name_op,不再是 name-op 了
❞
以上的方式应该满足了一些强迫症患者的期望,但是这样的代价是多写一个 UserQuery 类,这不禁让我们细想:这样做值得吗?
当然,写成这样是有一些好处的:
便于参数校验 便于生成接口文档
但是:
这是一个 非事务性 的检索接口,参数校验真的那么必要吗?本来就可以无参请求,参数传错了系统自动忽略它是不是也可以? 如果了解了 Bean Searcher 参数规则,是不是不用这个 UserQuery 类也可以生成文档,或者在文档中一句话概括 该接口是 Bean Searcher 检索接口,请按照规则传递参数,是不是也行呢?
所以,我的建议是:一切以真实需求为准则,不要为了规范而去规范,莫名徒增代码。
看到这里,你可能已经在心里准备了一大堆的话想要反驳我,别急,我们先回顾一下前面的 [多表联查] 章节所提到的:
Bean Searcher 中的实体类(SearchBean),实际上是一个可以 直接与 DB 有跨表映射关系 的 VO(View Ojbect),它代表一种检索业务,在概念上它与传统 ORM 的实体类(Entity)或 域类(Domain)有着本质的区别!
这一句话,道出了 Bean Searcher 的灵魂。它看似简单,实则难以悟透。如果您不是那种千古罕见先天通灵的奇才,强烈建议阅读官方文档的 介绍 > 设计思想(出发点) 章节,那里有对这一句话的详细解释。
3)想手动添加或修改参数,只能向 Map 里 put 吗?有没有优雅点写法?
答:当然有。Bean Searcher 提供了一个 参数构建器,可让后端人员想手动添加或修改检索参数时使用。例如:
@GetMapping("/bs")
public List<User> bs(@RequestParam Map<String, Object> params) {
params = MapUtils.builder(params) // 在原有参数基础之上
.field(User::getAge, 20, 30).op(Between.class) // 添加一个年龄区间条件
.field(User::getName).op(StartWith.class) // 修改 name 字段的运算符为 StartWith,参数值还是用前端传来的参数
.build();
return beanSearcher.searchList(User.class, params);
}
4)前端乱传参数的话,存在 SQL 注入风险吗?
答:不存在的,Bean Searcher 是一个 只读 ORM,它也存在 对象关系映射,所传参数都是实体类内定义的 Java 属性名,而非数据库表里的字段名(当前端传递实体类未定义的字段参数时,会被自动忽略)。
❝
也可以说:检索参数与数据库表是解耦的。
❞
5)可以随意传参,会让用户获取本不该看到的数据吗?
答:不会的,因为用户 可获取数据最多的请求就是无参请求,用户尝试的任何参数,都只会缩小数据范围,不可能扩大。
❝
如果想做 数据权限,根据不同的用户返回不同的数据:可在 参数过滤器 里为权限字段统一注入条件(前提是 实体类中得有一个数据权限字段,可以在基类中定义)。
❞
6)效率虽有提高,但性能如何呢?
前段时间又不少朋友看了这篇文章私下问我 Bean Searcher 的性能如何,这个周末我就在家做了下对比测试,结果如下:
比 Spring Data Jdbc 高 5 ~ 10 倍 比 Spring Data JPA 高 2 ~ 3 倍 比 原生 MyBatis 高 1 ~ 2 倍 比 MyBatis Plus 高 2 ~ 5 倍
7)支持哪些数据库呢?
只要支持正常的 SQL 语法,都是支持的,另外 Bean Searcher 内置了四个方言实现:
分页语法和 MySQL 一样的数据库,默认支持 分页语法和 PostgreSql 一样的数据库,选用 PostgreSql 方言 即可 分页语法和 Oracle 一样的数据库,选用 Oracle 方言 即可 分页语法和 SqlServer(v2012+)一样的数据库,选用 SqlServer 方言 即可
如果分页语法独创的,自定义一个方言,只需实现一个方法即可,参考:高级 > SQL 方言 章节。
总结
上文所述的各种区别,并不是说 MyBatis Plus 和 Bean Searcher 哪个好哪个不好,而是它们 专注的领域 确实不一样(BS 也不会替代 MP)。
❝
Bean Searcher 在刚诞生的时候是专门用来处理那种特别复杂的检索需求(如上文中的例图所示),一般都用在 管理后台 系统里。但用着用着,我们发现,对检索需求没那么复杂的普通分页查询接口,Bean Searcher 也非常好用。代码写起来比用传统的 ORM 要简洁的多,只需一个实体类和 Controller 里的几行代码,Service 和 Dao 什么的全都消失了,而且它返回的结果就是 VO, 也 不需要再做进一步的转换 了,可以直接返回给前端。
❞
在项目中配合使用它们,事务中使用 MyBatis Plus,列表检索场景使用 Bean Searcher,你将 如虎添翼。
❝
实际上,在旧项目中集成 Bean Searcher 更加容易,已有的单表实体类都能直接复用,而多表关联的 VO 对象类也只需添加相应注解即可拥有强大的检索能力。无论项目原来 ORM 用的是 MyBatis, MP, 还是 Hibernate,Data Jdbc 等,也无论 Web 框架是 Spring Boot, Spring MVC 还是 Grails 或 Jfinal 等,只要是 java 项目, 都可以用它,为系统赋能高级查询。
❞
来源:juejin.cn/post/7092411551507808264
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦