定一个小目标,一篇博客吃透JPA(底层原理篇)

目录

文档

 核心概念

查询方法

下面一步一步解读如何使用JPA

定义存储库接口

微调库定义

使用带有多个Spring数据模块的存储库(怎么确定数据接口绑定哪一个表?)

定义查询方法

查询查找策略

创建查询

属性表达式(嵌套属性查询)

特殊参数处理(比如分页,排序)

 限制查询结果

存储库方法返回集合或可迭代对象(推荐看文档)

存储库方法的空处理

流媒体查询结果

异步查询结果


文档

Datahttps://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql.jpa-and-spring-data

Spring Data JPA - Reference Documentationicon-default.png?t=M276https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference 

 核心概念

Spring数据存储库抽象中的中心接口是repository。它将要管理的域类以及域类的ID类型作为类型参数。此接口主要用作标记接口,以捕获要使用的类型,并帮助您发现扩展此接口的接口。TheCrudRepositoryinterface为正在管理的实体类提供了复杂的CRUD功能。

Spring还提供了特定于持久化技术的抽象,比如JpaRepository or MongoRepository这些接口扩展了crudrepository,并公开了底层持久性技术的功能,以及与持久性技术无关的相当通用的接口(如crudrepository)。

在crud repository的顶部,有一个PagingAndSortingRepository的抽象,它添加了额外的方法来简化对实体的分页访问:

举例

public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
  //排序
  Iterable<T> findAll(Sort sort);
  //分页
  Page<T> findAll(Pageable pageable);
}

要访问页面大小为20的User的第二个页面,您可以执行如下操作

PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法外,还可以提供计数和删除查询的查询派生。下面的列表显示了派生计数查询的接口定义:

interface UserRepository extends CrudRepository<User, Long> {

  long countByLastname(String lastname);
}

下面的列表显示了派生删除查询的接口定义:

interface UserRepository extends CrudRepository<User, Long> {

  long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);
}

查询方法

标准CRUD功能存储库通常对底层数据存储进行查询。使用Spring Data,声明这些查询变成了一个四个步骤的过程:

声明一个接口扩展了Repository或者它的一个子接口,并将它输入到它应该处理的域类和ID类型,如下面的例子所示:

interface PersonRepository extends Repository<Person, Long> { … }

在接口上声明查询方法。

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}

设置Spring为这些接口创建代理实例,可以使用javaconfigor和xml配置。

要使用Java配置,请创建一个类似如下的类:

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config { … }

要使用XML配置,定义一个类似如下的bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jpa="http://www.springframework.org/schema/data/jpa"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
     https://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/data/jpa
     https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

   <jpa:repositories base-package="com.acme.repositories"/>

</beans>

springboot可以这样配置

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

注入存储库实例并使用它,如下例所示:

class SomeClient {

  private final PersonRepository repository;

  SomeClient(PersonRepository repository) {
    this.repository = repository;
  }

  void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}

下面一步一步解读如何使用JPA

定义存储库接口

定义存储库接口,首先需要定义一个域类特定的存储库接口。接口必须扩展存储库,并被输入域类和ID类型。

微调库定义

通常,你的存储库接口会扩展为:repository,CrudRepository,或pagingandsortingrepository。或者,如果你不想扩展Spring Data接口,你也可以给你的存储库接口加上注解@RepositoryDefinition.Extending CrudRepository公开一套完整的方法来操作你的实体。如果您希望对公开的方法有选择性,可以将你想从crudrepository中公开的方法复制到你的域库中。

下面的例子展示了如何有选择地公开CRUD方法(在本例中是findByIdandsave):

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

在前面的示例中,为所有域存储库和exposedfindById(…)以及save(…)定义了公共基接口。这些方法被路由到Spring Data提供的存储库的基本存储库实现中(例如,如果使用JPA,则实现为simplejarepository),因为它们与CrudRepository中的方法签名相匹配。因此UserRepository现在可以保存用户,通过ID查找单个用户,并触发对findUsersby电子邮件地址的查询。

使用带有多个Spring数据模块的存储库(怎么确定数据接口绑定哪一个表?)

在应用程序中使用唯一的Spring Data模块可以使事情变得简单,因为已定义范围内的所有存储库接口都绑定到Spring Data模块。有时,应用程序需要使用多个Spring Data模块。在这种情况下,存储库定义必须区分持久性技术。当它检测到类路径上有多个存储库工厂时,Spring Data将进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来决定存储库定义的Spring Data模块绑定:

        如果存储库定义扩展了特定于模块的存储库,那么它就是特定Spring Data模块的有效候选。

        如果域类使用特定于模块的类型注释进行注释,则它是特定Spring Data模块的有效候选。Spring Data模块接受第三方注解(如JPA ' s@Entity)或提供自己的注解(如as@Documentfor Spring Data MongoDB和Spring Data Elasticsearch)。

下面的例子展示了一个使用特定模块接口的存储库(在这个例子中是JPA):

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }

