乐趣区

关于java:这样写代码比直接使用-MyBatis-效率提高了-100-倍

作者:乔伊酱 \
链接:https://juejin.cn/post/702773…

对一个 Java 后端程序员来说,MyBatisHibernateData 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用户名 年龄 等任意字段排序

后端接口该怎么写呢?

试想一下,对于这种要求的查问,后端接口里的代码如果用 MyBatisHibernateData Jdbc 间接来写的话,100 行代码 能实现吗?

反正我是没这个信念,算了,我还是间接坦率,面对这种需要后端如何 只用一行代码搞定 吧(有趣味的同学能够 MyBatis 等写个试试,最初能够比照一下)

手把手:只一行代码实现以上需要

首先,重点人物出场啦:Bean Searcher, 它就是专门来凑合这种列表检索的,无论简略的还是简单的,通通一行代码搞定!而且它还十分轻量,Jar 包体积仅不到 100KB,无第三方依赖。

假如咱们我的项目应用的框架是 Spring Boot(当然 Bean Searcher 对框架没有要求,但在 Spring Boot 中应用更加不便)

Spring Boot 根底就不介绍了,举荐下这个实战教程:
https://github.com/javastacks…

增加依赖

Maven:

<dependency>
    <groupId>com.ejlchina</groupId>
    <artifactId>bean-searcher-boot-starter</artifactId>
    <version>3.1.2</version>
</dependency>

Gradle:

implementation 'com.ejlchina:bean-searcher-boot-starter:3.1.2'

而后写个实体类来承载查问的后果

@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 String 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 检索器了。

这样就完了?那咱们来测一下这个接口,看看成果吧

(1)无参申请

  • GET /user/index
  • 返回后果:
{
    "dataList": [           // 用户列表,默认返回第 0 页,默认分页大小为 15(可配置){"id": 1, "name": "Jack", "age": 25, "roleId": 1, "role": "普通用户"},
        {"id": 2, "name": "Tom", "age": 26, "roleId": 1, "role": "普通用户"},
        ...
    ],
    "totalCount": 100       // 用户总数
}

(2)分页申请(page | size)

  • GET /user/index? page=2 & size=10
  • 返回后果:构造同 (1)(只是每页 10 条,返回第 2 页)

参数名 sizepage 可自定义,page 默认从 0 开始,同样可自定义,并且可与其它参数组合应用

(3)数据排序(sort | order)

  • GET /user/index? sort=age & order=desc
  • 返回后果:构造同 (1)(只是 dataList 数据列表以 age 字段降序输入)

参数名 sortorder 可自定义,可与其它参数组合应用

(4)指定(排除)字段(onlySelect | selectExclude)

  • GET /user/index? onlySelect=id,name,role
  • GET /user/index? selectExclude=age,roleId
  • 返回后果:(列表只含 id,name 与 role 三个字段)
{
    "dataList": [           // 用户列表,默认返回第 0 页(只蕴含 id,name,role 字段){"id": 1, "name": "Jack", "role": "普通用户"},
        {"id": 2, "name": "Tom", "role": "普通用户"},
        ...
    ],
    "totalCount": 100       // 用户总数
}

参数名 onlySelectselectExclude 可自定义,可与其它参数组合应用

(5)字段过滤(op = eq)

  • GET /user/index? age=20
  • GET /user/index? age=20 & age-op=eq
  • 返回后果:构造同 (1)(但只返回 age = 20 的数据)

参数 age-op = eq 示意 age字段运算符eqEqual 的缩写),示意参数 age 与参数值 20 之间的关系是 Equal,因为 Equal 是一个默认的关系,所以 age-op = eq 也能够省略

参数名 age-op 的后缀 -op 可自定义,且可与其它字段参数 和 上文所列的参数(分页、排序、指定字段)组合应用,下文所列的字段参数也是一样,不再复述。

(6)字段过滤(op = ne)

  • GET /user/index? age=20 & age-op=ne
  • 返回后果:构造同 (1)(但只返回 age != 20 的数据,neNotEqual 的缩写)

(7)字段过滤(op = ge)

  • GET /user/index? age=20 & age-op=ge
  • 返回后果:构造同 (1)(但只返回 age >= 20 的数据,geGreateEqual 的缩写)

