有必要深入探索下SpringDataJPA

java1234

共 3006字,需浏览 7分钟

 ·

2020-11-12 15:45

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

  作者 |  JadePeng

来源 |  urlify.cn/3EV7Vr

66套java从入门到精通实战课程分享

数据访问层,所谓的CRUD是后端程序员的必修课程,Spring Data JPA 可以让我们来简化CRUD过程,本文由简入深,从JPA的基本用法,到各种高级用法。

Repository

Spring Data JPA 可以用来简化data access的实现,借助JPA我们可以快速的实现一些简单的查询,分页,排序不在话下。

public interface MovieRepository extends JpaRepository {
  List findByTitle(String title, Sort sort);

  Page findByYear(Int year, Pageable pageable);
}

JPA会根据方法命名,通过JPA 查询生成器自动生成SQL,cool!

Criteria API

但是,简单并非万能,有时候也需要面对一些复杂的查询,不能享受JPA 查询生成器带来的便利。JPQ 提供了Criteria API 和

Criteria API 可以通过编程方式动态构建查询,强类型检查可以避免错误。核心原理就是构造一个Predicate

LocalDate today = new LocalDate();

CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(Movie.class);
Root root = query.from(Movie.class);

Predicate isComedy = builder.equal(root.get(Movie.genre), Genre.Comedy);
Predicate isReallyOld = builder.lessThan(root.get(Movie.createdAt), today.minusYears(25));
query.where(builder.and(isComedy, isReallyOld));
em.createQuery(query.select(root)).getResultList();

Predicate 可以很好的满足一些复杂的查询,但是他的问题在于不便于复用,因为你需要先构建CriteriaBuilder, CriteriaQueryRoot. 同时代码可读性也比较一般。

Specifications

能不能定义可复用的Predicate呢?JPA 提供Specification接口来解决这个问题。

先来看这个接口定义:

public interface Specification {
  Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb);
}

上文不是说需要先构建CriteriaBuilder, CriteriaQueryRoot吗,那么Specification接口就是给你提供这个三个参数,让你自己构建Predicate,想什么来什么。

我们用Specifications来改写代码,先定义Specification

public MovieSpecifications {
  public static Specification isComedy() {
     return (root, query, cb) -> {
         return cb.equal(root.get(Movie_.genre), Genre.Comedy);
     };
  }
  public static Specification isReallyOld() {
     return (root, query, cb) -> {
        return cb.lessThan(root.get(Movie_.createdAt), new LocalDate.now().minusYears(25));
     };
  }
}

然后改写MovieRepository,为了让Repository可以运行Specification,我们需要让其继承JpaSpecificationExecutor接口。

public interface MovieRepository extends JpaRepository, JpaSpecificationExecutor {
  // query methods here
}

然后我们就可以愉快的使用定义好的Specification了。

movieRepository.findAll(MovieSpecifications.isComedy());
movieRepository.findAll(MovieSpecifications.isReallyOld());

在这里,repository 的代理类,会自动准备好CriteriaBuilder, CriteriaQueryRoot,是不是很爽?

从面向对象编程来讲,MovieSpecifications并不是很优雅,你可以这样做:

public MovieComedySpecification implements Specification {
  @Override
  public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
    return cb.equal(root.get(Movie_.genre), Genre.Comedy);
}

联合Specifications

我们可以将多个predicates 合到一起使用,通过and,or来连接。

movieRepository.findAll(Specification.where(MovieSpecifications.isComedy())
                        .and(MovieSpecifications.isReallyOld()));

Specification 构造器

产品定义的业务逻辑,有时候会很复杂,比如我们需要根据条件动态拼接查询,我们可以定义一个SpecificationBuilder。

public enum SearchOperation {                           
  EQUALITY, NEGATION, GREATER_THAN, LESS_THAN, LIKE;
  public static final String[] SIMPLE_OPERATION_SET = 
   { ":""!"">""<""~" };
  public static SearchOperation getSimpleOperation(final char input)
  {
    switch (input) {
      case ':'return EQUALITY;
      case '!'return NEGATION;
      case '>'return GREATER_THAN;
      case '<'return LESS_THAN;
      case '~'return LIKE;
      default: return null;
    }
  }
}
public class SearchCriteria {
   private String key;
   private Object value;
   private SearchOperation operation;
}

