SpringDataJPA之Specification复杂查询实战

网友投稿 368 2022-09-14


SpringDataJPA之Specification复杂查询实战

目录SpringDataJPA Specification复杂查询前言实现Specification与Controller业务逻辑ApiReturnUtil.page封装查询效果可能遇到的错误JpaSpecificationExecutor接口Specification一个一目了然的方法Criteria+TypedQuery开发过程中JPA Specification的应用为什么需要Specification应用场景JPA Specification实现复杂查询JPA多条件、多表查询Spring Data Jpa 简单模糊查询

SpringDataJPA Specification复杂查询

前言

继上次SpringData-JPA之ExampleMatcher实例查询使用一会之后发现ExampleMatcher对日期的查询特别糟糕,所以才有了Specification查询的研究。

20200114:更新对JpaSpecificationExecutor的解析,Specification思路2,以及CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery查询部分

20180811:根据所学所用,重新更新了文章,并增加了Pageable分页排序功能。

实现

对应的Repository需要实现JpaSpecificationExecutor接口

public interface EventRepository extends JpaRepository , JpaSpecificationExecutor{

Specification与Controller业务逻辑

@GetMapping("/event/list")

public ApiReturnObject findAllEvent(String eventTitle,Timestamp registerTime,Integer pageNumber,Integer pageSize) {

if(pageNumber==null) pageNumber=1;

if(pageSize==null) pageNumber=10;

//分页

//Pageable是接口,PageRequest是接口实现,new PageRequest()是旧方法,PageRequest.of()是新方法

//PageRequest.of的对象构造函数有多个,page是页数,初始值是0,size是查询结果的条数,后两个参数参考Sort对象的构造方法

Pageable pageable = PageRequest.of(pageNumber,pageSize,Sort.Direction.DESC,"id");

//Specification查询构造器

Specification specification=new Specification() {

private static final long serialVersionUID = 1L;

@Override

public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder criteriaBuilder) {

Predicate condition1 = null;

if(StringUtils.isNotBlank(eventTitle)) {

condition1 = criteriaBuilder.like(root.get("eventTitle"),"%"+eventTitle+"%");

}else {

condition1 = criteriaBuilder.like(root.get("eventTitle"),"%%");

}

Predicate condition2 = null;

if(registerTime!=null) {

condition2 = criteriaBuilder.greaterThan(root.get("registerTime"), registerTime);

}else {

condition2 = criteriaBuilder.greaterThan(root.get("registerTime"), new Timestamp(1514736000000L));

}

//Predicate conditionX=criteriaBuilder.and(condition1,condition2);

//query.where(conditionX);

query.where(condition1,condition2);

//query.where(getPredicates(condition1,condition2)); //这里可以设置任意条查询条件

return null; //这种方式使用JPA的API设置了查询条件,所以不需要再返回查询条件Predicate给Spring Data Jpa,故最后return null

}

};

Page list=eventRepository.findAll(specification, pageable);

return ApiReturnUtil.page(list);

}

ApiReturnUtil.page封装

其实这个大家根据自己的项目自己封装,这部分不作为核心内容,知识之前有部分网友比较纠结这个工具,所以简单放出来参考一下.

public static ApiReturnObject page(Page returnObject) {

return new ApiReturnObject(returnObject.getNumber()+"",returnObject.getNumberOfElements()+"",returnObject.getTotalElements()+"",returnObject.getTotalPages()+"","00","success",returnObject.getContent());

}

ApiReturnObject的主要内容:

String errorCode="00";

Object errorMessage;

Object returnObject;

String pageNumber;

String pageSize;

String totalElements;

String totalPages;

public ApiReturnObject(String pageNumber,String pageSize,String totalElements,String totalPages,String errorCode, Objhttp://ect errorMessage, Object returnObject) {

super();

this.pageNumber = pageNumber;

this.errorCode = errorCode;

this.errorMessage = errorMessage;

this.returnObject = returnObject;

this.pageSize = pageSize;

this.totalElements = totalElements;

this.totalPages = totalPages;

}

查询效果

