Spring Data (数据)JDBC(二)

时间:2022-11-21 15:00:50

Spring Data (数据)JDBC(二)

9. JDBC 存储库

本章指出了 JDBC 存储库支持的特殊性。这建立在使用 Spring 数据存储库中解释的核心存储库支持之上。 您应该对那里解释的基本概念有很好的理解。

9.1. 为什么选择Spring Data JDBC?

Java 世界中关系数据库的主要持久性 API 当然是 JPA,它有自己的 Spring Data 模块。 为什么还有另一个?

JPA做了很多事情来帮助开发人员。 除此之外,它还跟踪对实体的更改。 它为您进行延迟加载。 它允许您将各种对象构造映射到同样广泛的数据库设计阵列。

这很棒,使很多事情变得非常容易。 只需看一个基本的 JPA 教程。 但是,对于JPA为什么要做某件事,它经常变得非常混乱。 此外,在概念上非常简单的事情在 JPA 中变得相当困难。

Spring Data JDBC旨在通过接受以下设计决策,在概念上变得更加简单:

  • 如果加载实体,则会运行 SQL 语句。 完成此操作后,您将拥有一个完全加载的实体。 不执行延迟加载或缓存。
  • 如果保存实体,则会保存该实体。 如果你不这样做,它就不会。 没有肮脏的跟踪,也没有会话。
  • 有一个如何将实体映射到表的简单模型。 它可能只适用于相当简单的情况。 如果你不喜欢这样,你应该编写自己的策略。 Spring Data JDBC仅提供非常有限的支持,用于使用注释自定义策略。

9.2. 领域驱动设计和关系数据库。

所有 Spring 数据模块的灵感都来自领域驱动设计的“存储库”、“聚合”和“聚合根”的概念。 这些对于Spring Data JDBC来说可能更为重要,因为它们在某种程度上与使用关系数据库时的正常做法相反。

聚合是一组实体,保证在对其进行原子更改之间保持一致。 一个典型的例子是安维斯。 属性 on(例如,与实际数量一致)在进行更改时保持一致。​​Order​​​​OrderItems​​​​Order​​​​numberOfItems​​​​OrderItems​

不能保证跨聚合的引用始终保持一致。 它们最终保证会保持一致。

每个聚合只有一个聚合根,它是聚合的实体之一。 聚合仅通过该聚合根上的方法进行操作。 这些是前面提到的原子变化。

存储库是对持久存储的抽象,看起来像是某种类型的所有聚合的集合。 对于一般的 Spring 数据,这意味着您希望每个聚合根都有一个。 此外,对于 Spring Data JDBC,这意味着可从聚合根访问的所有实体都被视为该聚合根的一部分。 Spring Data JDBC假设只有聚合具有存储聚合的非根实体的表的外键,并且没有其他实体指向非根实体。​​Repository​

在当前实现中,从聚合根引用的实体由 Spring Data JDBC 删除并重新创建。

您可以使用符合您的工作方式和设计数据库风格的实现来覆盖存储库方法。

9.3. 入门

引导设置工作环境的一种简单方法是在STS或SpringInitializr中创建基于Spring的项目。

首先,您需要设置正在运行的数据库服务器。请参阅供应商文档,了解如何配置 JDBC 访问数据库。

要在 STS 中创建 Spring 项目,请执行以下操作:

  1. 转到→“新建→ Spring 模板项目”→“简单 Spring 实用程序项目”,并在出现提示时按“是”。然后输入项目和包名称,例如。org.spring.jdbc.example
  2. 将以下内容添加到文件元素:pom.xmldependencies
<dependencies>

<!-- other dependency elements omitted -->

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>3.0.0</version>
</dependency>

</dependencies>
  1. 将 pom 中的 Spring 版本.xml更改为
<spring.framework.version>6.0.0</spring.framework.version>
  1. 将 Maven 的 Spring 里程碑存储库的以下位置添加到您的位置,使其与 yourelement 处于同一级别:pom.xml<dependencies/>
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Maven MILESTONE Repository</name>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>

存储库也可以在此处浏览。

9.4. 示例存储库

有一个GitHub 存储库,其中包含几个示例,您可以下载并试用这些示例,以了解库的工作原理。

9.5. 基于注释的配置

Spring Data JDBC 存储库支持可以通过 Java 配置的注释来激活,如以下示例所示:

例 48。使用 Java 配置的 Spring Data JDBC 存储库

@Configuration
@EnableJdbcRepositories
class ApplicationConfig extends AbstractJdbcConfiguration {

@Bean
DataSource dataSource() {

EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
return builder.setType(EmbeddedDatabaseType.HSQL).build();
}

@Bean
NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}

@Bean
TransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

​@EnableJdbcRepositories​​​为派生自的接口创建实现​​Repository​

​AbstractJdbcConfiguration​​提供 Spring Data JDBC 所需的各种默认 bean

创建与数据库的连接。 这是以下两种 Bean 方法所必需的。​​DataSource​

创建由 Spring Data JDBC 用于访问数据库。​​NamedParameterJdbcOperations​

Spring Data JDBC利用Spring JDBC提供的事务管理。

前面示例中的配置类使用 API 设置嵌入式 HSQL 数据库。 然后,泰斯用于设置和a。 我们最终通过使用 Spring Data JDBC 存储库激活。 如果未配置基础包,则使用配置类所在的包。 扩展可确保各种豆子注册。 覆盖其方法可用于自定义设置(见下文)。​​EmbeddedDatabaseBuilder​​​​spring-jdbc​​​​DataSource​​​​NamedParameterJdbcOperations​​​​TransactionManager​​​​@EnableJdbcRepositories​​​​AbstractJdbcConfiguration​

通过使用 Spring Boot 可以进一步简化此配置。 一旦启动器包含在依赖项中,Spring 启动就足够了。 其他一切都由Spring Boot完成。​​DataSource​​​​spring-boot-starter-data-jdbc​

在此设置中,可能需要自定义一些内容。

9.5.1. 方言

Spring Data JDBC 使用接口的实现来封装特定于数据库或其 JDBC 驱动程序的行为。 默认情况下,尝试确定正在使用的数据库并注册正确的数据库。 可以通过覆盖来更改此行为。​​Dialect​​​​AbstractJdbcConfiguration​​​​Dialect​​​​jdbcDialect(NamedParameterJdbcOperations)​

如果使用没有方言可用的数据库,则应用程序将无法启动。在这种情况下,您必须要求供应商提供实现。或者,您可以:​​Dialect​

  1. 实现你自己的。Dialect
  2. 实现返回。JdbcDialectProviderDialect
  3. 通过在下面创建资源来注册提供程序,并通过添加一行来执行注册spring.factoriesMETA-INF
    org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=<fully qualified name of your JdbcDialectProvider>

9.6. 持久实体

可以使用该方法执行保存聚合。 如果聚合是新的,则会导致对聚合根进行插入,然后对所有直接或间接引用的实体进行插入语句。​​CrudRepository.save(…)​

如果聚合根不是新的,则会删除所有引用的实体,更新聚合根,并再次插入所有引用的实体。 请注意,实例是否为新实例是实例状态的一部分。

这种方法有一些明显的缺点。 如果实际上只更改了很少的引用实体,则删除和插入是浪费。 虽然这个过程可以而且可能会得到改进,但Spring Data JDBC可以提供的功能存在一定的局限性。 它不知道聚合的先前状态。 因此,任何更新过程都必须始终获取它在数据库中找到的任何内容,并确保将其转换为传递给 save 方法的实体的状态。

9.6.1. 对象映射基础

本节介绍了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不变性的基础知识。 请注意,本节仅适用于不使用底层数据存储的对象映射(如 JPA)的 Spring 数据模块。 此外,请务必查阅特定于存储的部分,了解特定于存储的对象映射,例如索引、自定义列或字段名称等。

Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。 这意味着我们需要两个基本步骤:

  1. 使用公开的构造函数之一创建实例。
  2. 实例填充以具体化所有公开的属性。
对象创建

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  2. 如果只有一个构造函数,则使用它。
  3. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  4. 如果类型是 Java,则使用规范构造函数。Record
  5. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

值解析假定构造函数/工厂方法参数名称与实体的属性名称匹配,即解析将像要填充属性一样执行,包括映射中的所有自定义(不同的数据存储列或字段名称等)。 这还需要类文件中可用的参数名称信息或构造函数上存在的枚举注释。​​@ConstructorProperties​

值解析可以通过使用特定于商店的SpEL表达式使用Spring Framework的值注释来自定义。 有关更多详细信息,请参阅商店特定映射部分。​​@Value​

对象创建内部

为了避免反射的开销,Spring Data 对象创建默认使用运行时生成的工厂类,该工厂类将直接调用域类构造函数。 即对于此示例类型:

class Person {
Person(String firstname, String lastname) { … }
}

我们将在运行时创建一个语义等同于此工厂类的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}

This gives us a roundabout 10% performance boost over reflection. For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:

  • it must not be a private class
  • it must not be a non-static inner class
  • it must not be a CGLib proxy class
  • the constructor to be used by Spring Data must not be private

If any of these criteria match, Spring Data will fall back to entity instantiation via reflection.

Property population

创建实体的实例后,Spring Data 将填充该类的所有剩余持久属性。 除非已由实体的构造函数填充(即通过其构造函数参数列表使用),否则将首先填充标识符属性以允许解析循环对象引用。 之后,将在实体实例上设置构造函数尚未填充的所有非瞬态属性。 为此,我们使用以下算法:

  1. 如果属性是不可变的,但公开了 amethod(见下文),我们使用该方法创建一个具有新属性值的新实体实例。with…with…
  2. 如果定义了属性访问(即通过 getter 和 setter 的访问),我们将调用 setter 方法。
  3. 如果属性是可变的,我们直接设置字段。
  4. 如果属性是不可变的,我们将使用持久性操作要使用的构造函数(请参阅对象创建)来创建实例的副本。
  5. 默认情况下,我们直接设置字段值。

属性人口内部

与对象构造中的优化类似,我们还使用 Spring 数据运行时生成的访问器类与实体实例进行交互。

