Bean Search 超级好用的搜索工具

From: https://www.cnblogs.com/his365/p/17418817.html

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、请求格式

  1. GET /user/index:无参数,默认返回第 1 页,默认分页大小为 15 (可配置)bean-searcher.params.pagination.page 和 bean-searcher.params.pagination.size
  2. GET /user/index? page=2 & size=10 返回结果:结构同 (1)(只是每页 10 条,返回第 2 页)
  3. GET /user/index? sort=age & order=desc 返回结果:结构同 (1)(只是 dataList 数据列表以 age 字段降序输出)
  4. GET /user/index? age=20 & age-op=eq 返回结果:结构同 (1)(但只返回 age=20 的数据) age-op=eq 也可以省略
  5. GET /user/index? age=20 & age-op=ne 返回结果:结构同 (1)(但只返回 age != 20 的数据,ne 是 NotEqual 的缩写)
  6. GET /user/index? age-0=20 & age-1=30 & age-op=bt 返回结果:结构同 (1)(但只返回 20 <= age <= 30 的数据,bt 是 Between 的缩写)
  7. GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=il 返回结果:结构同 (1)(但只返回 age in (20, 30, 40) 的数据,il 是 InList 的缩写)
  8. GET /user/index? name-op=ey 返回结果:结构同 (1)(但只返回 name 为空 或为 null 的数据,ey 是 Empty 的缩写)
  9. GET /user/index? name=Jack & name-ic=true 返回结果:结构同 (1)(但只返回 name 等于 Jack (忽略大小写) 的数据,ic 是 IgnoreCase 的缩写)
  10. 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:返回满足条件的第一条,{}
  • 如果你是在 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,你将 如虎添翼。

世界上没有什么事情是跑步解决不了的,如果有,那就再跑一会!

 

posted on 2024-01-22 14:19  清清飞扬  阅读(873)  评论(0编辑  收藏  举报