返回对象有用的是pageNumber、pageSize、totalElements、totalPages等属性,可对其进行封装

{

"errorCode": "00",

"errorMessage": "success",

"pageNumber": "1",

"pageSize": "2",

"returnObject": [

{

"eventTitle": "1111",

"id": 3,

"registerTime": 1528702813000,

"status": "0"

},

{

"eventTitle": "小明失踪",

"id": 2,

"registerTime": 1526268436000,

"status": "0"

}

],

"totalElements": "5",

"totalPages": "3"

}

可以查询了。网上关于这个的资料也很少。希望可以帮到大家。

可能遇到的错误

Unable to locate Attribute with the the given name [event] on this ManagedType [org.microservice.tcbj.yytsg.checkcentersys.entity.Event]

出现这样的情况,一般是因为实体类中没有这个属性,例如我Event的是eventTitle,写成了event,就会报错。

JpaSpecificationExecutor接口

20200114补充

JPA 提供动态接口JpaSpecificationExecutor,利用类型检查的方式,利用Specification进行复杂的条件查询,比自己写 SQL 更加便捷和安全.

public interface JpaSpecificationExecutor {

/**

* Returns a single entity matching the given {@link Specification}.

*

* @param spec

* @return

*/

T findOne(Specification spec);

/**

* Returns all entities matching the given {@link Specification}.

*

* @param spec

* @return

*/

List findAll(Specification spec);

/**

* Returns a {@link Page} of entities matching the given {@link Specification}.

*

* @param spec

* @param pageable

* @return

*/

Page findAll(Specification spec, Pageable pageable);

/**

* Returns all entities matching the given {@link Specification} and {@link Sort}.

*

* @param spec

* @param sort

* @return

*/

List findAll(Specification spec, Sort sort);

/**

* Returns the number of instances that the given {@link Specification} will return.

*

* @param spec the {@link Specification} to count instances for

* @return the number of instances

*/

long count(Specification spec);

}

Specification

Specification是我们传入进去的查询参数,是一个接口,并且只有一个方法

public interface Specification {

Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb);

}

一个一目了然的方法

第二个实现思路:听说这个方法已经过时了,其实这个方法是最好理解的.这里附上作为思路参考.

public Page findAll(SearchEven even) {

Specification specification = new Specifications()

.eq(StringUtils.isNotBlank(even.getId()), "id", even.getId())

.gt(Objects.nonNull(even.getStatus()), "status", 0)

.between("registerTime", new Range<>(new Date()-1, new Date()))

.like("eventTitle", "%"+even.getEventTitle+"%")

.build();

return personRepository.findAll(specification, new PageRequest(0, 15));

}

Criteria+TypedQuery

思路三:利用EntityManager相关的CriteriaBuilder +CriteriaQuery+Predicate+TypedQuery进行查询.

@PersistenceContext

private EntityManager em;

/**

* CriteriaBuilder 安全查询创建工厂,创建CriteriaQuery,创建查询具体具体条件Predicate

* @author zhengkai.blog.csdn.net

*/

@Override

public List list(Even even) {

//查询工厂

CriteriaBuilder cb = em.getCriteriaBuilder();

//查询类

CriteriaQuery query = cb.createQuery(Even.class);

//查询条件

List predicates = new LinkedList<>();

//查询条件设置

predicates.add(cb.equal("id", even.getId()));

predicates.add(cb.like("eventTitle", even.getEventTitle()));

//拼接where查询

query.where(cb.or(predicates.toArray(new Predicate[predicates.size()])));

//用JPA 2.0的TypedQuery进行查询

TypedQuery typedQuery = em.createQuery(query);

return typedQuery.getResultList();

}

开发过程中JPA Specification的应用

Specification算是JPA里面比较灵活的查询规范了,方便实现复杂的查询方式。

为什么需要Specification

Spring-Data JPA 本身支持了比较简单的查询方式,也就是根据属性名成结合一些规范编写查询方法,例如,一个Customer对象有name属性,那么如果想要实现根据name来查询,只需要在接口文件中添加一个方法findByName(String name)即可实现。