(8)字段过滤(op = le)

  • GET /user/index? age=20 & age-op=le
  • 返回后果:构造同 (1)(但只返回 age <= 20 的数据,leLessEqual 的缩写)

(9)字段过滤(op = gt)

  • GET /user/index? age=20 & age-op=gt
  • 返回后果:构造同 (1)(但只返回 age > 20 的数据,gtGreateThan 的缩写)

(10)字段过滤(op = lt)

  • GET /user/index? age=20 & age-op=lt
  • 返回后果:构造同 (1)(但只返回 age < 20 的数据,ltLessThan 的缩写)

(11)字段过滤(op = bt)

  • GET /user/index? age-0=20 & age-1=30 & age-op=bt
  • GET /user/index? age=[20,30] & age-op=bt(简化版,[20,30] 须要 UrlEncode, 参考下文)
  • 返回后果:构造同 (1)(但只返回 20 <= age <= 30 的数据,btBetween 的缩写)

参数 age-0 = 20 示意 age 的第 0 个参数值是 20。上述提到的 age = 20 实际上是 age-0 = 20 的简写模式。另:参数名 age-0 age-1 中的连字符 - 可自定义。

(12)字段过滤(op = mv)

  • GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=mv
  • GET /user/index? age=[20,30,40] & age-op=mv(简化版,[20,30,40] 须要 UrlEncode, 参考下文)
  • 返回后果:构造同 (1)(但只返回 age in (20, 30, 40) 的数据,mvMultiValue 的缩写,示意有多个值的意思)

(13)字段过滤(op = in)

  • GET /user/index? name=Jack & name-op=in
  • 返回后果:构造同 (1)(但只返回 name 蕴含 Jack 的数据,inInclude 的缩写)

(14)字段过滤(op = sw)

  • GET /user/index? name=Jack & name-op=sw
  • 返回后果:构造同 (1)(但只返回 name 以 Jack 结尾的数据,swStartWith 的缩写)

(15)字段过滤(op = ew)

  • GET /user/index? name=Jack & name-op=ew
  • 返回后果:构造同 (1)(但只返回 name 以 Jack 结尾的数据,swEndWith 的缩写)

(16)字段过滤(op = ey)

  • GET /user/index? name-op=ey
  • 返回后果:构造同 (1)(但只返回 name 为空 或为 null 的数据,eyEmpty 的缩写)

(17)字段过滤(op = ny)

  • GET /user/index? name-op=ny
  • 返回后果:构造同 (1)(但只返回 name 非空 的数据,nyNotEmpty 的缩写)

(18)疏忽大小写(ic = true)

  • GET /user/index? name=Jack & name-ic=true
  • 返回后果:构造同 (1)(但只返回 name 等于 Jack (疏忽大小写) 的数据,icIgnoreCase 的缩写)

参数名 name-ic 中的后缀 -ic 可自定义,该参数可与其它的参数组合应用,比方这里检索的是 name 等于 Jack 时疏忽大小写,但同样实用于检索 name 以 Jack 结尾或结尾时疏忽大小写。

当然,以上各种条件都能够组合,例如

查问 name 以 Jack (疏忽大小写) 结尾,且 roleId = 1,后果以 id 字段排序,每页加载 10 条,查问第 2 页:

  • GET /user/index? name=Jack & name-op=sw & name-ic=true & roleId=1 & sort=id & size=10 & page=2
  • 返回后果:构造同 (1)

OK,成果看完了,/user/index 接口里咱们的确只写了一行代码,它便能够反对这么多种的检索形式,有没有感觉当初 你写的一行代码 就能够 干过他人的一百行 呢?

Bean Searcher

本例中,咱们只应用了 Bean Searcher 提供的 MapSearcher 检索器的一个检索办法,其实,它还有很多检索办法。

检索办法

  • searchCount(Class<T> beanClass, Map<String, Object> params) 查问指定条件下的数据 总条数
  • searchSum(Class<T> beanClass, Map<String, Object> params, String field) 查问指定条件下的 某字段 统计值
  • searchSum(Class<T> beanClass, Map<String, Object> params, String[] fields) 查问指定条件下的 多字段 统计值
  • search(Class<T> beanClass, Map<String, Object> params) 分页 查问指定条件下数据 列表 总条数
  • search(Class<T> beanClass, Map<String, Object> params, String[] summaryFields) 同上 + 多字段 统计
  • searchFirst(Class<T> beanClass, Map<String, Object> params) 查问指定条件下的 第一条 数据
  • searchList(Class<T> beanClass, Map<String, Object> params) 分页 查问指定条件下数据 列表
  • searchAll(Class<T> beanClass, Map<String, Object> params) 查问指定条件下 所有 数据 列表

