通过前面的几节内容介绍,我们其实大部分的简单场景可以满足,但是一个 ORM 的解决方案绝不能拘泥与简单方案,而 JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。
我们也可以通过 idea 工具详细看其用法和实现类,JpaSpecificationExecutor 是 Repository 要继承的接口,而 SimpleJpaRepository 是其默认实现。而通过源码来看其提供的 API 比较简单、明了,有如下几个方法:
public interface JpaSpecificationExecutor<T> {
//根据 Specification 条件查询单个对象,注意的是,如果条件能查出来多个会报错
T findOne(@Nullable Specification<T> spec);
//根据 Specification 条件查询 List 结果
List<T> findAll(@Nullable Specification<T> spec);
//根据 Specification 条件,分页查询
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
//根据 Specification 条件,带排序的查询结果
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
//根据 Specification 条件,查询数量
long count(@Nullable Specification<T> spec);
}
不难看出,这个接口基本是围绕着 Specification 接口来定义的。我们先来看下其源码:
仔细看看图,就会发现 Specifications 的接口实现类就三个。
/**
* Specifications 是 Spring Data JPA 对 Specification 的聚合操作工具类,里面有以下四个方法:
*/
@Deprecated //已经不推荐使用了,我们可以用 Specification 来代替,如上图。
public class Specifications<T> implements Specification<T>, Serializable {
private final Specification<T> spec;
//构造方法私有化,只能通过 where/not 创建 Specifications 对象。
private Specifications(Specification<T> spec) {
this.spec = spec;
}
//创建 where 后面的 Predicate 集合
public static <T> Specifications<T> where(Specification<T> spec) {
return new Specifications<T>(spec);
}
//创建 not 集合的 Predicate
public static <T> Specifications<T> not(Specification<T> spec) {
return new Specifications<T>(new NegatedSpecification<T>(spec));
}
//Specification 的 and 关系集合
public Specifications<T> and(Specification<T> other) {
return new Specifications<T>(new ComposedSpecification<T>(spec, other, AND));
}
//Specification 的 or 关系集合
public Specifications<T> or(Specification<T> other) {
return new Specifications<T>(new ComposedSpecification<T>(spec, other, OR));
}
......
}
而如果查看 Specifications 源码的话就会发现,其已经将来要被删除了,已经不推荐使用了,而另外两个都是局部私有的,所以真正关注的就是 Specification 接口中如下一个接口方法:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
从这里可以看出,每个调用的地方都需要,创建 Specification 的实现类,而 JpaSpecificationExecutor 是针对 Criteria API 进行了 predicate 标准封装,帮我们封装了通过 EntityManager 的查询和使用细节,使操作 Criteria 更加便利了一些。所以我们要掌握一下 Predicate、Root、CriteriaQuery、CriteriaBuilder 是什么?
(1)Root root
代表了可以查询和操作的实体对象的根,如果将实体对象比喻成表名,那 root 里面就是这张表里面的字段,这不过是 JPQL 的实体字段而已。通过里面的 Path get(String attributeName),来获得我们想操作的字段。
(2)CriteriaQuery<?> query
代表一个 specific 的顶层查询对象,它包含着查询的各个部分,比如 select 、from、where、group by、order by 等。CriteriaQuery 对象只对实体类型或嵌入式类型的 Criteria 查询起作用,简单理解,它提供了查询 ROOT 的方法。常用的方法有:
CriteriaQuery<T> where(Predicate... restrictions);
CriteriaQuery<T> select(Selection<? extends T> selection);
CriteriaQuery<T> having(Predicate... restrictions);
(3)CriteriaBuilder cb
用来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。下面是构建简单的 Predicate 示例:
Predicate p1=cb.like(root.get(“name”).as(String.class), “%”+uqm.getName()+“%”);
Predicate p2=cb.equal(root.get("uuid").as(Integer.class), uqm.getUuid());
Predicate p3=cb.gt(root.get("age").as(Integer.class), uqm.getAge());
构建组合的 Predicate 示例:
Predicate p = cb.and(p3,cb.or(p1,p2));
(4)实际经验
到此我们发现其实 JpaSpecificationExecutor 帮我提供了一个高级的入口和结构,通过这个入口,可以使用底层 JPA 的 Criteria 所有方法,其实就可以满足了所有业务场景。但在实际工作中,需要注意的是,如果一旦我们写的实现逻辑太复杂,第二个人看不懂时,那一定是有问题的,我要寻找更简单的、更易懂的、更优雅的方式。比如:
- 分页和排序我们就没有比较自己再去实现一遍逻辑,直接用其开放的 Pageable 和 Sort 即可。
- 当我们过多的使用 group 或者 having、sum、count 等内置的 SQL 函数的时候,我们想想就是通过 Specification 实现了逻辑,这种效率真的高吗?是不是数据在其他算好更好?
- 当我们过多的操作 left join 和 inner Join 的链表查询的时候,我们想想,是不是通过数据库的视图(view)更优雅一点?
@Entity(name = "UserInfoEntity")
@Table(name = "user_info", schema = "test")
public class UserInfoEntity implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "first_name", nullable = true, length = 100)
private String firstName;
@Column(name = "last_name", nullable = true, length = 100)
private String lastName;
@Column(name = "telephone", nullable = true, length = 100)
private String telephone;
@Column(name = "create_time", nullable = true)
private Date createTime;
@Column(name = "version", nullable = true)
private String version;
@OneToOne(optional = false,fetch = FetchType.EAGER)
@JoinColumn(referencedColumnName = "id",name = "address_id",nullable = false)
@Fetch(FetchMode.JOIN)
private UserReceivingAddressEntity addressEntity;
......
}
@Entity
@Table(name = "user_receiving_address", schema = "test")
public class UserReceivingAddressEntity implements Serializable {
@Id
@Column(name = "id", nullable = false)
private Integer id;
@Column(name = "user_id", nullable = false)
private Integer userId;
@Column(name = "address_city", nullable = true, length = 500)
private String addressCity;
......
}
public interface UserRepository extends JpaSpecificationExecutor<UserInfoEntity> {
}
- 我们演示一下直接用 lambda 使用 Root 和 CriteriaBuilder 做一个简单的不同条件的查询和链表查询。
@Component
public class UserInfoManager {
@Autowired
private UserRepository userRepository;
public Page<UserInfoEntity> findByCondition(UserInfoRequest userParam,Pageable pageable){
return userRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<Predicate>();
if (StringUtils.isNoneBlank(userParam.getFirstName())){
//liked的查询条件
predicates.add(cb.like(root.get("firstName"),"%"+userParam.getFirstName()+"%"));
}
if (StringUtils.isNoneBlank(userParam.getTelephone())){
//equal查询条件
predicates.add(cb.equal(root.get("telephone"),userParam.getTelephone()));
}
if (StringUtils.isNoneBlank(userParam.getVersion())){
//greaterThan大于等于查询条件
predicates.add(cb.greaterThan(root.get("version"),userParam.getVersion()));
}
if (userParam.getBeginCreateTime()!=null&&userParam.getEndCreateTime()!=null){
//根据时间区间去查询 predicates.add(cb.between(root.get("createTime"),userParam.getBeginCreateTime(),userParam.getEndCreateTime()));
}
if (StringUtils.isNotBlank(userParam.getAddressCity())) {
//联表查询,利用root的join方法,根据关联关系表里面的字段进行查询。
predicates.add(cb.equal(root.join("addressEntityList").get("addressCity"), userParam.getAddressCity()));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}, pageable);
}
}
//可以仔细体会上面这个案例,实际工作中应该大部分都是这种写法,就算扩展也是百变不离其中。
- 我们再来看一个不常见的复杂查询的写法,来展示一下 CriteriaQuery 的用法(作者已经强烈不推荐了哦,和上面比起来太不优雅了)。
public List<MessageRequest> findByConditions(String name, Integer price, Integer stock) {
messageRequestRepository.findAll((Specification<MessageRequest>) (itemRoot, query, criteriaBuilder) -> {
//这里用 List 存放多种查询条件,实现动态查询
List<Predicate> predicatesList = new ArrayList<>();
//name 模糊查询,like 语句
if (name != null) {
predicatesList.add(
criteriaBuilder.and(
criteriaBuilder.like(
itemRoot.get("name"), "%" + name + "%")));
}
// itemPrice 小于等于 <= 语句
if (price != null) {
predicatesList.add(
criteriaBuilder.and(
criteriaBuilder.le(
itemRoot.get("price"), price)));
}
//itemStock 大于等于 >= 语句
if (stock != null) {
predicatesList.add(
criteriaBuilder.and(
criteriaBuilder.ge(
itemRoot.get("stock"), stock)));
}
//where() 拼接查询条件
query.where(predicatesList.toArray(new Predicate[predicatesList.size()]));
//返回通过 CriteriaQuery 拼装的 Predicate
return query.getRestriction();
});
}
- 而没有 Spring Data JPA 封装之前,如果想获得此三个对象 Root root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder,老式 Hibernate 的写法如下(PS:强烈不推荐哦,虽然现在也支持,只是让大家知道了解一下。):
@Autowired //导入entityManager
private EntityManager entityManager;
//创建CriteriaBuilder安全查询工厂,CriteriaBuilder是一个工厂对象,安全查询的开始.用于构建JPA安全查询.
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
//创建CriteriaQuery安全查询主语句
//CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
CriteriaQuery<Item> query = criteriaBuilder.createQuery(Item.class);
//Root 定义查询的From子句中能出现的类型
Root<Item> itemRoot = query.from(Item.class);
- 我们再来看一个利用 CriteriaQuery 例子,其实大家可以扩展一下思路,就是 Hibernate 那套在这里面都支持,不过作者还是建议代码越简单越好。
List<UserSpuFavoriteEntity> result = userSpuFavoriteDao.findAll((Root<UserSpuFavoriteEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb)->{
query.where(cb.and(cb.equal(root.get("userName"), userName),cb.isFalse(root.get("isDelete"))));
query.orderBy(cb.desc(root.get("updateTime")));
return query.getRestriction();
});
我们在实际工作中会发现,如果上面的逻辑,简单重复写总感觉是不是可以抽出一些公用方法呢,此时引入一种工厂模式,帮我们做一些事情,可以让代码更加优雅。基于 JpaSpecificationExecutor 的思路,我们创建一个 SpecificationFactory.Java 内容如下:
public final class SpecificationFactory {
/**
* 模糊查询,匹配对应字段
*/
public static Specification containsLike(String attribute, String value) {
return (root, query, cb)-> cb.like(root.get(attribute), "%" + value + "%");
}
/**
* 某字段的值等于 value 的查询条件
*/
public static Specification equal(String attribute, Object value) {
return (root, query, cb) -> cb.equal(root.get(attribute),value);
}
/**
* 获取对应属性的值所在区间
*/
public static Specification isBetween(String attribute, int min, int max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
public static Specification isBetween(String attribute, double min, double max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
public static Specification isBetween(String attribute, Date min, Date max) {
return (root, query, cb) -> cb.between(root.get(attribute), min, max);
}
/**
* 通过属性名和集合实现 in 查询
*/
public static Specification in(String attribute, Collection c) {
return (root, query, cb) ->root.get(attribute).in(c);
}
/**
* 通过属性名构建大于等于 Value 的查询条件
*/
public static Specification greaterThan(String attribute, BigDecimal value) {
return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
}
public static Specification greaterThan(String attribute, Long value) {
return (root, query, cb) ->cb.greaterThan(root.get(attribute),value);
}
......
}
PS:可以根据实际工作需要和场景进行不断扩充。
userRepository.findAll(
SpecificationFactory.containsLike("firstName", userParam.getLastName()),
pageable);
是不是发现代码一下子少了很多?
userRepository.findAll(Specifications.where(
SpecificationFactory.containsLike("firstName", userParam.getLastName()))
.and(SpecificationFactory.greaterThan("version",userParam.getVersion())),
pageable);
和我们前面举的例子比起来是不是代码更加优雅、可读性更加强了?
我们通过上图可以看一下,前面介绍的几个类之间的关联关系。
/**
*以 findOne 为例
*/
public T findOne(Specification<T> spec) {
try {
return getQuery(spec, (Sort) null).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
/*
* 解析 Specification,利用 EntityManager 直接实现调用逻辑。
*/
protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
if (sort != null) {
query.orderBy(toOrders(sort, root, builder));
}
return applyRepositoryMethodMetadata(em.createQuery(query));
}
其实我们可以看的出来底层都是调用的 EntityManager。
通过此图可以体会一下 Repository 和 EntityManager 的关联关系。
其实在实际项目中,我们的 Repository 可以扩展的更加优雅一点来解决后台 API 的 search 问题和如何自定义 Respository?下一篇内容将会来讲解这个问题。