public interface CustomerRepository extends JpaRepository {

Customer findByName(String name);

Customer findByEmailAddress(String emailAddress);

List findByLastname(String lastname, Sort sort);

Page findByFirstname(String firstname, Pageable pageable);

}

但是在许多情况下,会有比较复杂的查询,那么这个时候通过自动生成查询方法的方式就不再可行。

应用场景

为了实现复杂查询,JPA提供了Criteria接口,这个是一套标准接口,来看一个例子,在一个平台中,当一个老客户(注册以来两年)生日的时候,系统想要发送一个优惠券给该用户,那么传统使用 JPA 2.0 Criteria API 去实现:

LocalDate today = new LocalDate();

CriteriaBuilder builder = em.getCriteriaBuilder();

CriteriaQuery query = builder.createQuery(Customer.class);

Root root = query.from(Customer.class);

Predicate hasBirthday = builder.equal(root.get(Customer_.birthday), today);

Predicate isLongTermCustomer = builder.lessThan(root.get(Customer_.createdAt), today.minusYears(2);

query.where(builder.and(hasBirthday, isLongTermCustomer));

em.createQuery(query.select(root)).getResultList();

首先获得时间,去比较用户的注册时间

接下来是获得JPA中查询使用的实例

设置查询条件,首先判断今天是否为某个客户的生日,然后判断是否为老客户

执行查询条件,获得满足条件的用户

这里面的主要问题就是代码扩展性比较差,因为需要设置CriteriaBuilder, CriteriaQuery, Root,同时这部分的代码可读性比较差。

JPA Specification实现复杂查询

Specification为了实现可重用的断言,JPA 里面引入了一个Specification接口,接口的封装很简单,如下

public interface Specification {

Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb);

}

在java8中,我们可以非常方便地实现如上使用Criteria实现的效果

public CustomerSpecifications {

public static Specification customerHasBirthday() {

return (root, query, cb) -> {

return cb.equal(root.get(Customer_.birthday), today);

};

}

public static Specification isLongTermCustomer() {

return (root, query, cb) -> {

return cb.lessThan(root.get(Customer_.createdAt), new LocalDate.minusYears(2));

};

}

}

这样对应JPA的repository实现就可以如下

customerRepository.findAll(hasBirthday());

customerRepository.findAll(isLongTermCustomer());

而其实Specification为我们做的事情就是替我们准备了CriteriaQuery, Root, CriteriaBuilder, 有了这些可重用的断言之后,便可以将他们组合起来实现更加复杂的查询了

customerRepository.findAll(where(customerHasBirthday()).and(isLongTermCustomer()));

JPA多条件、多表查询

如果需要使用Specification,那么对应的Repository需要实现接口JpaSpecificationExecutor

public interface UserRepository extends JpaRepository, JpaSpecificationExecutor {}

单表多条件查询

在结合 Spring Boot 和 JPA 之后,为了四号线多条件查询,并且整理分页, 则可以考虑使用Predicate 断言, 例如现在针对 User , 想要根据用户的不同属性进行模糊查询,同时如果属性值为空或者空字符串,则跳过该属性,不作为查询条件,同时属于单表多条件查询,则

//在spring-jpa 2之后 不再使用 new PageRuest(page, pageSize) 的方式

Pageable pageable = PageRequest.of(page, pageSize);

//实现条件查询,组合查询