MapSearcher 与 BeanSearcher

另外,Bean Searcher 除了提供了 MapSearcher 检索器外,还提供了 BeanSearcher 检索器,它同样领有 MapSearcher 所有的办法,只是它返回的单条数据不是 Map,而是一个 泛型 对象。

参数构建工具

另外,如果你是在 Service 里应用 Bean Searcher,那么间接应用 Map<String, Object> 类型的参数可能不太优雅,为此,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<T> beanClass, Map<String, Object> 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 只能有两种检索形式,如果再严格一点,只容许 准确匹配,那其实有两种写法。

(1)还是应用运算符束缚:
@SearchBean(tables="user u, role r", joinCond="u.role_id = r.id", autoMapTo="u")
public class User {@DbField(onlyOn = Operator.Equal)
    private String name;

    // 为缩小篇幅,省略其它字段...
}
(2)在 Controller 的接口办法里把运算符参数笼罩:
@GetMapping("/index")
public SearchResult<Map<String, Object>> index(HttpServletRequest request) {Map<String, Object> params = MapUtils.flatBuilder(request.getParameterMap())
        .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;      // 返回过滤后的检索参数
        }
    };
}

某同学问

参数咋这么怪,这么多呢,和前端有仇么

  1. 参数名是否奇怪,这其实看集体爱好,如果你不喜爱中划线 -,不喜爱 opic 后缀,齐全能够自定义,参考这篇文档:

searcher.ejlchina.com/guide/lates…

  1. 参数个数的多少,其实是和需要的复杂程度相干的。如果需要很简略,那么很多参数没必要让前端传,后端间接塞进去就好。比方:name 只要求后含糊匹配,age 只要求区间匹配,则能够:
@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);
}

这样前端就不必传 name-opage-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;

    // 为缩小篇幅,省略其它字段...
}
  1. 对于 op=bt/mv 的多值参数传递,参数的确能够简化,例如:
  • age-0=20 & age-1=30 & age-op=bt 简化为 age=[20,30] & age-op=bt,
  • age-0=20 & age-1=30 & age-2=40 & age-op=mv 简化为 age=[20,30,40] & age-op=mv

简化办法:只需配置一个 ParamFilter(参数过滤器)即可,具体代码能够参考这里:

https://github.com/ejlchina/b…

入参是 request,我 swagger 文档不好渲染了呀

其实,Bean Searcher 的检索器只是须要一个 Map<String, Object> 类型的参数,至于这个参数是怎么来的,和 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/b…

这里就不再复述了。

开发效率真的进步 100 倍了吗?

从本例其实能够看出,效率晋升的水平依赖于检索需要的复杂度。需要越简单,则效率进步倍数越多,反之则越少,如果需要超级简单,则进步 1000 倍都有可能。

但即便咱们日常开发中没有如此简单的需要,开发效率只晋升了 510 倍,那是不是也十分可观呢?

结语

本文介绍了 Bean Searcher 在简单列表检索畛域的超强能力。它之所以能够极大进步这类需要的研发效率,基本上归功于它 独创 动静字段运算符 多表映射机制,这是传统 ORM 框架所没有的。但因为篇幅所限,它的个性本文不能尽述,比方它还:

  • 反对 聚合查问
  • 反对 Select|Where|From 子查问
  • 反对 实体类嵌入参数
  • 反对 字段转换器
  • 反对 Sql 拦截器
  • 反对 数据库 Dialect 扩大
  • 反对 多数据源
  • 反对 自定义注解
  • 等等

Bean Searcher 是我在工作中总结封装进去的一个小工具,公司外部应用了 4 年,经验大小我的项目三四十个,只是最近才着手欠缺文档分享给大家,如果你喜爱,肯定去点个 Star 哦 ^_^。

再奉上 Bean Searcher 的具体文档:searcher.ejlchina.com/

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.6 正式公布,一大波新个性。。

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

退出移动版