class Person {

private final Long id;
private String firstname;
private @AccessType(Type.PROPERTY) String lastname;

Person() {
this.id = null;
}

Person(Long id, String firstname, String lastname) {
// Field assignments
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastame);
}

void setLastname(String lastname) {
this.lastname = lastname;
}
}

例 49。生成的属性访问器

class PersonPropertyAccessor implements PersistentPropertyAccessor {

private static final MethodHandle firstname;

private Person person;

public void setProperty(PersistentProperty property, Object value) {

String name = property.getName();

if ("firstname".equals(name)) {
firstname.invoke(person, (String) value);
} else if ("id".equals(name)) {
this.person = person.withId((Long) value);
} else if ("lastname".equals(name)) {
this.person.setLastname((String) value);
}
}
}


PropertyAccessor 保存基础对象的可变实例。这是为了启用其他不可变属性的突变。


默认情况下,Spring 数据使用字段访问来读取和写入属性值。根据字段的可见性规则,用于与字段进行交互。​​private​​​​MethodHandles​


该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。调用创建一个新对象。所有后续突变都将发生在新实例中,而之前的突变保持不变。​​withId(…)​​​​withId(…)​​​​Person​


使用属性访问允许直接调用方法,而无需使用。​​MethodHandles​

这为我们提供了大约 25% 的性能提升。 要使域类符合此类优化的条件,它需要遵守一组约束:

  • 类型不得驻留在默认值或包下。java
  • 类型及其构造函数必须是public
  • 内部类的类型必须是。static
  • 使用的 Java 运行时必须允许在原始文件中声明类。Java 9 及更高版本施加了某些限制。ClassLoader

默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看一下以下实体:

例 50。示例实体

class Person {

private final @Id Long id;
private final String firstname, lastname;
private final LocalDate birthday;
private final int age;

private String comment;
private @AccessType(Type.PROPERTY) String remarks;

static Person of(String firstname, String lastname, LocalDate birthday) {

return new Person(null, firstname, lastname, birthday,
Period.between(birthday, LocalDate.now()).getYears());
}

Person(Long id, String firstname, String lastname, LocalDate birthday, int age) {

this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.birthday = birthday;
this.age = age;
}

Person withId(Long id) {
return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
}

void setRemarks(String remarks) {
this.remarks = remarks;
}
}


标识符属性是最终的,但在构造函数中设置为。 该类公开用于设置标识符的方法,例如,当实例插入数据存储并生成标识符时。 创建新实例时,原始实例保持不变。 相同的模式通常应用于存储管理的其他属性,但可能必须更改持久性操作。 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,设置属性将转换为创建应用了新标识符值的新实例。​​null​​​​withId(…)​​​​Person​


和属性是普通的不可变属性,可能通过 getter 公开。​​firstname​​​​lastname​


属性是不可变的,但派生自属性。 按照所示的设计,数据库值将胜过默认值,因为 Spring Data 使用唯一声明的构造函数。 即使意图是首选计算,重要的是此构造函数也采用 as 参数(可能忽略它),否则属性填充步骤将尝试设置 age 字段并失败,因为它是不可变的并且不存在任何方法。​​age​​​​birthday​​​​age​​​​with…​


属性是可变的,通过直接设置其字段来填充。​​comment​


属性是可变的,并通过直接设置 thefield 或通过调用 setter 方法来填充​​remarks​​​​comment​


该类公开用于创建对象的工厂方法和构造函数。 这里的核心思想是使用工厂方法而不是其他构造函数,以避免通过构造函数消除歧义的需要。 相反,属性的默认值在工厂方法中处理。 如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用 注释。​​@PersistenceCreator​​​​@PersistenceCreator​

一般性建议
  • 尝试坚持使用不可变对象 - 不可变对象很容易创建,因为具体化对象只需调用其构造函数即可。 此外,这可以避免域对象充斥着允许客户端代码操作对象状态的 setter 方法。 如果需要这些,最好使它们受到包保护,以便只能由有限数量的共存类型调用它们。 仅构造函数具体化比属性填充快 30%。
  • 提供全参数构造函数 — 即使不能或不想将实体建模为不可变值,提供将实体的所有属性(包括可变属性)作为参数的构造函数仍然有价值,因为这允许对象映射跳过属性填充以获得最佳性能。
  • 使用工厂方法而不是重载的构造函数来避免​​@PersistenceCreator​​ — 使用最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序用例的构造函数,这些构造函数省略了自动生成的标识符等内容。 这是一种既定模式,而是使用静态工厂方法来公开 all-args 构造函数的这些变体。
  • 确保遵守允许使用生成的实例化器和属性访问器类的约束
  • 对于要生成的标识符,仍将最终字段与全参数持久性构造函数(首选)或​​with​​...方法
  • 使用 Lombok 避免样板代码 — 由于持久性操作通常需要构造函数获取所有参数,因此它们的声明变成了对字段分配的繁琐重复,而使用 Lombok 可以最好地避免这些参数。@AllArgsConstructor
覆盖属性

Java允许灵活设计域类,其中子类可以定义已在其超类中声明具有相同名称的属性。 请考虑以下示例:

public class SuperType {

private CharSequence field;

public SuperType(CharSequence field) {
this.field = field;
}

public CharSequence getField() {
return this.field;
}

public void setField(CharSequence field) {
this.field = field;
}
}

public class SubType extends SuperType {

private String field;

public SubType(String field) {
super(field);
this.field = field;
}

@Override
public String getField() {
return this.field;
}

public void setField(String field) {
this.field = field;

// optional
super.setField(field);
}
}

这两个类都使用可赋值类型定义。 根据类设计,使用构造函数可能是唯一的默认设置方法。 或者,调用二传手可以设置。 所有这些机制在某种程度上都会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 如果类型不可分配,则 Spring Data 将跳过超类型属性。 也就是说,重写属性的类型必须可分配给其超类型属性类型才能注册为重写,否则超类型属性被视为暂时性属性。 我们通常建议使用不同的属性名称。​​field​​​​SubType​​​​SuperType.field​​​​SuperType.field​​​​super.setField(…)​​​​field​​​​SuperType​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 不能使用,因为如果不对 setter 实现进行任何进一步的假设,通常无法设置超级属性。@AccessType(PROPERTY)
Kotlin 支持

Spring Data 调整了 Kotlin 的细节,以允许对象创建和更改。

Kotlin 对象创建

Kotlin 类支持实例化,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。

Spring Data 自动尝试检测持久实体的构造函数,以用于具体化该类型的对象。 解析算法的工作原理如下:

  1. 如果存在带有注释的构造函数,则使用它。@PersistenceCreator
  2. 如果类型是Kotlin 数据 cass,则使用主构造函数。
  3. 如果有一个静态工厂方法注释,则使用它。@PersistenceCreator
  4. 如果只有一个构造函数,则使用它。
  5. 如果有多个构造函数,并且只有一个被批注,则使用它。@PersistenceCreator
  6. 如果类型是 Java,则使用规范构造函数。Record
  7. 如果存在无参数构造函数,则使用它。 其他构造函数将被忽略。

请考虑以下类:​​data​​​​Person​

data class Person(val id: String, val name: String)

上面的类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数来自定义此类并对其进行注释以指示构造函数首选项:​​@PersistenceCreator​

data class Person(var id: String, val name: String) {

@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}

Kotlin 通过允许在未提供参数时使用默认值来支持参数可选性。 当 Spring Data 检测到参数默认值的构造函数时,如果数据存储不提供值(或只是返回),则这些参数将保留为不存在,以便 Kotlin 可以应用参数默认值。请考虑以下应用参数默认值的类​​null​​​​name​

data class Person(var id: String, val name: String = "unknown")

每次参数不是结果的一部分或其值是时,则默认为。​​name​​​​null​​​​name​​​​unknown​

Kotlin 数据类的属性填充

在 Kotlin 中,默认情况下所有类都是不可变的,并且需要显式属性声明来定义可变属性。 请考虑以下类:​​data​​​​Person​

data class Person(val id: String, val name: String)

此类实际上是不可变的。 它允许在 Kotlin 生成方法时创建新实例,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用于该方法。​​copy(…)​

Kotlin 覆盖属性

Kotlin 允许声明属性覆盖以更改子类中的属性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
SuperType(field) {
}

这种安排呈现两个具有名称的属性。 Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。 实际上,代码如下所示:​​field​

public class SuperType {

private int field;

public SuperType(int field) {
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}

public final class SubType extends SuperType {

private int field;

public SubType(int field) {
super(field);
this.field = field;
}

public int getField() {
return this.field;
}

public void setField(int field) {
this.field = field;
}
}

吉特和二传手仅发病,不发病。 在这种安排中,使用构造函数是唯一要设置的默认方法。 可以添加方法toto setvia,但不属于支持的约定。 属性重写在某种程度上会产生冲突,因为属性共享相同的名称,但可能表示两个不同的值。 我们通常建议使用不同的属性名称。​​SubType​​​​SubType.field​​​​SuperType.field​​​​SuperType.field​​​​SubType​​​​SuperType.field​​​​this.SuperType.field = …​

Spring 数据模块通常支持保存不同值的被覆盖属性。 从编程模型的角度来看,需要考虑以下几点:

  1. 应保留哪个属性(默认为所有声明的属性)? 您可以通过用这些属性批注来排除属性。@Transient
  2. 如何表示数据存储中的属性? 对不同的值使用相同的字段/列名称通常会导致数据损坏,因此应使用显式字段/列名称至少对其中一个属性进行批注。
  3. 使用不能使用,因为无法设置超级属性。@AccessType(PROPERTY)

9.6.2. 实体中支持的类型

当前支持以下类型的属性:

  • 所有基元类型及其装箱类型(,,,,等)intfloatIntegerFloat
  • 枚举映射到其名称。
  • ​String​
  • ​java.util.Date​​和java.time.LocalDatejava.time.LocalDateTimejava.time.LocalTime
  • 如果您的数据库支持,则可以将上述类型的数组和集合映射到数组类型的列。
  • 数据库驱动程序接受的任何内容。
  • 对其他实体的引用。 它们被视为一对一关系或嵌入类型。 一对一关系实体具有属性是可选的。 引用实体的表应具有一个附加列,其名称基于引用实体,请参阅反向引用。 嵌入实体不需要。 如果存在一个,它将被忽略。idid
  • ​Set<some entity>​​被视为一对多关系。 引用实体的表应具有一个附加列,其名称基于引用实体,请参阅反向引用。
  • ​Map<simple type, some entity>​​被视为合格的一对多关系。 引用实体的表应具有两个附加列:一个基于外键的引用实体命名(请参阅反向引用),另一个具有相同的名称和映射键的附加后缀。 您可以通过分别实现和来更改此行为。 或者,您可以使用_keyNamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)NamingStrategy.getKeyColumn(RelationalPersistentProperty property)@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")
  • ​List<some entity>​​映射为 a。Map<Integer, some entity>
引用的实体

对引用实体的处理是有限的。 这是基于上述聚合根的思想。 如果引用另一个实体,则根据定义,该实体是聚合的一部分。 因此,如果删除引用,则会删除以前引用的实体。 这也意味着引用是 1-1 或 1-n,而不是 n-1 或 n-m。

如果您有 n-1 或 n-m 引用,则根据定义,您将处理两个单独的聚合。 它们之间的引用可以编码为简单值,这些值与Spring Data JDBC正确映射。 对这些进行编码的更好方法是使它们成为实例。 Anis 一个围绕 id 值的包装器,它将该值标记为对不同聚合的引用。 此外,该聚合的类型在类型参数中编码。​​id​​​​AggregateReference​​​​AggregateReference​

返回参考文献

聚合中的所有引用都会在数据库中产生相反方向的外键关系。 缺省情况下,外键列的名称是引用实体的表名。

或者,您可以选择按引用实体的实体名称命名它们,忽略注释。 您可以通过调用来激活此行为。​​@Table​​​​setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING)​​​​RelationalMappingContext​

Forand引用 保存列表索引或映射键需要额外的列。它基于带有附加后缀的外键列。​​List​​​​Map​​​​_KEY​

如果你想要一种完全不同的方法来命名这些反向引用,你可以以适合你需求的方式实现。​​NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)​

例 51。声明和设置​​AggregateReference​

class Person {
@Id long id;
AggregateReference<Person, Long> bestFriend;
}

// ...

Person p1, p2 = // some initialization

p1.bestFriend = AggregateReference.to(p2.id);
  • 您注册适合的类型。

9.6.3. ​​NamingStrategy​

当您使用 JDBC 提供的 Spring Data 的标准实现时,它们需要特定的表结构。 您可以通过在应用程序上下文中提供命名策略来调整它。​​CrudRepository​

9.6.4. ​​Custom table names​

当命名策略与数据库表名不匹配时,您可以使用@Table注释自定义名称。 此批注的元素提供自定义表名。 下面的示例将类映射到数据库中的表:​​value​​​​MyEntity​​​​CUSTOM_TABLE_NAME​

@Table("CUSTOM_TABLE_NAME")
class MyEntity {
@Id
Integer id;

String name;
}

9.6.5. ​​Custom column names​

当命名策略与数据库列名称不匹配时,可以使用@Column注释自定义名称。 此批注的元素提供自定义列名。 下面的示例将类的属性映射到数据库中的列:​​value​​​​name​​​​MyEntity​​​​CUSTOM_COLUMN_NAME​

class MyEntity {
@Id
Integer id;

@Column("CUSTOM_COLUMN_NAME")
String name;
}

@MappedCollection批注可用于引用类型(一对一关系)或集、列表和映射(一对多关系)。元素为引用其他表中 id 列的外键列提供自定义名称。 在下面的示例中,类的相应表具有 acolumn,并且出于关系原因具有 theid 的列:​​idColumn​​​​MySubEntity​​​​NAME​​​​CUSTOM_MY_ENTITY_ID_COLUMN_NAME​​​​MyEntity​

class MyEntity {
@Id
Integer id;

@MappedCollection(idColumn = "CUSTOM_MY_ENTITY_ID_COLUMN_NAME")
Set<MySubEntity> subEntities;
}

class MySubEntity {
String name;
}

使用时,您必须有一个额外的列,用于数据集在理论中的位置或实体的键值。 此附加列名可以使用@MappedCollection注释的元素进行自定义:​​List​​​​Map​​​​List​​​​Map​​​​keyColumn​

class MyEntity {
@Id
Integer id;

@MappedCollection(idColumn = "CUSTOM_COLUMN_NAME", keyColumn = "CUSTOM_KEY_COLUMN_NAME")
List<MySubEntity> name;
}

class MySubEntity {
String name;
}

9.6.6. 嵌入式实体

嵌入实体用于在 Java 数据模型中具有值对象,即使数据库中只有一个表也是如此。 在下面的示例中,您会看到,这是使用注释映射的。 这样做的结果是,在数据库中,一个包含两列和(来自类)的表是预期的。​​MyEntity​​​​@Embedded​​​​my_entity​​​​id​​​​name​​​​EmbeddedEntity​

但是,如果列实际上在结果集中,则整个属性将根据theof设置为null,当所有嵌套属性都是对象时。
与此行为相反,尝试使用默认构造函数或接受结果集中的可为 null 的参数值的构造函数创建新实例。​​name​​​​null​​​​embeddedEntity​​​​onEmpty​​​​@Embedded​​​​null​​​​null​​​​USE_EMPTY​

例 52。嵌入对象的示例代码

class MyEntity {

@Id
Integer id;

@Embedded(onEmpty = USE_NULL)
EmbeddedEntity embeddedEntity;
}

class EmbeddedEntity {
String name;
}

​Null​​​西芬。 用于实例化属性的潜在值。​​embeddedEntity​​​​name​​​​null​​​​USE_EMPTY​​​​embeddedEntity​​​​null​​​​name​

如果在实体中多次需要值对象,则可以使用注释的可选元素来实现。 此元素表示前缀,并在嵌入对象中的每个列名前面加上前缀。​​prefix​​​​@Embedded​


利用快捷方式来减少冗长,并同时相应地设置 JSR-305。​​@Embedded.Nullable​​​​@Embedded.Empty​​​​@Embedded(onEmpty = USE_NULL)​​​​@Embedded(onEmpty = USE_EMPTY)​​​​@javax.annotation.Nonnull​




class MyEntity {

@Id
Integer id;

@Embedded.Nullable
EmbeddedEntity embeddedEntity;
}



的快捷方式。​​@Embedded(onEmpty = USE_NULL)​

包含 aor a 的嵌入实体将始终被视为非空实体,因为它们至少包含空集合或映射。 因此,这样的实体在使用@Embedded(onEmpty = USE_NULL)时永远不会偶数。​​Collection​​​​Map​​​​null​

9.6.7. 实体状态检测策略

下表描述了 Spring Data 提供的用于检测实体是否为新实体的策略:

表 2.用于检测实体是否是 Spring 数据中的新实体的选项

​@Id​​-属性检查(默认)

默认情况下,Spring 数据检查给定实体的标识符属性。 如果标识符属性 isorin 在基元类型的情况下,则假定该实体是新的。 否则,假定它不是新的。​​null​​​​0​

​@Version​​-物业检查

如果存在带批注的属性,或者如果是基元类型的版本属性,则认为该实体是新的。 如果 version 属性存在但具有不同的值,则该实体被视为不是新的。 如果不存在版本属性,Spring 数据将回退到标识符属性的检查。​​@Version​​​​null​​​​0​

实施​​Persistable​

如果一个实体实现了,Spring 数据会将新的检测委托给实体的方法。 有关详细信息,请参阅​​Javadoc​​​。​​Persistable​​​​isNew(…)​

注意:如果使用​​AccessType.PROPERTY,​​​则会检测并持久化​​Persistable​​​的属性。 为避免这种情况,请使用​​@Transient​​。

提供自定义实现​​EntityInformation​

您可以通过创建特定于模块的存储库工厂的子类并重写该方法来自定义存储库基本实现中使用的抽象。 然后,您必须将特定于模块的存储库工厂的自定义实现注册为 Spring Bean。 请注意,这应该很少是必需的。​​EntityInformation​​​​getEntityInformation(…)​

9.6.8. ID 生成

Spring Data JDBC使用ID来识别实体。 实体的ID必须用Spring Data的注释进行注释​​@Id​​。

当数据库具有 ID 列的自动增量列时,生成的值在将其插入数据库后在实体中设置。

一个重要的约束是,在保存实体后,该实体不得再是新的。 请注意,实体是否为新实体是实体状态的一部分。 对于自动增量列,这会自动发生,因为 Spring Data 使用 ID 列中的值设置了 ID。 如果不使用自动增量列,则可以使用 alistener,它设置实体的 ID(本文档稍后将介绍)。​​BeforeConvert​

9.6.9. 只读属性

带有注释的属性不会被 Spring Data JDBC 写入数据库,但是它们将在加载实体时被读取。​​@ReadOnlyProperty​

Spring Data JDBC 在写入实体后不会自动重新加载实体。 因此,如果要查看数据库中为此类列生成的数据,则必须显式重新加载它。

如果带批注的属性是实体或实体集合,则它由单独表中的一个或多个单独行表示。 Spring Data JDBC不会对这些行执行任何插入,删除或更新。

9.6.10. 仅插入属性

注释的属性将仅在插入操作期间由 Spring Data JDBC 写入数据库。 对于更新,这些属性将被忽略。​​@InsertOnlyProperty​

​@InsertOnlyProperty​​仅聚合根支持。

9.6.11. 乐观锁定

Spring Data JDBC支持通过聚合根上注释的数值属性@Version进行乐观锁定。 每当 Spring Data JDBC 保存具有此类版本属性的聚合时,都会发生两件事: 聚合根的更新语句将包含一个 where 子句,用于检查存储在数据库中的版本是否实际未更改。 如果不是这种情况,就会被抛出。 此外,版本属性在实体和数据库中都会增加,因此并发操作将注意到更改并抛出如上所述适用的 anif。​​OptimisticLockingFailureException​​​​OptimisticLockingFailureException​