interface UserRepository extends MyBaseRepository<User, Long> { … }

MyRepository和UserRepository在它们的类型层次结构中扩展了jarepository。它们是Spring Data JPA模块的有效候选对象。

面的例子展示了一个使用通用接口的存储库:

interface AmbiguousRepository extends Repository<User, Long> { … }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }

AmbiguousRepository和AmbiguousUserRepository在它们的类型层次结构中只扩展了Repository和crudrepository。虽然在使用唯一的Spring Data模块时这很好,但多个模块无法区分这些存储库应该绑定到哪个特定的Spring Data。

下面的例子展示了一个使用带注解的域类的存储库:

interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

interface UserRepository extends Repository<User, Long> { … }

@Document
class User { … }

PersonRepository引用Person, Person是用JPA@Entity注释标注的,所以这个存储库显然属Spring Data JPA。UserRepository引用User,它被Spring Data MongoDB@Documentannotation注解。PersonRepository引用Person, Person是用JPA@Entity注释标注的,所以这个存储库显然属Spring Data JPA。UserRepository引用User,它被Spring Data MongoDB   用@Document 注解。

下面这个糟糕的例子展示了一个使用混合注解的域类的存储库:

interface JpaPersonRepository extends Repository<Person, Long> { … }

interface MongoDBPersonRepository extends Repository<Person, Long> { … }

@Entity
@Document
class Person { … }

 这个例子展示了一个使用JPA和Spring Data MongoDB注解的域类。它定义了两个存储库,JpaPersonRepository和MongoDBPersonRepository。一个用于JPA,另一个用于MongoDB。Spring Data不再能够区分存储库,这将导致未定义的行为。

存储库类型细节和区分域类注释用于严格的存储库配置,以识别特定Spring Data模块的存储库候选。在相同的域类型上使用多个特定于持久性技术的注释是可能的,并允许跨多个持久性技术重用域类型。然而,Spring Data再也不能确定要绑定存储库的唯一模块。

最后一种区分存储库的方法是确定存储库基础包的范围。基础包定义了扫描存储库接口定义的起点,这意味着将存储库定义放置在适当的包中。默认情况下,注解驱动的配置使用配置类的包。在基于xml的配置中,基本包是必需的。

下面的例子展示了注释驱动的基本包配置:

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

定义查询方法

存储库代理有两种方法从方法名派生一个特定于存储库的查询:

通过直接从方法名派生查询。

通过使用手动定义的查询。

可用的选项取决于实际的存储。但是,必须有一个策略来决定创建什么实际的查询。下一节将介绍可用的选项。

查询查找策略

存储库基础设施可以使用以下策略来解析查询。使用XML配置,您可以通过query-lookup-strategy属性在名称空间上配置策略。对于Java配置,您可以使用Enable${store}Repositories注释的queryLookupStrategy属性。某些策略可能不支持特定的数据存储。

CREATE尝试从查询方法名构造一个特定于存储的查询。一般的方法是从方法名中删除一组已知的前缀,然后解析方法的其余部分。您可以在“查询创建”中阅读有关查询构造的更多信息。

USE_DECLARED_QUERY试图找到一个声明过的查询,如果找不到则抛出异常。查询可以通过某处的注释定义,也可以通过其他方式声明。请参阅特定商店的文档,以查找该商店的可用选项。如果存储库基础设施在引导时没有找到为该方法声明的查询,则会失败。

CREATE_IF_NOT_FOUND(默认)结合了CREATE和USE_DECLARED_QUERY。它首先查找一个声明的查询,如果没有找到声明的查询,它就创建一个基于自定义方法名的查询。这是默认的查找策略,因此,如果您没有显式地配置任何内容,就会使用此策略。它允许通过方法名快速定义查询,也允许根据需要引入已声明的查询对这些查询进行自定义调优。

创建查询

Spring Data存储库基础设施中内置的查询构建器机制对于在存储库实体上构建约束查询非常有用。

下面的例子展示了如何创建多个查询:

下面注意看注释,这里我不解释了

interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析查询方法名称分为主语和谓语。第一部分(find…By,exists…By)定义查询的主题,第二部分形成谓词。引入子句(主语)可以包含进一步的表达式。在find(或其他引入的关键字)和By之间的任何文本都被认为是描述性的,除非使用一个限制结果的关键字,比如在要创建的查询上设置一个不同的标志,或者使用top /First来限制查询结果。

