深入探索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
您的支持是對博主最大的鼓勵,感謝您的認真閱讀。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章