此过程也适用于插入新的聚合,其中 aorversion 表示一个新实例,之后增加的实例将实例标记为不再新实例,这使得这在对象构造期间生成 id 的情况(例如使用 UUID 时)相当有效。​​null​​​​0​

在删除期间,版本检查也适用,但不会增加版本。

9.7. 查询方法

本节提供了一些关于 Spring Data JDBC 的实现和使用的具体信息。

通常在存储库上触发的大多数数据访问操作都会导致对数据库运行查询。 定义此类查询是在存储库接口上声明方法的问题,如以下示例所示:

例 53。具有查询方法的人员存储库

interface PersonRepository extends PagingAndSortingRepository<Person, String> {

List<Person> findByFirstname(String firstname);

List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable);

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

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

Person findByFirstnameAndLastname(String firstname, String lastname);

Person findFirstByLastname(String lastname);

@Query("SELECT * FROM person WHERE lastname = :lastname")
List<Person> findByLastname(String lastname);
@Query("SELECT * FROM person WHERE lastname = :lastname")
Stream<Person> streamByLastname(String lastname);

@Query("SELECT * FROM person WHERE username = :#{ principal?.username }")
Person findActiveUser();
}

该方法显示所有给定人员的查询。 查询是通过分析可与 and 连接的约束的方法名称派生的。 因此,方法名称导致查询表达式。​​firstname​​​​And​​​​Or​​​​SELECT … FROM person WHERE firstname = :firstname​

用于将偏移量和排序参数传递给数据库。​​Pageable​

返回 a。选择行以确定是否有更多数据可供使用。不支持自定义。​​Slice<Person>​​​​LIMIT+1​​​​ResultSetExtractor​

运行返回的分页查询。仅选择给定页边界内的数据,并可能选择计数查询以确定总数。不支持自定义。​​Page<Person>​​​​ResultSetExtractor​

查找给定条件的单个实体。 它以非唯一结果完成。​​IncorrectResultSizeDataAccessException​

与 <3> 相反,即使查询生成更多结果文档,也始终发出第一个实体。

该方法显示所有具有给定人员的查询。​​findByLastname​​​​lastname​

该方法返回 a,这使得值在从数据库返回后立即成为可能。​​streamByLastname​​​​Stream​

您可以使用 Spring 表达式语言动态解析参数。在示例中,Spring 安全性用于解析当前用户的用户名。

下表显示了查询方法支持的关键字:

表 3.查询方法支持的关键字

关键词

样本

逻辑结果

​After​

​findByBirthdateAfter(Date date)​

​birthdate > date​

​GreaterThan​

​findByAgeGreaterThan(int age)​

​age > age​

​GreaterThanEqual​

​findByAgeGreaterThanEqual(int age)​

​age >= age​

​Before​

​findByBirthdateBefore(Date date)​

​birthdate < date​

​LessThan​

​findByAgeLessThan(int age)​

​age < age​

​LessThanEqual​

​findByAgeLessThanEqual(int age)​

​age <= age​

​Between​

​findByAgeBetween(int from, int to)​

​age BETWEEN from AND to​

​NotBetween​

​findByAgeNotBetween(int from, int to)​

​age NOT BETWEEN from AND to​

​In​

​findByAgeIn(Collection<Integer> ages)​

​age IN (age1, age2, ageN)​

​NotIn​

​findByAgeNotIn(Collection ages)​

​age NOT IN (age1, age2, ageN)​

​IsNotNull​​​, ​​NotNull​

​findByFirstnameNotNull()​

​firstname IS NOT NULL​

​IsNull​​​, ​​Null​

​findByFirstnameNull()​

​firstname IS NULL​

​Like​​​, , ​​StartingWith​​​​EndingWith​

​findByFirstnameLike(String name)​

​firstname LIKE name​

​NotLike​​​, ​​IsNotLike​

​findByFirstnameNotLike(String name)​

​firstname NOT LIKE name​

​Containing​​在字符串上

​findByFirstnameContaining(String name)​

​firstname LIKE '%' + name + '%'​

​NotContaining​​在字符串上

​findByFirstnameNotContaining(String name)​

​firstname NOT LIKE '%' + name + '%'​

​(No keyword)​

​findByFirstname(String name)​

​firstname = name​

​Not​

​findByFirstnameNot(String name)​

​firstname != name​

​IsTrue​​​, ​​True​

​findByActiveIsTrue()​

​active IS TRUE​

​IsFalse​​​, ​​False​

​findByActiveIsFalse()​

​active IS FALSE​


查询派生仅限于可在 aclause 中使用的属性,而无需使用联接。​​WHERE​

9.7.1. 查询查找策略

JDBC 模块支持手动将查询定义为注释中的字符串或属性文件中的命名查询。​​@Query​

从方法名称派生查询目前仅限于简单属性,这意味着直接存在于聚合根中的属性。 此外,此方法仅支持选择查询。

9.7.2. 使用​​@Query​

下面的示例演示如何使用 to 声明查询方法:​​@Query​

例 54。使用 @Query 声明查询方法

interface UserRepository extends CrudRepository<User, Long> {

@Query("select firstName, lastName from User u where u.emailAddress = :email")
User findByEmailAddress(@Param("email") String email);
}

为了将查询结果转换为实体,默认情况下使用与 Spring Data JDBC 自己生成的查询相同。 您提供的查询必须与预期的格式匹配。 必须提供实体构造函数中使用的所有属性的列。 通过 setter、wither 或字段访问设置的属性的列是可选的。 将不会设置结果中没有匹配列的属性。 查询用于填充聚合根、嵌入实体和一对一关系,包括作为 SQL 数组类型存储和加载的基元类型的数组。 为实体的映射、列表、集和数组生成单独的查询。​​RowMapper​​​​RowMapper​

Spring 完全支持基于 Java 8 的基于编译器标志的参数名称发现。 通过在生成中使用此标志作为调试信息的替代方法,可以省略命名参数的注释。​​-parameters​​​​@Param​

Spring Data JDBC仅支持命名参数。

9.7.3. 命名查询

如果在注释中没有给出任何查询,如上一节 Spring Data 中所述,JDBC 将尝试查找命名查询。 有两种方法可以确定查询的名称。 默认是采用查询的域类,即存储库的聚合根,采用其简单名称并附加用 a 分隔的方法名称。 或者,注释具有一个属性,可用于指定要查找的查询的名称。​​.​​​​@Query​​​​name​

命名查询应在类路径的属性文件中提供。​​META-INF/jdbc-named-queries.properties​

可以通过将值设置为来更改该文件的位置。​​@EnableJdbcRepositories.namedQueriesLocation​

流式处理结果

当您将 Stream 指定为查询方法的返回类型时,Spring Data JDBC 会在元素可用时立即返回它们。 在处理大量数据时,这适用于减少延迟和内存需求。

流包含与数据库的打开连接。 为了避免内存泄漏,最终需要通过关闭流来关闭该连接。 建议的方法是a。 这也意味着,一旦关闭与数据库的连接,流将无法获取更多元素,并可能引发异常。​​try-with-resource clause​

习惯​​RowMapper​

您可以通过注册 abean 和注册 aper 方法返回类型来配置要使用的方法。 以下示例演示如何注册:​​RowMapper​​​​@Query(rowMapperClass = ….)​​​​RowMapperMap​​​​RowMapper​​​​DefaultQueryMappingConfiguration​

@Bean
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration()
.register(Person.class, new PersonRowMapper())
.register(Address.class, new AddressRowMapper());
}

确定方法使用哪个方法时,将根据方法的返回类型执行以下步骤:​​RowMapper​

  1. 如果类型是简单类型,则使用 no。RowMapper

    相反,查询应返回具有单个列的单行,并且对返回类型的转换将应用于该值。
  2. 中的实体类将迭代,直到找到一个是相关返回类型的超类或接口。 使用该类的已注册。QueryMappingConfigurationRowMapper

    迭代按注册顺序进行,因此请确保在特定类型之后注册更常规的类型。

如果适用,包装器类型(如集合)或已解开包装。 因此,返回类型在前面的过程中使用该类型。​​Optional​​​​Optional<Person>​​​​Person​

如果需要,使用自定义或自定义禁用实体回调和生命周期事件作为结果映射可以发出自己的事件/回调。​​RowMapper​​​​QueryMappingConfiguration​​​​@Query(rowMapperClass=…)​​​​ResultSetExtractor​

修改查询

可以使用 theon 查询方法将查询标记为修改查询,如以下示例所示:​​@Modifying​

@Modifying
@Query("UPDATE DUMMYENTITY SET name = :name WHERE id = :id")
boolean updateName(@Param("id") Long id, @Param("name") String name);

您可以指定以下返回类型:

  • ​void​
  • ​int​​(更新的记录计数)
  • ​boolean​​(记录是否已更新)

修改查询直接针对数据库执行。 不会调用任何事件或回调。 因此,如果带有审核批注的字段未在批注查询中更新,则不会更新这些批注。

9.8. 示例查询

9.8.1. 简介

本章介绍按示例查询并说明如何使用它。

按示例查询 (QBE) 是一种用户友好的查询技术,具有简单的界面。 它允许创建动态查询,并且不需要您编写包含字段名称的查询。 事实上,按示例查询根本不要求您使用特定于存储的查询语言编写查询。

9.8.2. 用法

按示例查询 API 由四个部分组成:

  • 探测器:具有填充字段的域对象的实际示例。
  • ​ExampleMatcher​​:包含有关如何匹配特定字段的详细信息。 它可以在多个示例中重复使用。ExampleMatcher
  • ​Example​​:由探头和探头组成。 它用于创建查询。ExampleExampleMatcher
  • ​FetchableFluentQuery​​:提供流畅的 API,允许进一步自定义从 . 使用流畅的 API 可以为查询指定排序投影和结果处理。FetchableFluentQueryExample

