本文主要讲弃用。
入门
入门的文章一大堆,基本上学会使用JpaRepository
编写增删查改代码,使用Specification
编写动态查询就可以。
比如编写这样一个UserSpecification
类,
public class UserSpecification implements Specification<User> {
private final UserQuery query;
public UserSpecification(UserQuery query) {
this.query = query;
}
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> list = new ArrayList<>();
if (StringUtils.isNotBlank(query.getAccount())) {
list.add(criteriaBuilder.equal(root.get("account").as(String.class), query.getAccount()));
}
if (StringUtils.isNotBlank(query.getUserStatus())) {
list.add(criteriaBuilder.equal(root.get("status").as(String.class), query.getStatus()));
}
if (StringUtils.isNotBlank(query.getAccountLike())) {
list.add(criteriaBuilder.like(root.get("account").as(String.class), "%" + query.getAccountLike() + "%"));
}
return criteriaBuilder.and(list.toArray(new Predicate[0]));
}
}
然后我们来演示如何通过对这个UserSpecification
类进行重构,来替换掉复杂难用的SpringDataJPA
的。
重构
在分析代码之前,先介绍点重构的知识。
“重构就是在不改变软件系统外部行为的前提下,对它的内部结构进行改善”。
这句话大家可能都听过,但是重构的两大目标许多人却未必清楚。
其中一个是表达意图,另一个是消除重复。
表达意图
那么UserSpecification
类背后要表达的意图是什么呢?
单看这段代码只是将UserQuery
类定义的对象query
转换成Predicate
对象,再结合所要完成的动态查询功能来看,这个Predicate
对象最终将被转换成SQL语句用于数据库查询。
那么我们为什么不绕过Predicate
对象,直接把query
对象转换成SQL语句,来达到我们进行动态查询的意图呢?
于是,我们编写一个UserQueryBuilder
类来基于UserQuery
构建查询语句和参数,
public class UserQueryBuilder {
public List<String> toWhere(UserQuery query, List<Object> argList) {
List<String> whereList = new LinkedList<>();
if (StringUtils.isNotBlank(query.getAccount())) {
whereList.add("account = ?");
argList.add(query.getAccount());
}
if (query.getValid() != null) {
whereList.add("valid = ?");
argList.add(query.getValid());
}
if (StringUtils.isNotBlank(query.getAccountLike())) {
whereList.add("account like ?");
argList.add("%" + query.getAccountLike() + "%");
}
return whereList;
}
public String buildSelectAndArgs(UserQuery query, List<Object> argList) {
String where = StringUtils.join(toWhere(query, argList), " ");
if (!where.isEmpty()) {
where = "where " + where;
}
return "select * from t_user " + where;
}
}
并在UserService
中将生成的查询语句和参数交给EntityManager
执行。
@Service
public class UserService {
@Resource
EntityManager entityManager;
UserQueryBuilder userQueryBuilder = new UserQueryBuilder();
public List<User> query(UserQuery userQuery) {
ArrayList<Object> argList = new ArrayList<>();
String sql = userQueryBuilder.buildSelectAndArgs(userQuery, argList);
Query query = entityManager.createNativeQuery(sql, User.class);
for (int i = 0; i < argList.size(); i++) {
query.setParameter(i + 1, argList.get(i));
}
return query.getResultList();
}
}
完整代码见这里:https://github.com/doytowin/doyto-query-origin/tree/step1
这时我们已经不再需要使用UserSpecification
类来进行动态查询了。
消除重复
代码重复分为两种,一种是简单型重复,一种是结构型重复。
再看看UserQueryBuilder#toWhere
方法,每个if语句的代码都比较相似
if (some condition) {
whereList.add(sql);
argList.add(value);
}
这就是一种典型的结构型重复,而消除这种重复的常见手法就是使用反射。
另外每行whereList.add
语句里面的参数都是字符串常量,并且跟UserQuery
里的字段一一对应,可以通过引入注解来将查询语句和字段关联起来。
经过这一步重构后的代码结构大概是这样:
- 通过反射获取查询对象的所有字段并进行遍历
- 通过反射读取对应字段的值
- 如果为有效值
- 向whereList添加注解配置的查询条件
- 向argList添加字段的值
重构后的UserQueryBuilder
代码如下:
public class UserQueryBuilder {
@SneakyThrows
public List<String> toWhere(UserQuery query, List<Object> argList) {
List<String> whereList = new LinkedList<>();
for (Field field : FieldUtils.getAllFields(query.getClass())) {
Object value = FieldUtils.readField(field, query, true);
if (isValidValue(value)) {
appendAnd(whereList, field);
appendArg(argList, value);
}
}
return whereList;
}
private void appendArg(List<Object> argList, Object value) {
argList.add(value);
}
private void appendAnd(List<String> whereList, Field field) {
QueryField queryField = field.getAnnotation(QueryField.class);
whereList.add(queryField.and());
}
// ...
}
对应的UserQuery
代码如下:
public class UserQuery extends PageQuery {
@QueryField(and = "account = ?")
private String account;
@QueryField(and = "valid = ?")
private Boolean valid;
@QueryField(and = "account Like CONCAT('%', ?, '%')")
private String accountLike;
}
以及QueryField
的定义:
@Target(FIELD)
@Retention(RUNTIME)
public @interface QueryField {
String and();
}
完整代码见这里:https://github.com/doytowin/doyto-query-origin/tree/step2
现在当我们需要增加新的查询条件时,只需要在UserQuery
类中添加新的字段并配上相应的注解即可。
弃用
接着再通过对UserQueryBuilder
和UserService
应用抽取父类,抽取泛型参数等重构手法,
将动态查询逻辑进一步封装和抽象,进而得到一个将Query对象映射成Where语句的独立模块。
后续再基于Entity类生成增删查改语句,交给EntityManager
执行,从而不再需要使用JpaRepository
,
最后将SQL执行模块从EntityManager
切换为spring-jdbc
,彻底完成对SpringDataJPA
的替换。
这就是用世界上最好的开发语言JAVA开发的世界上最好的第二代ORM框架DoytoQuery
的起源。