附录中包含了查询方法主题关键字和查询方法谓词关键字的完整列表,包括排序和字母大小写修饰符。但是,第一个By充当分隔符,指示实际条件谓词的开始。在非常基础的级别上,您可以在实体属性上定义条件,并将它们与and和Or连接起来。

解析该方法的实际结果取决于为其创建查询的持久性存储。然而,有一些一般的事情需要注意:

表达式通常是属性遍历和可以连接的操作符。可以将属性表达式与AND和OR结合使用。对于属性表达式,还支持Between、LessThan、GreaterThan和like等操作符。受支持的操作符可能因数据存储的不同而不同,因此请参阅参考文档中相应的部分。

方法解析器支持为单个属性(例如,findByLastnameIgnoreCase(…))或为支持忽略大小写的类型的所有属性(通常是Stringinstances -例如,findByLastnameAndFirstnameAllIgnoreCase(…))设置IgnoreCase标志。是否支持忽略大小写可能因存储而异,因此请参阅参考文档中的相关章节,以了解特定于存储的查询方法。

您可以通过将anOrderByclause附加到引用属性的查询方法并提供排序方向(Asc或Desc)来应用静态排序。要创建支持动态排序的查询方法,请参见“特殊参数处理”。

属性表达式(嵌套属性查询)

属性表达式只能引用托管实体的直接属性,如前面的示例所示。在创建查询时,您已经确保解析的属性是托管域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。考虑以下方法签名:

List<Person> findByAddressZipCode(ZipCode zipCode);

假设Person有一个带有邮政编码的地址。在这种情况下,该方法将创建x.address. zipcode属性遍历。解析算法首先将整个部分(AddressZipCode)解释为属性,并在域类中检查具有该名称的属性(不大写)。如果算法成功,它将使用该属性。如果没有,算法将驼峰型部分的源从右侧分割为头部和尾部,并试图找到相应的属性——在我们的示例中,AddressZip和Code。如果算法找到一个有头部的属性,它就会取尾部,并从那里继续构建树,按照刚才描述的方式将尾部向上分割。如果第一次分割不匹配,算法将分割点向左移动(Address,ZipCode)并继续。

虽然这在大多数情况下都是可行的,但是算法可能会选择错误的属性。假设Person类也有一个addressZip属性。算法将在第一轮分割中匹配,选择错误的属性,并失败(因为addressZip的类型可能没有code属性)。

要解决这种模糊性,可以在方法名中使用_来手动定义遍历点。所以我们的方法名是这样的:

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准的Java命名约定(也就是说,在属性名称中不使用下划线,而是使用驼峰大小写)。

特殊参数处理(比如分页,排序)

要处理查询中的参数,请按照前面的示例定义方法参数。除此之外,基础设施还可以识别某些特定类型,比如Pageable和Sort,以便动态地对查询应用分页和排序。下面的例子演示了这些特性:

Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Pageable pageable);

接受Sort和Pageable的api希望将非空值传递到方法中。如果您不想应用任何排序或分页,请使用sort .unsorted()和pageable .unpage()。

第一个方法允许您向查询方法传递一个org.springframework.data.domain. pageable实例,以动态地向静态定义的查询添加分页。pageage知道可用的元素和页面的总数。它通过基础结构触发一个count查询来计算总数量来实现这一点。因为这可能会很昂贵(取决于所使用的商店),所以您可以返回一个Slice。Slice只知道下一个Slice是否可用,这在遍历更大的结果集时可能就足够了。

排序选项也通过Pageable实例处理。如果你只需要排序,添加一个org.springframework.data.domain. sort参数到你的方法中。如您所见,也可以返回List。在这种情况下,没有创建构建实际Page实例所需的额外元数据(这意味着没有发出必要的额外计数查询)。相反,它限制查询只能查找给定范围内的实体。

要了解整个查询得到多少页面,必须触发一个额外的count查询。默认情况下,此查询派生自您实际触发的查询。

分页和排序

您可以通过使用属性名来定义简单的排序表达式。可以连接表达式以将多个条件收集到一个表达式中。

Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

要使用更类型安全的方式来定义排序表达式,首先要定义排序表达式的类型,然后使用方法引用来定义要排序的属性。

TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());

TypedSort.by(…)通过(通常)使用CGlib来使用运行时代理,当使用诸如Graal VM native这样的工具时,CGlib可能会干扰本地映像编译。

如果您的存储实现支持Querydsl,您还可以使用生成的元模型类型来定义排序表达式

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

 限制查询结果