按示例查询非常适合多种用例:

  • 使用一组静态或动态约束查询数据存储。
  • 频繁重构域对象,无需担心中断现有查询。
  • 独立于基础数据存储 API 工作。

按示例查询也有几个限制:

  • 不支持嵌套或分组属性约束,例如。firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。

在开始使用按示例查询之前,您需要有一个域对象。 首先,请为存储库创建一个接口,如以下示例所示:

例 55。示例人员对象

public class Person {

@Id
private String id;
private String firstname;
private String lastname;
private Address address;

// … getters and setters omitted
}

前面的示例显示了一个简单的域对象。 您可以使用它来创建. 默认情况下,将忽略具有值的字段,并使用特定于存储的默认值匹配字符串。​​Example​​​​null​

将属性包含在“按示例查询”条件中基于可空性。 始终包含使用基元类型 (,, ...) 的属性,除非ExampleMatcher忽略属性路径​。​​int​​​​double​

可以使用工厂方法或使用ExampleMatcher.is 不可变来构建示例。 下面的清单显示了一个简单的示例:​​of​​​​Example​

例 56。简单示例

Person person = new Person();                         
person.setFirstname("Dave");

Example<Person> example = Example.of(person);

创建域对象的新实例。

设置要查询的属性。

创建。​​Example​

您可以使用存储库运行示例查询。 为此,请让您的存储库界面扩展。 以下清单显示了界面的摘录:​​QueryByExampleExecutor<T>​​​​QueryByExampleExecutor​

例 57。这​​QueryByExampleExecutor​

public interface QueryByExampleExecutor<T> {

<S extends T> S findOne(Example<S> example);

<S extends T> Iterable<S> findAll(Example<S> example);

// … more functionality omitted.
}

9.8.3. 匹配器示例

示例不限于默认设置。 您可以使用 指定自己的字符串匹配、null 处理和特定于属性的设置的默认值,如以下示例所示:​​ExampleMatcher​

例 58。具有自定义匹配的示例匹配器

Person person = new Person();                          
person.setFirstname("Dave");

ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("lastname")
.withIncludeNullValues()
.withStringMatcher(StringMatcher.ENDING);

Example<Person> example = Example.of(person, matcher);

创建域对象的新实例。

设置属性。

创建 anto 期望所有值都匹配。 即使没有进一步的配置,它也可以在此阶段使用。​​ExampleMatcher​

构造一个新忽略属性路径。​​ExampleMatcher​​​​lastname​

构造一个新以忽略属性路径并包含空值。​​ExampleMatcher​​​​lastname​

构造一个 newto 忽略属性路径,以包含 null 值,并执行后缀字符串匹配。​​ExampleMatcher​​​​lastname​

创建一个基于域对象和配置的 new。​​Example​​​​ExampleMatcher​

默认情况下,期望探测器上设置的所有值都匹配。 如果要获取与隐式定义的任何谓词匹配的结果,请使用。​​ExampleMatcher​​​​ExampleMatcher.matchingAny()​

您可以为单个属性指定行为(例如“名字”和“姓氏”,或者对于嵌套属性,可以指定“address.city”)。 您可以使用匹配选项和区分大小写来调整它,如以下示例所示:

例 59。配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", endsWith())
.withMatcher("lastname", startsWith().ignoreCase());
}

配置匹配器选项的另一种方法是使用 lambda(在 Java 8 中引入)。 此方法创建一个回调,要求实现者修改匹配器。 您无需返回匹配器,因为配置选项保存在匹配器实例中。 以下示例显示了使用 lambda 的匹配器:

例 60。使用 lambda 配置匹配器选项

ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("firstname", match -> match.endsWith())
.withMatcher("firstname", match -> match.startsWith());
}

Queries created by use a merged view of the configuration. Default matching settings can be set at the level, while individual settings can be applied to particular property paths. Settings that are set on are inherited by property path settings unless they are defined explicitly. Settings on a property patch have higher precedence than default settings. The following table describes the scope of the various settings:​​Example​​​​ExampleMatcher​​​​ExampleMatcher​​​​ExampleMatcher​

Table 4. Scope of settings​​ExampleMatcher​

Setting

Scope

Null-handling

​ExampleMatcher​

String matching

​ExampleMatcher​​ and property path

Ignoring properties

Property path

区分大小写

​ExampleMatcher​​和属性路径

价值转型

属性路径

9.8.4. 流畅的接口

​QueryByExampleExecutor​​提供了另一种方法,到目前为止我们没有提到: 与其他方法一样,它执行从 . 但是,使用第二个参数,您可以控制该执行的某些方面,否则无法动态控制。 为此,可以在第二个参数中调用各种方法。用于指定结果的排序。用于指定要将结果转换为的类型。限制查询的属性。,,,,,,,,并定义获得的结果类型以及当可用结果数超过预期数时查询的行为方式。​​<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction)​​​​Example​​​​FetchableFluentQuery​​​​sortBy​​​​as​​​​project​​​​first​​​​firstValue​​​​one​​​​oneValue​​​​all​​​​page​​​​stream​​​​count​​​​exists​

例 61。使用流畅的 API 获取可能许多结果中的最后一个,按姓氏排序。

Optional<Person> match = repository.findBy(example,
q -> q
.sortBy(Sort.by("lastname").descending())
.first()
);

9.8.5. 运行示例

在 Spring Data JDBC 中,您可以将 Query by Example 与 Repository 一起使用,如以下示例所示:

例 62。使用存储库按示例查询

public interface PersonRepository
extends CrudRepository<Person, String>,
QueryByExampleExecutor<Person> { … }

public class PersonService {

@Autowired PersonRepository personRepository;

public List<Person> findPeople(Person probe) {
return personRepository.findAll(Example.of(probe));
}
}

目前,只有属性可用于属性匹配。​​SingularAttribute​

属性说明符接受属性名称(如 and)。可以通过将属性与点 () 链接在一起来导航。您还可以使用匹配选项和区分大小写来调整它。​​firstname​​​​lastname​​​​address.city​

下表显示了可以使用的各种选项,以及在名为以下字段上使用它们的结果:​​StringMatcher​​​​firstname​

表 5.选项​​StringMatcher​

匹配

逻辑结果

​DEFAULT​​(区分大小写)

​firstname = ?0​

​DEFAULT​​(不区分大小写)

​LOWER(firstname) = LOWER(?0)​

​EXACT​​(区分大小写)

​firstname = ?0​

​EXACT​​(不区分大小写)

​LOWER(firstname) = LOWER(?0)​

​STARTING​​(区分大小写)

​firstname like ?0 + '%'​

​STARTING​​(不区分大小写)

​LOWER(firstname) like LOWER(?0) + '%'​

​ENDING​​(区分大小写)

​firstname like '%' + ?0​

​ENDING​​(不区分大小写)

​LOWER(firstname) like '%' + LOWER(?0)​

​CONTAINING​​(区分大小写)

​firstname like '%' + ?0 + '%'​

​CONTAINING​​(不区分大小写)

​LOWER(firstname) like '%' + LOWER(?0) + '%'​

9.8.6. 预测

Spring 数据查询方法通常返回由存储库管理的聚合根的一个或多个实例。 但是,有时可能需要基于这些类型的某些属性创建投影。 Spring 数据允许对专用返回类型进行建模,以更有选择性地检索托管聚合的部分视图。

假设存储库和聚合根类型,如以下示例所示:

例 63。示例聚合和存储库

class Person {

@Id UUID id;
String firstname, lastname;
Address address;

static class Address {
String zipCode, city, street;
}
}

interface PersonRepository extends Repository<Person, UUID> {

Collection<Person> findByLastname(String lastname);
}

Now imagine that we want to retrieve the person’s name attributes only. What means does Spring Data offer to achieve this? The rest of this chapter answers that question.

Interface-based Projections

The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:

Example 64. A projection interface to retrieve a subset of attributes

interface NamesOnly {

String getFirstname();
String getLastname();
}

这里重要的一点是,此处定义的属性与聚合根中的属性完全匹配。 这样做可以添加查询方法,如下所示:

例 65。使用基于接口的投影和查询方法的存储库

interface PersonRepository extends Repository<Person, UUID> {

Collection<NamesOnly> findByLastname(String lastname);
}

查询执行引擎在运行时为返回的每个元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。

在 yourThat 中声明方法会覆盖基方法(例如,声明在、特定于存储的存储库接口或 the)会导致对基方法的调用,而不管声明的返回类型如何。请确保使用兼容的返回类型,因为基方法不能用于投影。某些存储模块支持注释,以将重写的基方法转换为查询方法,然后可用于返回投影。​​Repository​​​​CrudRepository​​​​Simple…Repository​​​​@Query​

投影可以递归使用。如果还希望包含某些信息,请为其创建一个投影接口,并从声明中返回该接口,如以下示例所示:​​Address​​​​getAddress()​

例 66。用于检索属性子集的投影接口

interface PersonSummary {

String getFirstname();
String getLastname();
AddressSummary getAddress();

interface AddressSummary {
String getCity();
}
}

在方法调用时,将获取目标实例的属性并依次包装到投影代理中。​​address​

封闭式投影

其访问器方法都与目标聚合的属性匹配的投影接口被视为封闭投影。以下示例(我们在本章前面也使用过)是一个封闭投影:

例 67。封闭投影

interface NamesOnly {

String getFirstname();
String getLastname();
}

如果您使用封闭投影,Spring Data 可以优化查询执行,因为我们知道支持投影代理所需的所有属性。 有关这方面的更多详细信息,请参阅参考文档中特定于模块的部分。

开放投影

投影接口中的访问器方法还可用于通过注释来计算新值,如以下示例所示:​​@Value​

例 68。开放式投影

interface NamesOnly {

@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();

}

支持投影的聚合根在变量中可用。 投影接口使用是开放式投影。 在这种情况下,Spring 数据无法应用查询执行优化,因为 SpEL 表达式可以使用聚合根的任何属性。​​target​​​​@Value​

