• 深入探索Spring Data JPA, 从Repository 到 Specifications 和 Querydsl


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

    Repository

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

    public interface MovieRepository extends JpaRepository<Movie, Long> {
      List<Movie> findByTitle(String title, Sort sort);
    
      Page<Movie> 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<Movie> query = builder.createQuery(Movie.class);
    Root<Movie> 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, CriteriaQuery, Root. 同时代码可读性也比较一般。

    Specifications

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

    先来看这个接口定义:

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

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

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

    public MovieSpecifications {
      public static Specification<Movie> isComedy() {
         return (root, query, cb) -> {
             return cb.equal(root.get(Movie_.genre), Genre.Comedy);
         };
      }
      public static Specification<Movie> 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<Movie, Long>, JpaSpecificationExecutor<Movie> {
      // query methods here
    }
    

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

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

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

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

    public MovieComedySpecification implements Specification<Movie> {
      @Override
      public Predicate toPredicate(Root<Movie> 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<SearchCriteria> params;
      
      public MovieSpecificationsBuilder() {
        params = new ArrayList<>();
      }
      public Specification<Movie> 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<Movie> spec = msb.build();
    movieRepository.findAll(spec);
    

    Querydsl

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

    <dependency>
      <groupId>com.querydsl</groupId>
      <artifactId>querydsl-apt</artifactId>
      <version>${querydsl.version}</version>
      <scope>provided</scope>
    </dependency>
    
    <dependency>
      <groupId>com.querydsl</groupId>
      <artifactId>querydsl-jpa</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.6.1</version>
    </dependency>
    

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

    maven配置:

    <project>
      <build>
      <plugins>
        ...
        <plugin>
          <groupId>com.mysema.maven</groupId>
          <artifactId>apt-maven-plugin</artifactId>
          <version>1.1.3</version>
          <executions>
            <execution>
              <goals>
                <goal>process</goal>
              </goals>
              <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
              </configuration>
            </execution>
          </executions>
        </plugin>
        ...
      </plugins>
      </build>
    </project>
    

    假设,我们有下面的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<Customer>, 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可以有效减少代码编写量,提升代码易读性和可维护性。

    参考


    作者:Jadepeng
    出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi
    您的支持是对博主最大的鼓励,感谢您的认真阅读。
    本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    Apache Kafka(七)- Kafka ElasticSearch Comsumer
    【数据库】SQL经典面试题
    【数据库】SQL经典面试题
    【数据库】软件安全测试之SQL注入
    Jmeter代理服务器设置
    代理服务器之趣谈工作原理
    Jmeter之解决烦人的中文乱码问题
    Python之测试webservice接口
    Jmeter录制脚本工具之chrome插件--BlazeMeter
    如何利用Jmeter做代理录制脚本
  • 原文地址:https://www.cnblogs.com/xiaoqi/p/spring-data-jpa-specifications-querydsl.html
Copyright © 2020-2023  润新知