您可以通过使用第一个或最前面的关键字来限制查询方法的结果,您可以交替使用这两个关键字。您可以将一个可选的数值附加到top或first以指定要返回的最大结果大小。如果该数字被忽略,则假定结果大小为1。下面的示例展示了如何限制查询大小:

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

限制表达式还为支持不同查询的数据存储支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持使用Optional关键字将结果包装。

如果分页或切片应用于有限的查询分页(以及可用页面数量的计算),则它将应用于有限的结果。

通过使用Sort参数来限制结果与动态排序的结合使用,可以表示最小的“K”元素和最大的“K”元素的查询方法。

存储库方法返回集合或可迭代对象(推荐看文档)

返回多个结果的查询方法可以使用标准的Java Iterable、List和set。除此之外,我们还支持返回Spring Data的streamable, iterable的自定义扩展,以及vavr提供的集合类型。请参阅附录,解释所有可能的查询方法返回类型。

使用Streamable作为查询方法返回类型
你可以使用Streamable作为Iterableor任何集合类型的替代品。它提供了方便的方法来访问非parallelstream(缺少fromable),并能够直接....filter(…)和....map(…)在元素上,并将Streamable连接到其他元素:

使用Streamable组合查询方法结果

interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

返回自定义可流包装类型 

为集合提供专用包装器类型是为返回多个元素的查询结果提供API的常用模式。通常,通过调用存储库方法返回类似于集合的类型并手动创建包装器类型的实例来使用这些类型。你可以避免这个额外的步骤,因为Spring Data允许你使用这些包装器类型作为查询方法的返回类型,如果它们满足以下条件:

        该类型实现了Streamable。

        该类型公开了一个构造函数或一个名为of(…)或valueof(…)的静态工厂方法,该方法以streamableas为参数。

class Product {                                         
  MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {         

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {                    
    return streamable.stream()
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {                 
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); 
}

支持Vavr集合

存储库方法的空处理

从Spring Data 2.0开始,存储库中返回单个聚合实例的CRUD方法使用Java 8 's Optional 表示可能缺少一个值。除此之外,Spring Data支持在查询方法上返回以下包装器类型:

com.google.common.base.Optional

scala.Option

io.vavr.control.Option

或者,查询方法可以选择完全不使用包装器类型。查询结果的缺失由返回null表示。返回集合、集合替代、包装器和流的存储库方法保证永远不会返回null,而是返回相应的空表示。有关详细信息,请参阅“存储库查询返回类型”。

 Nullability注释

你可以通过spring框架的空性注解来表达存储库方法的空性约束。它们提供了一种工具友好的方法,并在运行时选择插入空检查,如下所示:

        @NonNullApi:用于包级别,声明参数和返回值的默认行为分别是不接受或不产生空值。

        @NonNull:用于不能是null的参数或返回值(不需要的参数和返回值where@NonNullApiapplies)。

        @Nullable:用于可能是null的参数或返回值。

一旦设置了非空默认值,存储库查询方法调用将在运行时验证其可空性约束。如果查询结果违反了定义的约束,则抛出异常。这种情况发生在方法返回null但声明为非空(默认值是在存储库所在的包上定义的注释)时。如果您想再次选择可为空的结果,有选择地在单个方法上使用@Nullable。使用本节开始时提到的结果包装器类型继续按预期工作:空结果被转换为表示缺席的值。

下面的例子展示了刚才描述的一些技术:

package com.acme;                                                       

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);                    

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);          

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); 
}

存储库驻留在我们为其定义了非空行为的包(或子包)中。
当查询没有产生结果时抛出一个EmptyResultDataAccessException异常。当传递给该方法的emailAddress为空时,抛出非法参数异常。
当查询没有产生结果时,返回null。也接受null作为emailAddress的值。
当查询没有产生结果时,返回optional.empty()。当传递给该方法的emailAddress为空时抛出一个IllegalArgumentException。

基于kotlin的库中的可空性

流媒体查询结果

您可以使用Java 8 Stream<t>作为返回类型,以增量方式处理查询方法的结果。</t>不是将查询结果包装在一个流中,而是使用特定于数据存储的方法来执行流,如下例所示:

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

异步查询结果

通过使用spring的异步方法运行功能,可以异步运行存储库查询。这意味着当实际的查询发生在已提交给SpringTaskExecutor的任务中时,该方法在调用时立即返回。异步查询不同于反应性查询,不应该混合使用。有关反应性支持的更多细节,请参阅特定于商店的文档。下面的例子展示了许多异步查询: 

@Async
Future<User> findByFirstname(String firstname);               

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值