使用的表达式不应该太复杂 — 您希望避免对变量进行编程。 对于非常简单的表达式,一种选择可能是采用默认方法(Java 8 中引入),如以下示例所示:​​@Value​​​​String​

例 69。使用自定义逻辑的默认方法的投影接口

interface NamesOnly {

String getFirstname();
String getLastname();

default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}

This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface. A second, more flexible, option is to implement the custom logic in a Spring bean and then invoke that from the SpEL expression, as shown in the following example:

Example 70. Sample Person object

@Component
class MyBean {

String getFullName(Person person) {

}
}

interface NamesOnly {

@Value("#{@myBean.getFullName(target)}")
String getFullName();

}

请注意 SpEL 表达式如何引用和调用该方法,并将投影目标作为方法参数转发。 SpEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式中引用这些参数。 方法参数可通过名为的数组获得。下面的示例演示如何从数组中获取方法参数:​​myBean​​​​getFullName(…)​​​​Object​​​​args​​​​args​

例 71。示例人员对象

interface NamesOnly {

@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}

同样,对于更复杂的表达式,您应该使用 Spring Bean 并让表达式调用方法,如前所述。

可为空的包装器

投影接口中的 Getter 可以使用可为空的包装器来提高空安全性。目前支持的包装器类型包括:

  • ​java.util.Optional​
  • ​com.google.common.base.Optional​
  • ​scala.Option​
  • ​io.vavr.control.Option​

例 72。使用可为空包装器的投影接口

interface NamesOnly {

Optional<String> getFirstname();
}

如果基础投影值不是,则使用包装器的当前表示形式返回值。 如果支持值为 ,则 getter 方法返回所用包装类型的空表示形式。​​null​​​​null​

基于类的投影 (DTO)

定义投影的另一种方法是使用值类型 DTO(数据传输对象),用于保存应检索的字段的属性。 这些 DTO 类型的使用方式与投影接口的使用方式完全相同,只是不会发生代理,并且不能应用嵌套投影。

如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名称确定。

以下示例显示了一个投影 DTO:

例 73。一个突出的DTO

class NamesOnly {

private final String firstname, lastname;

NamesOnly(String firstname, String lastname) {

this.firstname = firstname;
this.lastname = lastname;
}

String getFirstname() {
return this.firstname;
}

String getLastname() {
return this.lastname;
}

// equals(…) and hashCode() implementations
}

避免投影 DTO 的样板代码


你可以通过使用ProjectLombok​来大大简化DTO的代码,它提供了anannotation(不要与前面的接口示例中所示的Spring'sannotation混淆)。 如果您使用龙目岛项目的注释,前面显示的示例 DTO 将变为以下内容:​​@Value​​​​@Value​​​​@Value​




@Value
class NamesOnly {
String firstname, lastname;
}




字段是默认的,该类公开一个构造函数,该构造函数采用所有字段并自动获取实现的 sand方法。​​private final​​​​equals(…)​​​​hashCode()​


动态投影

到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。 但是,您可能希望选择要在调用时使用的类型(这使其成为动态的)。 若要应用动态投影,请使用查询方法,如以下示例所示:

例 74。使用动态投影参数的存储库

interface PersonRepository extends Repository<Person, UUID> {

<T> Collection<T> findByLastname(String lastname, Class<T> type);
}

这样,该方法可用于按原样获取聚合或应用投影,如以下示例所示:

例 75。使用具有动态投影的存储库

void someMethod(PersonRepository people) {

Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);

Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}

检查类型的查询参数是否符合动态投影参数的条件。 如果查询的实际返回类型等于参数的泛型参数类型,则匹配参数不可用于查询或 SpEL 表达式。 如果要使用 aparameter 作为查询参数,请确保使用其他泛型参数,例如。​​Class​​​​Class​​​​Class​​​​Class​​​​Class<?>​

9.9. 我的巴蒂斯集成

CRUD操作和查询方法可以委托给MyBatis。 本节介绍如何配置 Spring Data JDBC 以与 MyBatis 集成,以及遵循哪些约定来移交查询的运行以及到库的映射。

9.9.1. 配置

将 MyBatis 正确插入 Spring Data JDBC 的最简单方法是将应用程序配置导入到您的应用程序中:​​MyBatisJdbcConfiguration​

@Configuration
@EnableJdbcRepositories
@Import(MyBatisJdbcConfiguration.class)
class Application {

@Bean
SqlSessionFactoryBean sqlSessionFactoryBean() {
// Configure MyBatis here
}
}

如您所见,您需要声明的只是aas依赖abean最终可用。​​SqlSessionFactoryBean​​​​MyBatisJdbcConfiguration​​​​SqlSession​​​​ApplicationContext​

9.9.2. 使用约定

对于中的每个操作,Spring Data JDBC运行多个语句。 如果应用程序上下文中有一个SqlSessionFactory,Spring Data 会检查每个步骤是否提供语句。 如果找到,则使用该语句(包括其配置的到实体的映射)。​​CrudRepository​​​​SessionFactory​

语句的名称是通过连接实体类型的完全限定名称并确定语句类型来构造的。 例如,如果要插入一个实例,Spring Data JDBC 会查找一个名为的语句。​​Mapper.​​​​String​​​​org.example.User​​​​org.example.UserMapper.insert​

运行语句时,[] 的实例将作为参数传递,这使各种参数可用于语句。​​MyBatisContext​

下表描述了可用的 MyBatis 语句:

名字

目的

可能触发此语句的 CrudRepository 方法

属性在​​MyBatisContext​

​insert​

插入单个图元。这也适用于聚合根引用的实体。

​save​​​, .​​saveAll​

​getInstance​​:要保存的实例

​getDomainType​​:要保存的实体的类型。

​get(<key>)​​​:引用实体的 ID,其中 是 提供的反向引用列的名称。​​<key>​​​​NamingStrategy​

​update​

更新单个实体。这也适用于聚合根引用的实体。

​save​​​, .​​saveAll​

​getInstance​​:要保存的实例

​getDomainType​​:要保存的实体的类型。

​delete​

删除单个实体。

​delete​​​, .​​deleteById​

​getId​​:待删除实例的 ID

​getDomainType​​:要删除的实体的类型。

​deleteAll-<propertyPath>​

删除用作具有给定属性路径的前缀类型的任何聚合根引用的所有实体。 请注意,用于为语句名称添加前缀的类型是聚合根的名称,而不是要删除的实体的名称。

​deleteAll​​.

​getDomainType​​:要删除的实体的类型。

​deleteAll​

删除用作前缀的类型的所有聚合根

​deleteAll​​.

​getDomainType​​:要删除的实体的类型。

​delete-<propertyPath>​

删除具有给定属性路径的聚合根引用的所有实体

​deleteById​​.

​getId​​:要删除其引用实体的聚合根的 ID。

​getDomainType​​:要删除的实体的类型。

​findById​

按 ID 选择聚合根

​findById​​.

​getId​​:要加载的实体的 ID。

​getDomainType​​:要加载的实体的类型。

​findAll​

选择所有聚合根目录

​findAll​​.

​getDomainType​​:要加载的实体的类型。

​findAllById​

按 ID 值选择一组聚合根

​findAllById​​.

​getId​​:要加载的实体的 ID 值列表。

​getDomainType​​:要加载的实体的类型。

​findAllByProperty-<propertyName>​

选择由另一个实体引用的一组实体。引用实体的类型用于前缀。引用的实体类型用作后缀。此方法已弃用。改用​​findAllByPath​

所有方法。如果未为 定义查询​​find*​​​​findAllByPath​

​getId​​:引用要加载的实体的实体的 ID。

​getDomainType​​:要加载的实体的类型。

​findAllByPath-<propertyPath>​

选择一组由另一个实体通过属性路径引用的实体。

所有方法。​​find*​

​getIdentifier​​​:保存聚合根的 id 加上所有路径元素的键和列表索引。​​Identifier​

​getDomainType​​:要加载的实体的类型。

​findAllSorted​

选择所有聚合根,排序

​findAll(Sort)​​.

​getSort​​:排序规范。

​findAllPaged​

选择聚合根目录的页面,可以选择排序

​findAll(Page)​​.

​getPageable​​:分页规范。

​count​

计算用作前缀的类型的聚合根数

​count​

​getDomainType​​:要计数的聚合根的类型。

9.10. 生命周期事件

Spring Data JDBC 触发事件,这些事件发布到应用程序上下文中的任何匹配 bean。 事件和回调仅针对聚合根触发。 如果要处理非根实体,则需要通过包含聚合根的侦听器执行此操作。​​ApplicationListener​

实体生命周期事件的成本可能很高,在加载大型结果集时,您可能会注意到性能配置文件发生了变化。 您可以在模板 API 上禁用生命周期事件。

例如,在保存聚合之前调用以下侦听器:

@Bean
ApplicationListener<BeforeSaveEvent<Object>> loggingSaves() {

return event -> {

Object entity = event.getEntity();
LOG.info("{} is getting saved.", entity);
};
}

如果只想处理特定域类型的事件,则可以从中派生侦听器并覆盖一个或多个方法,其中代表事件类型。 只会为与域类型及其子类型相关的事件调用回调方法,因此不需要进一步强制转换。​​AbstractRelationalEventListener​​​​onXXX​​​​XXX​

class PersonLoadListener extends AbstractRelationalEventListener<Person> {

@Override
protected void onAfterLoad(AfterLoadEvent<Person> personLoad) {
LOG.info(personLoad.getEntity());
}
}

下表描述了可用的事件。有关流程步骤之间确切关系的更多详细信息,请参阅将 1:1 映射到事件的可用回调的说明。

表 6.可用事件

事件

何时发布

BeforeDeleteEvent

在删除聚合根之前。

AfterDeleteEvent

删除聚合根目录后。

BeforeConvertEvent

在聚合根转换为执行 SQL 语句的计划之前,但在决定聚合是否是新的之后,即更新或插入是否有序。 如果要以编程方式设置 id,则这是正确的事件。

BeforeSaveEvent

在保存聚合根之前(即,插入或更新,但在决定是否插入或更新聚合根之后)。不要使用它为新聚合创建 ID。使用甚至更好。​​BeforeConvertEvent​​​​BeforeConvertCallback​