Specification specification = new Specification() {

private static final long serialVersionUID = 1L;

@Override

public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {

String account = request.getAccount();

String name = request.getName();

String phone = request.getPhone();

String accountType = request.getAccountType()http://;

String city = request.getCity();

String type = request.getType();

//用列表装载断言对象

List predicates = new ArrayList();

if(org.apache.commons.lang3.StringUtils.isNotBlank(name)) {

//模糊查询,like

Predicate predicate = cb.like(root.get("name").as(String.class), "%" + name +"%");

predicates.add(predicate);

}

if (StringUtils.isNotBlank(account)) {

Predicate predicate = cb.like(root.get("account").as(String.class), "%" + account +"%");

predicates.add(predicate);

}

if (StringUtils.isNotBlank(phone)) {

//精确查询,equal

Predicate predicate = cb.equal(root.get("phoneNumber").as(String.class), phone);

predicates.add(predicate);

}

if (StringUtils.isNotBlank(accountType)) {

Predicate predicate = cb.equal(root.get("accountType").as(String.class), accountType);

predicates.add(predicate);

}

if (StringUtils.isNotBlank(city)) {

Predicate predicate = cb.equal(root.get("city").as(String.class), city);

predicates.add(predicate);

}

if (StringUtils.isNotBlank(type)) {

Predicate predicate = cb.equal(root.get("type").as(String.class), type);

predicates.add(predicate);

}

//判断是否有断言,如果没有则返回空,不进行条件组合

if (predicates.size() == 0) {

return null;

}

//转换为数组,组合查询条件

Predicate[] p = new Predicate[predicates.size()];

return cb.and(predicates.toArray(p));

}

};

//交给DAO处理查询任务

Page dataPages = userDAO.findAll(specification, pageable);

多表多条件查询

在许多时候会面对多表多条件查询,实现实例如下

//封装查询对象Specification

Specification example = new Specification() {

@Override

public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {

//获取客户端查询条件

String company = model.getCompany();

String courierNum = model.getCjFHgysxHjnourierNum();

Standard standard = model.getStandard();

String type = model.getType();

//定义集合来确定Predicate[] 的长度,因为CriteriaBuilder的or方法需要传入的是断言数组

List predicates = new ArrayList<>();

//对客户端查询条件进行判断,并封装Predicate断言对象

if (StringUtils.isNotBlank(company)) {

//root.get("company")获取字段名

//company客户端请求的字段值

//as(String.class)指定该字段的类型

Predicate predicate = cb.equal(root.get("company").as(String.class), company);

predicates.add(predicate);

}

if (StringUtils.isNotBlank(courierNum)) {

Predicate predicate = cb.equal(root.get("courierNum").as(String.class), courierNum);

predicates.add(predicate);

}

if (StringUtils.isNotBlank(type)) {

Predicate predicate = cb.equal(root.get("type").as(String.class), type);

predicates.add(predicate);

}

//多表的条件查询封装,这是和单表查询的区别

if (standard != null) {

if (StringUtils.isNotBlank(standard.getName())) {

//创建关联对象(需要连接的另外一张表对象)

//JoinType.INNER内连接(默认)

//JoinType.LEFT左外连接

//JoinType.RIGHT右外连接

Join join = root.join("standard",JoinType.INNER);

//join.get("name")连接表字段值

Predicate predicate = cb.equal(join.get("name").as(String.class), standard.getName());

predicates.add(predicate);

}

}

//判断结合中是否有数据

if (predicates.size() == 0) {

return null;

}

//将集合转化为CriteriaBuilder所需要的Predicate[]

Predicate[] predicateArr = new Predicate[predicates.size()];

predicateArr = predicates.toArray(predicateArr);

// 返回所有获取的条件: 条件 or 条件 or 条件 or 条件

return cb.or(predicateArr);

}

};

//调用Dao方法进行条件查询

Page page = courierDao.findAll(example, pageable);

Spring Data Jpa 简单模糊查询

在一些比较简单的查询条件下,不一定要使用 Specification 接口,比如

@Repository

public interface UserRepository extends CrudRepository {

/**

* username不支持模糊查询,deviceNames支持模糊查询

* @param deviceNames 模糊查询deviceNames

* @param username 用户名称

* @return {@link List}

*/

List findAllByDeviceNamesContainingAndUsername(String deviceNames,String username);

/**

* 其中username不支持模糊查询,deviceNames支持模糊查询

* 传入的deviceNames需要在前后添加%,否则可能返回的结果是精确查询的结果

* @param deviceNames 模糊查询deviceNames

* @param username 用户名称

* @return {@link List}

*/

List findAllByDeviceNamesLikeAndUsername(String deviceNames,String username);

}


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:五天学redhat系列之---网络管理
下一篇:运用网络管理工具主动纠错
相关文章

 发表评论

暂时没有评论,来抢沙发吧~