public final class MovieSpecificationsBuilder {
  private final List params;
  
  public MovieSpecificationsBuilder() {
    params = new ArrayList<>();
  }
  public Specification build() { 
    // convert each of SearchCriteria params to Specification and construct combined specification based on custom rules
  }
  public final MovieSpecificationsBuilder with(final SearchCriteria criteria) { 
    params.add(criteria);
    return this;
  }
}


使用方法:

final MovieSpecificationsBuilder msb = new MovieSpecificationsBuilder();
// add SearchCriteria by invoking with()
final Specification spec = msb.build();
movieRepository.findAll(spec);

Querydsl

Querydsl, 动态查询语言,支持JPA。先引入:


  com.querydsl
  querydsl-apt
  ${querydsl.version}
  provided



  com.querydsl
  querydsl-jpa
  ${querydsl.version}



  org.slf4j
  slf4j-log4j12
  1.6.1

Querydsl会根据表结构,生成meta-model,需要引入APT插件

maven配置:


  
  
    ...
    
      com.mysema.maven
      apt-maven-plugin
      1.1.3
      
        
          
            process
          

          
            target/generated-sources/java
            com.querydsl.apt.jpa.JPAAnnotationProcessor
          

        

      

    

    ...
  

  


假设,我们有下面的Domain类:

@Entity
public class Customer {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String firstname;
  private String lastname;

  // … methods omitted
}

在这里生成,会根据表结构生成查询classes,比如QCustomer:

QCustomer customer = QCustomer.customer;
LocalDate today = new LocalDate();
BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));

对比Specifications,这里是BooleanExpression,基本上基于生成的代码就可以构造了,更方便快捷。

现在我们到JPA使用,JPA 接口需要继承QueryDslPredicateExecutor

public interface CustomerRepository extends JpaRepository, QueryDslPredicateExecutor {
  // Your query methods here
}

查询代码:

BooleanExpression customerHasBirthday = customer.birthday.eq(today);
BooleanExpression isLongTermCustomer = customer.createdAt.lt(today.minusYears(2));
customerRepository.findAll(customerHasBirthday.and(isLongTermCustomer));

同样的,Queydsl 还有一些类似直接写SQL的骚操作。

简单如:

QCustomer customer = QCustomer.customer;
Customer bob = queryFactory.selectFrom(customer)
  .where(customer.firstName.eq("Bob"))
  .fetchOne();

多表查询:

QCustomer customer = QCustomer.customer;
QCompany company = QCompany.company;
query.from(customer, company);

多条件

queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob"), customer.lastName.eq("Wilson"));


queryFactory.selectFrom(customer)
    .where(customer.firstName.eq("Bob").and(customer.lastName.eq("Wilson")));

使用JOIN

QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCat kitten = new QCat("kitten");
queryFactory.selectFrom(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .fetch();

对应JPQL

inner join cat.mate as mate
left outer join cat.kittens as kitten

另外一个例子

queryFactory.selectFrom(cat)
    .leftJoin(cat.kittens, kitten)
    .on(kitten.bodyWeight.gt(10.0))
    .fetch();

JPQL version

select cat from Cat as cat
left join cat.kittens as kitten
on kitten.bodyWeight > 10.0

Ordering

QCustomer customer = QCustomer.customer;
queryFactory.selectFrom(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .fetch();

Grouping

queryFactory.select(customer.lastName).from(customer)
    .groupBy(customer.lastName)
    .fetch();

子查询

QDepartment department = QDepartment.department;
QDepartment d = new QDepartment("d");
queryFactory.selectFrom(department)
    .where(department.size.eq(
        JPAExpressions.select(d.size.max()).from(d)))
     .fetch();

小结

本文简单介绍了JPA的Repository,以及面向动态查询的Querydsl和Specifications 的用法,使用JPA可以有效减少代码编写量,提升代码易读性和可维护性。

参考

  • https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

  • http://www.querydsl.com/static/querydsl/latest/reference/html/ch02.html#jpa_integration

  • https://medium.com/@milan.brankovic/spring-advanced-search-filtering-5ee850f9458c






粉丝福利:实战springboot+CAS单点登录系统视频教程免费领取

???

?长按上方微信二维码 2 秒
即可获取资料



感谢点赞支持下哈 

浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报