• Bean Searcher 与 MyBatis Plus哪个功能更强大?
  • 发布于 2个月前
  • 176 热度
    0 评论
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 的 字段运算符 是静态的,而 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 的 字段运算符 是 静态 的,不能由参数动态指定。

❝当然我们可以在 Controller 里根据参数调用 QueryWrapper 的不同方法让它支持,但这样代码就不只一行了,检索的需求越复杂,需要编写的代码就越多了。❞
2)使用BeanSearcher查询:
依赖:
implementation 'cn.zhxu:bean-searcher-boot-starter:4.1.2'  
不用编写任何接口,复用同一个实体类,直接进行查询:
@RestController  
@RequestMapping("/user")  
public class UserController {  
  
    @Autowired  
    private BeanSearcher beanSearcher;  
     // 堆代码 duidaima.com
    @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 了。


网友质疑
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 类,这不禁让我们细想:这样做值得吗?

当然,写成这样是有一些好处的:
1.便于参数校验
2.便于生成接口文档
但是:
这是一个 非事务性 的检索接口,参数校验真的那么必要吗?本来就可以无参请求,参数传错了系统自动忽略它是不是也可以?

如果了解了 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 项目, 都可以用它,为系统赋能高级查询。❞
用户评论