AfterSaveEvent

保存聚合根目录(即插入或更新)后。

AfterLoadEvent

从数据库创建聚合根并设置其所有属性之后。注意:此功能已弃用。改用​​后转换​​ResultSet​

AfterConvertEvent

从数据库创建聚合根并设置其所有属性之后。​​ResultSet​

生命周期事件依赖于 an,在这种情况下可以配置一个,因此在处理事件时不提供任何保证。​​ApplicationEventMulticaster​​​​SimpleApplicationEventMulticaster​​​​TaskExecutor​

9.10.1. 特定于商店的实体回调

Spring Data JDBC使用API作为其审计支持,并对下表中列出的回调做出反应。​​EntityCallback​

表 7.Spring Data JDBC执行的不同进程的流程步骤和回调。

过程

​EntityCallback​​/ 工艺步骤

评论

删除

BeforeDeleteCallback

在实际删除之前。

聚合根和该聚合的所有实体将从数据库中删除。

AfterDeleteCallback

删除聚合后。

确定是否要执行聚合的插入或更新,具体取决于聚合是否是新的。

BeforeConvertCallback

如果要以编程方式设置 id,这是正确的回调。在上一步中,检测到新的聚合,在此步骤中生成的 Id 将在下一步中使用。

将聚合转换为聚合更改,它是要对数据库执行的一系列 SQL 语句。在此步骤中,如果聚合提供了 Id,或者 Id 仍为空并预期由数据库生成,则决定。

BeforeSaveCallback

对聚合根所做的更改可能会被考虑,但是否将 id 值发送到数据库的决定已在上一步中做出。

上面确定的 SQL 语句针对数据库执行。

AfterSaveCallback

保存聚合根目录(即插入或更新)后。

负荷

使用 1 个或多个 SQL 查询加载聚合。从结果集构造聚合。

AfterConvertCallback

我们鼓励对事件使用回调,因为它们支持使用不可变类,因此比事件更强大、更通用。

9.11. 实体回调

Spring 数据基础架构提供了用于在调用某些方法之前和之后修改实体的钩子。 这些所谓的实例提供了一种方便的方法,可以检查并可能以回调样式修改实体。
安看起来很像一个专业的人。 一些 Spring 数据模块发布存储允许修改给定实体的特定事件(例如)。在某些情况下,例如在使用不可变类型时,这些事件可能会导致麻烦。 此外,事件发布依赖于。如果使用异步配置它可能会导致不可预测的结果,因为事件处理可以分叉到线程上。​​EntityCallback​​​​EntityCallback​​​​ApplicationListener​​​​BeforeSaveEvent​​​​ApplicationEventMulticaster​​​​TaskExecutor​

实体回调为集成点提供同步和反应式 API,以保证在处理链中定义良好的检查点按顺序执行,返回可能修改的实体或反应式包装器类型。

实体回调通常按 API 类型分隔。这种分离意味着同步 API 仅考虑同步实体回调,而反应式实现仅考虑反应式实体回调。


实体回调 API 已在 Spring Data Commons 2.2 中引入。这是应用实体修改的推荐方法。 现有存储特定仍会在调用可能注册的实例之前发布。​​ApplicationEvents​​​​EntityCallback​


9.11.1. 实现实体回调

Anis 通过其泛型类型参数与其域类型直接关联。 每个 Spring 数据模块通常附带一组涵盖实体生命周期的预定义接口。​​EntityCallback​​​​EntityCallback​

例 76。剖析​​EntityCallback​

@FunctionalInterface
public interface BeforeSaveCallback<T> extends EntityCallback<T> {

/**
* Entity callback method invoked before a domain object is saved.
* Can return either the same or a modified instance.
*
* @return the domain object to be persisted.
*/
T onBeforeSave(T entity <2>, String collection <3>);
}

​BeforeSaveCallback​​在保存实体之前要调用的特定方法。返回可能已修改的实例。

持久之前的实体。

许多存储特定的参数,例如实体保存到的集合

例 77。反应性解剖​​EntityCallback​

@FunctionalInterface
public interface ReactiveBeforeSaveCallback<T> extends EntityCallback<T> {

/**
* Entity callback method invoked on subscription, before a domain object is saved.
* The returned Publisher can emit either the same or a modified instance.
*
* @return Publisher emitting the domain object to be persisted.
*/
Publisher<T> onBeforeSave(T entity <2>, String collection <3>);
}


​BeforeSaveCallback​​在保存实体之前,要在订阅上调用的特定方法。发出可能经过修改的实例。


持久之前的实体。


许多存储特定的参数,例如实体保存到的集合


可选的实体回调参数由实现 Spring 数据模块定义,并从调用站点推断。​​EntityCallback.callback()​

实现适合您的应用程序需求的接口,如以下示例所示:

例 78。例​​BeforeSaveCallback​

class DefaultingEntityCallback implements BeforeSaveCallback<Person>, Ordered {      

@Override
public Object onBeforeSave(Person entity, String collection) {

if(collection == "user") {
return // ...
}

return // ...
}

@Override
public int getOrder() {
return 100;
}
}

根据您的需求实现回调。

如果存在同一域类型的多个实体回调,则可能会对实体回调进行排序。排序遵循最低优先级。

9.11.2. 注册实体回调

​EntityCallback​​如果 bean 在 中注册,则由商店特定的实现拾取。 大多数模板 API 已经实现,因此可以访问​​ApplicationContext​​​​ApplicationContextAware​​​​ApplicationContext​

以下示例说明了有效实体回调注册的集合:

例 79。示例 Bean 注册​​EntityCallback​

@Order(1)                                                           
@Component
class First implements BeforeSaveCallback<Person> {

@Override
public Person onBeforeSave(Person person) {
return // ...
}
}

@Component
class DefaultingEntityCallback implements BeforeSaveCallback<Person>,
Ordered {

@Override
public Object onBeforeSave(Person entity, String collection) {
// ...
}

@Override
public int getOrder() {
return 100;
}
}

@Configuration
public class EntityCallbackConfiguration {

@Bean
BeforeSaveCallback<Person> unorderedLambdaReceiverCallback() {
return (BeforeSaveCallback<Person>) it -> // ...
}
}

@Component
class UserCallbacks implements BeforeConvertCallback<User>,
BeforeSaveCallback<User> {

@Override
public Person onBeforeConvert(User user) {
return // ...
}

@Override
public Person onBeforeSave(User user) {
return // ...
}
}


​BeforeSaveCallback​​​从注释中接收其命令。​​@Order​


​BeforeSaveCallback​​​通过接口实现接收其订单。​​Ordered​


​BeforeSaveCallback​​​使用 lambda 表达式。默认无序,最后调用。请注意,由 lambda 表达式实现的回调不会公开类型信息,因此使用不可分配的实体调用这些信息会影响回调吞吐量。使用 aorto 为回调 Bean 启用类型筛选。​​class​​​​enum​


在单个实现类中组合多个实体回调接口。

9.12. 自定义转换

Spring Data JDBC允许注册自定义转换器,以影响值在数据库中的映射方式。 目前,转换器仅适用于媒体资源级。

9.12.1. 使用已注册的 Spring 转换器编写属性

以下示例显示了从对象转换为值的 a 的实现:​​Converter​​​​Boolean​​​​String​

@WritingConverter
public class BooleanToStringConverter implements Converter<Boolean, String> {

@Override
public String convert(Boolean source) {
return source != null && source ? "T" : "F";
}
}

这里有几件事需要注意:并且都是简单的类型,因此 Spring Data 需要提示此转换器应应用于哪个方向(读取或写入)。 通过注释此转换器,您可以指示 Spring Data 将每个属性写入数据库。​​Boolean​​​​String​​​​@WritingConverter​​​​Boolean​​​​String​

9.12.2. 使用弹簧转换器读取

以下示例显示了从 ato avalue 转换的 a 的实现:​​Converter​​​​String​​​​Boolean​

@ReadingConverter
public class StringToBooleanConverter implements Converter<String, Boolean> {

@Override
public Boolean convert(String source) {
return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE;
}
}

这里有几件事需要注意:并且都是简单的类型,因此 Spring Data 需要提示此转换器应应用于哪个方向(读取或写入)。 通过注释此转换器,您可以指示 Spring Data 转换应分配给 aproperty 的数据库中的每个值。​​String​​​​Boolean​​​​@ReadingConverter​​​​String​​​​Boolean​

9.12.3. 向​​JdbcConverter​

class MyJdbcConfiguration extends AbstractJdbcConfiguration {

// …

@Override
protected List<?> userConverters() {
return Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter());
}

}

在以前版本的 Spring Data JDBC 中,建议直接覆盖。 这不再是必需的,甚至不再建议这样做,因为该方法汇集了适用于所有数据库的转换、用户注册的转换和用户注册的转换。 如果您从旧版本的 Spring Data JDBC 迁移并覆盖了来自您的转换,则不会注册。​​AbstractJdbcConfiguration.jdbcCustomConversions()​​​​Dialect​​​​AbstractJdbcConfiguration.jdbcCustomConversions()​​​​Dialect​

9.12.4. Jdbc值

值转换用于丰富使用 atype 传播到 JDBC 操作的值。 如果需要指定特定于 JDBC 的类型而不是使用类型派生,请注册自定义写入转换器。 此转换器应将具有值和实际字段的值转换为该值。​​JdbcValue​​​​java.sql.Types​​​​JdbcValue​​​​JDBCType​

下面的 Spring实现示例从 ato 转换为自定义值对象:​​Converter​​​​String​​​​Email​

@ReadingConverter
public class EmailReadConverter implements Converter<String, Email> {

public Email convert(String source) {
return Email.valueOf(source);
}
}

如果您编写的源和目标类型是本机类型,则我们无法确定是否应将其视为读取或写入转换器。 将转换器实例注册为两者可能会导致不需要的结果。 例如,a是模棱两可的,尽管在编写时尝试将allinstance转换为实例可能没有意义。 为了让您强制基础结构仅以一种方式注册转换器,我们提供了要在转换器实现中使用的注释。​​Converter​​​​Converter<String, Long>​​​​String​​​​Long​​​​@ReadingConverter​​​​@WritingConverter​

