修為進階之Spring Data API

2021-03-02 Android編程精選

數據訪問層,所謂的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可以有效減少代碼編寫量,提升代碼易讀性和可維護性。

相關焦點

  • Spring Doc 生成OPEN API 3文檔
    設置springdoc-openapi如果想讓 springdoc-openapi 為我們的API生成標準的 OpenAPI 3 文檔, 只需要添加 [springdoc-openapi-core](https://search.maven.org/search?
  • Postgres、R2DBC、Spring Data JDBC和Spring WebFlux的響應式API簡介
    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId
  • Swagger在Spring Rest API中的使用
    編寫了Spring rest apis後,與前端開發人員共享,以便他們可以與之集成。前端開發人員將需要所有其餘的api端點以及每個端點的請求方法,請求參數,請求正文和響應格式。你將如何分享有關您的api的所有信息?手動記錄所有api是非常困難和耗時的。此外,如果您手動記錄api,則每次在api中進行一些更改時都必須更改文檔。好!
  • Spring Boot 無侵入式 實現API接口統一JSON格式返回
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫無侵入式 統一返回JSON格式其實本沒有沒打算寫這篇博客的,但還是要寫一下寫這篇博客的起因是因為,現在呆著的這家公司居然沒有統一的API
  • SpringBoot(五) :spring data jpa 的使用
    的使用.html如有好文章投稿,請點擊 → 這裡了解詳情在上篇文章《 springboot(二):web綜合開發 》中簡單介紹了一下spring data jpa的基礎性使用,這篇文章將更加全面的介紹spring data jpa 常見用法以及注意事項。
  • springboot(五):spring data jpa的使用
    在上篇文章springboot(二):web綜合開發中簡單介紹了一下spring data jpa的基礎性使用,這篇文章將更加全面的介紹spring
  • Rocket-API 版本更新,基於 Spring Boot 的 API 敏捷開發框架
    處理 mongo 下 findAll 返回數據最多 101 條記錄問題 處理 #{${}} 變量值篏套問題 db.count() 計數優化 添加全局變量 Utils 中的 pasreToString, pasreToObject 方法來實現對象與 string 的轉換概述"Rocket-API" 基於 spring
  • 詳述 Spring Data JPA 的那些事兒
    當我們項目中使用 spring data jpa 的時候,你會發現有時候沒有 sql 語句,其實框架的底層已經幫我們實現了,我們只需要遵守規範使用就可以了,下面會詳細談到 spring data jpa 的各種規範細則。使用 jpa 是可以解決一些我們寫 sql 語句的煩惱,但是搞開發的如果 sql 寫不好,還是很頭疼的。
  • Spring Data Redis使用
    系列的第十四篇文章,了解前面的文章有助於更好的理解本文:1.Linux上安裝Redis2.Redis中的五種數據類型簡介3.Redis字符串(STRING)介紹4.Redis字符串(STRING)中BIT相關命令5.Redis列表與集合6.Redis散列與有序集合7.Redis中的發布訂閱和事務8.Redis快照持久化9.Redis之AOF
  • 深入淺出 spring-data-elasticsearch 之 ElasticSearch 架構初探(一)
    如何健康檢查集群名,集群的健康狀態GET http:{   "cluster_name":          "elasticsearch",   "status":                "green",   "timed_out":             false,   "number_of_nodes":       1,   "number_of_data_nodes
  • 5w 字 | 172 圖 | 超級賽亞級 Spring Cloud 實戰
    =/api/(?[0].data-id=datasource.ymlspring.cloud.nacos.config.extension-configs[0].group=devspring.cloud.nacos.config.extension-configs[0].refresh=truespring.cloud.nacos.config.extension-configs[
  • Spring Cloud 中 Zuul 網關到底有何牛逼之處?竟然這麼多人在用!
    zuul.routes.eureka-application-service.path=/api/**# 通配方式配置排除網關代理路徑。所有符合ignored-patterns的請求路徑都不被zuul網關代理。
  • Salesforce Rest Api詳解一之Spring Boot集成
    (https://spring.io/guides/gs/spring-boot/)要在STS中創建項目,請運行STS安裝並為您自己創建一個新的工作區。Salesforce Rest Api詳解一之Postman演示獲取Token
  • springcloud實踐二:gateway網關詳解
    : - Path=/user-api/**效果:匹配請求為/user-api/開頭的請求,並轉發到http://localhost:8092/user-api/**:8091/user-api/delete/a?
  • 帶有Angular和Spring App的Google Recaptcha示例
    Google recaptcha代碼如下所示:<div ng-model="g-recaptcha-response" data-sitekey="6YbGpACUBBBBBJ9pa2Km3vYeVCBV8UHYGic-dbGD"></div>將代碼放在適當的地方登錄,註冊頁面
  • Spring Boot 2.X 實戰--SQL 資料庫(MyBatis)
    Gradle 依賴 1dependencies { 2    implementation 'org.springframework.boot:spring-boot-starter-web' 3    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter
  • 快速創建 Spring Cloud 應用的 Spring Initializr 使用及原理
    本文,圍繞 spring initializr 框架,以 start.spring.io 為例,全面的給大家介紹如何使用和擴展這個框架,以及背後的運行原理。: groupId: org.acme artifactId: my-api-dependencies version: 1.0.0.RELEASE repositories: my-api-repo-1
  • magic-api 0.4.8 發布,接口快速開發框架
    一元運算符,支持非布爾值運算 修複函數命名atPercent變更為asPercent項目介紹magic-api 是一個基於Java的接口快速開發框架,通過magic-api提供的UI界面完成編寫接口,無需定義Controller
  • SwaggerSpringBootStarter 2.1.1 版本更新發布
    SwaggerSpringBootStarter更新到2.1.1版本了,配套依賴spring boot 2.1.1版本。
  • mongoHelper 0.3.9 發布,spring-data-mongodb 增強工具包
    mongoHelper 是基於 spring-data-mongodb 的增強工具包,簡化 CRUD 操作,提供類 jpa 的資料庫操作。