zoukankan      html  css  js  c++  java
  • 深入探索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
    您的支持是对博主最大的鼓励,感谢您的认真阅读。
    本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 实现业务
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 开发流程
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 报表系统集成说明
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 处理报表
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 数据访问
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 分布式应用
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 实现插件
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 对象设计器使用帮助
    基于DotNet构件技术的企业级敏捷软件开发平台 AgileEAS.NET平台开发指南 数据层开发
    Jquery 中的CheckBox、 RadioButton、 DropDownList、CheckBoxList、RadioButtonList的取值赋值
  • 原文地址:https://www.cnblogs.com/xiaoqi/p/spring-data-jpa-specifications-querydsl.html
Copyright © 2011-2022 走看看