转换器需要显式注册,因为不会从类路径或容器扫描中选取实例,以避免向转换服务进行不必要的注册以及此类注册产生的副作用。转换器注册为*工具,允许根据源类型和目标类型注册和查询已注册的转换器。​​CustomConversions​

​CustomConversions​​附带一组预定义的转换器注册:

  • JSR-310 转换器,用于类型之间的转换。java.timejava.util.DateString

本地时态类型 (e.g.to) 的默认转换器依赖于系统默认时区设置在这些类型之间进行转换。您可以通过注册自己的转换器来覆盖默认转换器。​​LocalDateTime​​​​java.util.Date​

转换器消歧

通常,我们会检查它们转换的源和目标类型的实现。 根据其中一个类型是否是基础数据访问 API 可以本机处理的类型,我们将转换器实例注册为读取或写入转换器。 以下示例显示了写入和读取转换器(请注意,区别在于限定符的顺序):​​Converter​​​​Converter​

// Write converter as only the target type is one that can be handled natively
class MyConverter implements Converter<Person, String> { … }

// Read converter as only the source type is one that can be handled natively
class MyConverter implements Converter<String, Person> { … }

9.13. 日志记录

Spring Data JDBC自己几乎不做日志记录。 相反,发出 SQL 语句的机制提供日志记录。 因此,如果你想检查运行了哪些SQL语句,请激活Spring的NamedParameterJdbcTemplate或MyBatis的日志记录。​​JdbcTemplate​

9.14. 交易性

默认情况下,实例的方法是事务性的。 对于读取操作,事务配置标志设置为 。 所有其他配置都配置了纯注释,以便应用默认事务配置。 有关详细信息,请参阅SimpleJdbcRepository 的 Javadoc。 如果需要调整存储库中声明的方法之一的事务配置,请在存储库界面中重新声明该方法,如下所示:​​CrudRepository​​​​readOnly​​​​true​​​​@Transactional​

例 80。CRUD 的自定义事务配置

interface UserRepository extends CrudRepository<User, Long> {

@Override
@Transactional(timeout = 10)
List<User> findAll();

// Further query method declarations
}

上述方法会导致该方法以 10 秒的超时运行,并且没有标志。​​findAll()​​​​readOnly​

更改事务行为的另一种方法是使用通常涵盖多个存储库的外观或服务实现。 其目的是为非 CRUD 操作定义事务边界。 下面的示例演示如何创建此类外观:

例 81。使用外观为多个存储库调用定义事务

@Service
public class UserManagementImpl implements UserManagement {

private final UserRepository userRepository;
private final RoleRepository roleRepository;

UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}

@Transactional
public void addRoleToAllUsers(String roleName) {

Role role = roleRepository.findByName(roleName);

for (User user : userRepository.findAll()) {
user.addRole(role);
userRepository.save(user);
}
}

前面的示例导致调用 toto 在事务中运行(参与现有事务或创建新事务(如果尚未运行任何事务)。 存储库的事务配置将被忽略,因为外部事务配置决定了要使用的实际存储库。 请注意,您必须显式激活 use 才能为外观工作获取基于注释的配置。 请注意,前面的示例假定您使用组件扫描。​​addRoleToAllUsers(…)​​​​<tx:annotation-driven />​​​​@EnableTransactionManagement​

9.14.1. 事务查询方法

要让您的查询方法是事务性的,请使用您定义的存储库接口,如以下示例所示:​​@Transactional​

例 82。在查询方法中使用@Transactional

@Transactional(readOnly = true)
interface UserRepository extends CrudRepository<User, Long> {

List<User> findByLastname(String lastname);

@Modifying
@Transactional
@Query("delete from User u where u.active = false")
void deleteInactiveUsers();
}

通常,您希望将标志设置为 true,因为大多数查询方法仅读取数据。 与此相反,使用注释并覆盖事务配置。 因此,该方法是将标志设置为。​​readOnly​​​​deleteInactiveUsers()​​​​@Modifying​​​​readOnly​​​​false​

强烈建议将查询方法设置为事务性查询。这些方法可能会执行多个查询以填充实体。 没有公共事务 Spring Data JDBC 在不同的连接中执行查询。 这可能会给连接池带来过大的压力,当多个方法在保持一个新连接时请求新连接时,甚至可能导致死锁。

通过设置标志来标记只读查询绝对是合理的。 但是,这并不能检查您不会触发操作查询(尽管某些数据库在只读事务中拒绝语句)。 相反,该标志作为对底层 JDBC 驱动程序的提示进行传播,以实现性能优化。​​readOnly​​​​INSERT​​​​UPDATE​​​​readOnly​

9.15. 审计

9.15.1. 基础知识

Spring Data提供了复杂的支持,可以透明地跟踪谁创建或更改了实体以及更改发生的时间。若要从该功能中受益,必须为实体类配备可以使用批注或通过实现接口来定义的审核元数据。 此外,必须通过注释配置或 XML 配置启用审核,以注册所需的基础结构组件。 有关配置示例,请参阅特定于商店的部分。



不需要仅跟踪创建和修改日期的应用程序会使其实体实现​​AuditorAware​​。


基于注释的审核元数据

我们提供捕获创建或修改实体的用户以及何时发生更改的捕获。​​@CreatedBy​​​​@LastModifiedBy​​​​@CreatedDate​​​​@LastModifiedDate​

例 83。被审计的实体

class Customer {

@CreatedBy
private User user;

@CreatedDate
private Instant createdDate;

// … further properties omitted
}

如您所见,注释可以有选择地应用,具体取决于要捕获的信息。 指示在进行更改时捕获的注释可用于 JDK8 日期和时间类型,,,以及旧版 Javaand 的属性。​​long​​​​Long​​​​Date​​​​Calendar​

审核元数据不一定需要位于根级实体中,但可以添加到嵌入的实体中(取决于使用的实际存储),如下面的代码片段所示。

例 84。审核嵌入实体中的元数据

class Customer {

private AuditMetadata auditingMetadata;

// … further properties omitted
}

class AuditMetadata {

@CreatedBy
private User user;

@CreatedDate
private Instant createdDate;

}
基于接口的审核元数据

如果您不想使用注释来定义审核元数据,则可以让您的域类实现接口。它公开所有审核属性的 setter 方法。​​Auditable​

​AuditorAware​

如果使用任一 or,则审计基础结构需要以某种方式了解当前主体。为此,我们提供了 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义批注属性的类型。​​@CreatedBy​​​​@LastModifiedBy​​​​AuditorAware<T>​​​​T​​​​@CreatedBy​​​​@LastModifiedBy​

以下示例显示了使用 Spring 安全性对象的接口的实现:​​Authentication​

例 85。基于弹簧安全性的实现​​AuditorAware​

class SpringSecurityAuditorAware implements AuditorAware<User> {

@Override
public Optional<User> getCurrentAuditor() {

return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}

该实现访问 Spring 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。​​Authentication​​​​UserDetails​​​​UserDetailsService​​​​UserDetails​​​​Authentication​

​ReactiveAuditorAware​

使用响应式基础结构时,您可能希望利用上下文信息来提供信息。 我们提供 anSPI 接口,您必须实现该接口来告诉基础架构当前与应用程序交互的用户或系统是谁。泛型类型定义批注属性的类型。​​@CreatedBy​​​​@LastModifiedBy​​​​ReactiveAuditorAware<T>​​​​T​​​​@CreatedBy​​​​@LastModifiedBy​

以下示例显示了使用反应式 Spring 安全性对象的接口的实现:​​Authentication​

例 86。基于弹簧安全性的实现​​ReactiveAuditorAware​

class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {

@Override
public Mono<User> getCurrentAuditor() {

return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}

该实现访问 Spring 安全性提供的对象,并查找您在实现中创建的自定义实例。我们在这里假设您通过实现公开域用户,但基于发现,您也可以从任何地方查找它。​​Authentication​​​​UserDetails​​​​UserDetailsService​​​​UserDetails​​​​Authentication​

9.16. JDBC 审计

要激活审核,请添加到您的配置中,如以下示例所示:​​@EnableJdbcAuditing​

例 87。使用 Java 配置激活审计

@Configuration
@EnableJdbcAuditing
class Config {

@Bean
AuditorAware<AuditableUser> auditorProvider() {
return new AuditorAwareImpl();
}
}

如果公开 typeto 的 bean,则审核基础结构会自动选取它并使用它来确定要在域类型上设置的当前用户。 如果在中注册了多个实现,则可以通过显式设置属性来选择要使用的一个。​​AuditorAware​​​​ApplicationContext​​​​ApplicationContext​​​​auditorAwareRef​​​​@EnableJdbcAuditing​

9.17. JDBC 锁定

Spring Data JDBC支持锁定派生查询方法。 若要对存储库中的给定派生查询方法启用锁定,请使用 对其进行批注。 type的必需值提供两个值:保证您正在读取的数据不会被修改,并获得一个锁来修改数据。 有些数据库没有做出这种区分。 在这种情况下,这两种模式都是等效的。​​@Lock​​​​LockMode​​​​PESSIMISTIC_READ​​​​PESSIMISTIC_WRITE​​​​PESSIMISTIC_WRITE​

例 88。对派生查询方法使用@Lock

interface UserRepository extends CrudRepository<User, Long> {

@Lock(LockMode.PESSIMISTIC_READ)
List<User> findByLastname(String lastname);
}

正如您在上面看到的,该方法将使用悲观的读锁定执行。如果您将数据库与 MySQL 方言一起使用,这将导致例如以下查询:​​findByLastname(String lastname)​

例 89。生成的 MySQL 方言的 SQL 查询

Select * from user u where u.lastname = lastname LOCK IN SHARE MODE

替代你可以使用。​​LockMode.PESSIMISTIC_READ​​​​LockMode.PESSIMISTIC_WRITE​