SpringData-教程-全-

SpringData 教程(全)

原文:zh.annas-archive.org/md5/28DD94223A475A77126C29F9DB046845

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Spring 框架一直对不同的数据访问技术有很好的支持。然而,有一件事长期保持不变:开发人员必须使用特定于技术的 API 来实现其数据访问层,而且这些 API 通常不是很简洁。这导致了这样一种情况:为了实现期望的结果,人们不得不编写大量样板代码。听起来很熟悉,对吧?

Spring Data 项目诞生是为了解决这些问题。它的目标是为使用 Spring 框架的应用程序提供更简单的创建方式,无论是使用关系数据库还是较新的数据访问技术,如非关系数据库、映射-减少框架或基于云的存储技术。它本质上是一个父项目,将数据存储特定的子项目收集到一个品牌下。Spring Data 项目的所有子项目的完整列表可以从 Spring Data 项目的主页上找到:www.springsource.org/spring-data/

本书集中在两个特定的子项目上:Spring Data JPA 和 Spring Data Redis。您将学习一种更简单的方法来管理实体并使用 Spring Data JPA 创建数据库查询。本书还演示了如何向存储库添加自定义功能。您还将学习如何使用 Redis 键值存储作为数据存储,并利用其其他功能来增强应用程序的性能。

这本实用指南证明了实现 JPA 存储库可以很有趣,并帮助您在应用程序中利用 Redis 的性能。

本书涵盖的内容

第一章《入门》简要介绍了本书中描述的技术。本章分为两部分:第一部分描述了 Java 持久性 API 背后的动机,概述了其主要概念,并展示了如何使用它构建数据库查询。第二部分确定了 Redis 键值存储的关键特性。

第二章《使用 Spring Data JPA 入门》帮助您开始使用 Spring Data JPA 构建应用程序。您将学习如何设置一个使用 Spring Data JPA 的项目,并通过编程配置来配置您的应用程序。您还将学习一种简单的方法来为您的实体创建存储库,并使用 Spring Data JPA 实现一个简单的联系人管理应用程序。

第三章《使用 Spring Data JPA 构建查询》描述了您可以使用的技术来构建数据库查询。阅读本章后,您将了解如何使用查询方法、JPA Criteria API 和 Querydsl 来构建数据库查询。您还将通过向其添加搜索功能来继续实现联系人管理应用程序。

第四章《向 JPA 存储库添加自定义功能》教会您如何自定义存储库。您将学习如何将自定义功能添加到单个存储库或所有存储库。本章讨论的原则是通过自定义联系人管理应用程序的存储库来演示的。

第五章《使用 Spring Data Redis 入门》将指导您完成安装和配置阶段,这是在您的应用程序中使用 Spring Data Redis 之前所必需的。它描述了如何在运行类 Unix 操作系统的计算机上安装 Redis。然后您可以设置一个使用 Spring Data Redis 的项目。在本章的最后部分,您将学习如何配置 Redis 连接并比较支持的连接器库的特性。

第六章,使用 Spring Data Redis 构建应用程序,教您如何在 Spring 应用程序中使用 Redis。它描述了 Spring Data Redis 的关键组件,并教您如何使用它们。当您将 Redis 用作联系人管理应用程序的数据存储时,您还将看到 Spring Data Redis 的实际应用。本章的最后部分描述了如何将 Spring Data Redis 用作 Spring 3.1 缓存抽象的实现。您还将在本章中看到如何利用 Redis 的发布/订阅消息模式实现。

您需要为这本书做些什么

为了运行本书的代码示例,您需要安装以下软件:

  • Java 1.6

  • Maven 3.0.X

  • Redis 2.6.0-rc6

  • 一个网络浏览器

如果您想尝试代码示例,您还需要:

  • 诸如 Eclipse、Netbeans 或 IntelliJ Idea 之类的 IDE

  • 每章的完整源代码包(请参阅下面的下载示例代码部分)

这本书适合谁

这本书非常适合正在使用 Spring 应用程序的开发人员,并且正在寻找一种更容易的方式来编写使用关系数据库的数据访问代码。此外,如果您有兴趣了解如何在应用程序中使用 Redis,那么这本书适合您。这本书假定您已经从 Spring 框架和 Java 持久性 API 中获得了一些经验。不需要来自 Redis 的先前经验。

约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些示例以及它们的含义解释。

文本中的代码单词显示如下:“我们可以通过使用@EnableJpaRepositories注释的repositoryFactoryBeanClass属性来实现这一点。”

代码块设置如下:

@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    return new BaseRepositoryFactory(entityManager);
}

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将以粗体显示:

@CachePut(value = "contacts", key="#p0.id")
@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact update(ContactDTO updated) throws NotFoundException {
    //Implementation remains unchanged.
}

新术语重要单词以粗体显示。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:入门

在本书中,我们将集中讨论两个特定的子项目,它们支持 Java 持久化 API 2.0 和 Redis 键值存储。但在我们深入讨论之前,我们需要对这两种技术进行简要介绍。我们需要这样做有两个原因:

首先,如果我们想真正理解 Spring Data JPA 的好处,我们需要了解在使用标准 API 时如何创建数据库查询。一旦我们将这些代码示例与使用 Spring Data JPA 的查询创建代码进行比较,它的好处就会显露出来。

其次,对 Redis 键值存储的基本了解将有助于我们理解本书的第二部分,其中描述了我们如何在应用程序中使用它。毕竟,我们应该熟悉我们在应用程序中使用的任何技术。对吧?

本章中,我们将涵盖以下主题:

  • Java 持久化 API 背后的动机

  • Java 持久化 API 的主要组件

  • 我们如何使用 Java 持久化 API 创建数据库查询

  • Redis 键值存储支持的数据类型。

  • Redis 键值存储的主要特性。

Java 持久化 API

在引入Java 持久化 APIJPA)之前,我们有以下三种替代技术可用于实现持久化层:

  • 企业 JavaBeanEJB)2.x 规范提供的持久化机制

  • JDBC API

  • 第三方对象关系映射ORM)框架,如 Hibernate。

这给了我们在选择最适合工作的工具时一些自由,但像往常一样,这些选项都不是没有问题的。

EJB 2.x 的问题在于它太过沉重和复杂。它的配置依赖于复杂的 XML 文档,其编程模型需要大量样板代码。此外,EJB 要求应用程序部署到Java EE应用服务器。

针对 JDBC API 的编程相当简单,我们可以在任何 servlet 容器中部署我们的应用程序。但是,当我们将领域模型的信息转换为查询或从查询结果构建领域模型对象时,我们必须编写大量样板代码。

第三方 ORM 框架通常是一个不错的选择,因为它们使我们摆脱了编写不必要的代码,用于构建查询或从查询结果构造领域对象。这种自由是有代价的:对象和关系数据不兼容,即使 ORM 框架可以解决大部分由对象关系不匹配引起的问题,但它们无法高效解决的问题是我们最头疼的问题。

Java 持久化 API 提供了一种标准机制,用于实现使用关系数据库的持久化层。它的主要动机是取代 EJB 2.x 的持久化机制,并为对象关系映射提供了标准化方法。它的许多特性最初是由第三方 ORM 框架引入的,后来成为 Java 持久化 API 的实现。以下部分介绍了其关键概念,并描述了我们如何使用它创建查询。

关键概念

实体是持久化的领域对象。每个实体类通常表示一个数据库表,并且这样的类的实例包含单个表行的数据。每个实体实例始终具有唯一的对象标识符,这对于实体来说就像主键对于数据库表一样。

实体管理器工厂创建实体管理器实例。由同一实体管理器工厂创建的所有实体管理器实例将使用相同的配置和数据库。如果需要访问多个数据库,则必须为每个使用的数据库配置一个实体管理器工厂。实体管理器工厂的方法由EntityManagerFactory接口指定。

实体管理器管理应用程序的实体。实体管理器可用于对实体执行 CRUD(创建、读取、更新和删除)操作,并针对数据库运行复杂查询。实体管理器的方法由EntityManager接口声明。

持久化单元指定了所有实体类,这些类由应用程序的实体管理器管理。每个持久化单元包含表示存储在单个数据库中的数据的所有类。

持久化上下文包含实体实例。在持久化上下文中,每个对象标识符只能有一个实体实例。每个持久化上下文与管理持久化上下文中包含的实体实例的特定实体管理器相关联。

创建数据库查询

Java 持久化 API 引入了两种创建数据库查询的新方法:Java 持久化查询语言JPQL)和标准查询 API。使用这些技术编写的查询不直接处理数据库表,而是针对应用程序的实体及其持久状态编写。这在理论上确保创建的查询是可移植的,不与特定的数据库模式或数据库提供程序绑定。

也可以使用 SQL 查询,但这会将应用程序与特定的数据库模式绑定。如果使用了特定于数据库提供程序的扩展,我们的应用程序也将与数据库提供程序绑定。

接下来,我们将看看如何使用 Java 持久化 API 通过使用 SQL、JPQL 和标准查询 API 构建数据库查询。我们的示例查询将从数据库中获取所有名字为“John”的联系人。这个例子使用了一个简单的实体类Contact,表示了contacts表中存储的数据。以下表将实体的属性映射到数据库的列:

联系人 联系人
firstName first_name

本地 SQL 查询

SQL 是一种标准化的查询语言,旨在管理存储在关系数据库中的数据。以下代码示例描述了如何使用 SQL 实现指定的查询:

//Obtain an instance of the entity manager
EntityManager em = ...

//Build the SQL query string with a query parameter
String getByFirstName="SELECT * FROM contacts c WHERE c.first_name = ?1";

//Create the Query instance
Query query = em.createNativeQuery(getByFirstName, Contact.class);

//Set the value of the query parameter
query.setParameter(1, "John");

//Get the list of results
List contacts = query.getResultList();

这个例子教会我们三件事:

  • 我们不必学习新的查询语言来构建 JPA 查询。

  • 创建的查询不是类型安全的,我们必须在使用之前对结果进行转换。

  • 我们必须在验证查询的拼写或语法错误之前运行应用程序。这增加了开发人员反馈循环的长度,降低了生产率。

因为 SQL 查询与特定的数据库模式(或使用的数据库提供程序)绑定,所以只有在绝对必要时才应使用它们。通常使用 SQL 查询的原因是性能,但我们可能还有其他使用它的原因。例如,我们可能正在将传统应用程序迁移到 JPA,而一开始没有时间做得很好。

Java 持久化查询语言

JPQL 是一种基于字符串的查询语言,其语法类似于 SQL。因此,只要您具有一些 SQL 经验,学习 JPQL 就相当容易。执行指定查询的代码示例如下:

//Obtain an instance of the entity manager
EntityManager em = ...

//Build the JPQL query string with named parameter
String getByFirstName="SELECT c FROM Contact c WHERE c.firstName = :firstName";

//Create the Query instance
TypedQuery<Contact> query = em.createQuery(getByFirstName, Contact.class);

//Set the value of the named parameter
query.setParameter("firstName", "John");

//Get the list of results
List<Contact> contacts = query.getResultList();

这个例子告诉我们三件事:

  • 创建的查询是类型安全的,我们不必对查询结果进行转换。

  • JPQL 查询字符串非常易读且易于解释。

  • 创建的查询字符串无法在编译期间进行验证。验证查询字符串的拼写或语法错误的唯一方法是运行我们的应用程序。不幸的是,这意味着开发人员反馈循环的长度增加,从而降低了生产率。

JPQL 是静态查询的不错选择。换句话说,如果查询参数的数量始终相同,JPQL 应该是我们的首选。但是,使用 JPQL 实现动态查询通常很麻烦,因为我们必须手动构建查询字符串。

标准查询 API

Criteria API 是为了解决在使用 JPQL 时发现的问题并标准化第三方 ORM 框架的标准化努力而引入的。它用于构建查询定义对象,这些对象被转换为执行的 SQL 查询。下面的代码示例演示了我们可以通过使用 Criteria API 来实现我们的查询:

//Obtain an instance of entity manager
EntityManager em = ...
//Get criteria builder
CriteriaBuilder cb = em.getCriteriaBuilder();

//Create criteria query
CriteriaQuery<Contact> query = cb.greateQuery(Contact.class);

//Create query root
Root<Contact> root = query.from(Contact.class);

//Create condition for the first name by using static meta
//model. You can also use "firstName" here.
Predicate firstNameIs = cb.equal(root.get(Contact_.firstName, "John");

//Specify the where condition of query
query.where(firstNameIs);

//Create typed query and get results
TypedQuery<Contact> q = em.createQuery(query);
List<Contact> contacts = q.getResultList();

我们可以从这个例子中看到三件事:

  • 创建的查询是类型安全的,可以在不进行强制转换的情况下获得结果

  • 代码不像使用 SQL 或 JPQL 的相应代码那样可读

  • 由于我们正在处理 Java API,Java 编译器确保不可能创建语法不正确的查询

如果我们必须创建动态查询,Criteria API 是一个很好的工具。创建动态查询更容易,因为我们可以处理对象而不是手动构建查询字符串。不幸的是,当创建的查询复杂性增加时,创建查询定义对象可能会很麻烦,代码变得更难理解。

Redis

Redis 是一个将整个数据集保存在内存中并仅将磁盘空间用作辅助持久存储的内存数据存储。因此,Redis 可以提供非常快速的读写操作。问题在于 Redis 数据集的大小不能超过内存量。Redis 的其他特性包括:

  • 支持复杂数据类型

  • 多种持久化机制

  • 主从复制

  • 实现发布/订阅消息模式

这些特性在以下小节中描述。

支持的数据类型

Redis 存储的每个值都有一个键。键和值都是二进制安全的,这意味着键或存储的值可以是字符串或二进制文件的内容。然而,Redis 不仅仅是一个简单的键值存储。它支持多种二进制安全的数据类型,这对每个程序员来说应该是熟悉的。这些数据类型如下:

  • 字符串:这是一种数据类型,其中一个键始终指向单个值。

  • 列表:这是一种数据类型,其中一个键引用多个字符串值,这些值按插入顺序排序。

  • 集合:这是一个无序字符串的集合,不能包含相同的值超过一次。

  • 有序集合:这类似于一个集合,但它的每个值都有一个分数,用于将有序集合的值从最低分数到最高分数排序。相同的分数可以分配给多个值。

  • 哈希:这是一种数据类型,其中一个哈希键始终指向特定的字符串键和值的映射。

持久化

Redis 支持两种持久化机制,可用于将数据集存储在磁盘上。它们如下:

  • RDB 是 Redis 最简单的持久化机制。它在配置的间隔时间内从内存数据集中获取快照,并将快照存储在磁盘上。服务器启动时,它将从快照文件中读取数据集到内存中。这是 Redis 的默认持久化机制。

RDB 最大化了 Redis 服务器的性能,其文件格式非常紧凑,这使得它成为灾难恢复的非常有用的工具。此外,如果你想使用主从复制,你必须使用 RDB,因为在主从之间同步数据时会使用 RDB 快照。

然而,如果你必须在所有情况下最小化数据丢失的机会,RDB 不是适合你的解决方案。因为 RDB 在配置的间隔时间内持久化数据,你总是可以在最后一个快照保存到磁盘后丢失存储在 Redis 实例中的数据。

  • 追加模式文件AOF)是一种持久化模型,它将改变内存数据集状态的每个操作记录到特定的日志文件中。当 Redis 实例启动时,它将通过执行从日志文件中找到的所有操作来重建数据集。

AOF 的优势在于它最大程度地减少了在所有情况下的数据丢失的机会。此外,由于日志文件是追加日志,它不会被不可逆地损坏。另一方面,与相同数据相比,AOF 日志文件通常比 RDB 文件大,并且如果服务器正在经历大量写入负载,AOF 可能比 RDB 慢。

您还可以启用两种持久性机制,并兼得两全。您可以使用 RDB 来创建数据集的备份,并确保数据的安全。在这种情况下,Redis 将使用 AOF 日志文件在服务器启动时构建数据集,因为它很可能包含最新的数据。

如果您将 Redis 用作临时数据存储并且不需要持久性,您可以禁用两种持久性机制。这意味着当服务器关闭时,数据集将被销毁。

复制

Redis 支持主从复制,其中单个主机可以有一个或多个从机。每个从机都是其主机的精确副本,并且可以连接到主机和其他从机。换句话说,从机可以是其他从机的主机。自 Redis 2.6 以来,每个从机默认为只读,并且拒绝对从机的所有写操作。如果我们需要将临时信息存储到从机,我们必须配置该从机以允许写操作。

复制在双方都是非阻塞的。即使从机或从机在第一次同步数据时,也不会阻塞对主机的查询。从机可以配置为在同步数据时提供旧数据。然而,当旧数据被新数据替换时,对从机的传入连接将被短暂地阻塞。

如果从机与主机失去连接,它将继续提供旧数据或向客户端返回错误,这取决于其配置。当主机和从机之间的连接丢失时,从机将自动重新打开连接并向主机发送同步请求。

发布/订阅消息模式

发布/订阅消息模式是一种消息模式,其中消息发送者(发布者)不直接向接收者(订阅者)发送消息。相反,使用一个名为通道的附加元素来传输从发布者到订阅者的消息。发布者可以向一个或多个通道发送消息。订阅者可以选择感兴趣的通道,并通过订阅这些通道来接收发送到这些通道的消息。

让我们想象一个情况,一个单一的发布者正在向两个通道发布消息,通道 1 和通道 2。通道 1 有两个订阅者:订阅者 1 和订阅者 2。通道 2 也有两个订阅者:订阅者 2 和订阅者 3。这种情况在下图中有所说明:

发布/订阅消息模式

发布/订阅模式确保发布者不知道订阅者,反之亦然。这使我们有可能将应用程序分成更小的模块,它们之间的耦合度较低。这使得模块更容易维护和替换。

然而,发布/订阅模式的最大优势也是它的最大弱点。首先,我们的应用程序不能依赖于特定组件已订阅特定通道的事实。其次,我们没有办法清楚地验证是否是这种情况。事实上,我们的应用程序不能假设有人在听。

Redis 为发布/订阅模式提供了坚实的支持。其发布/订阅实现的主要特点包括:

  • 发布者可以同时向一个或多个通道发布消息

  • 订阅者可以通过使用通道的名称或包含通配符的模式订阅感兴趣的通道

  • 取消订阅通道也支持名称和模式匹配

总结

在本章中,我们已经了解到:

  • Java 持久化 API 被引入是为了解决与 EJB 2.x 相关的问题,并提供对象关系映射的标准方法。它的特性是从最流行的第三方持久化框架的特性中选择的。

  • Redis 是一个内存数据存储,它将整个数据集保留在内存中,支持复杂数据类型,可以使用磁盘作为持久存储,并支持主从复制。它还实现了发布/订阅消息模式。

在下一章中,我们将学习如何设置一个使用 Spring Data JPA 的 Web 应用程序项目,并使用它来实现一个简单的联系人管理应用程序。

第二章:开始使用 Spring Data JPA

本章为我们提供了设置 Web 应用程序项目并使用 Spring Data JPA 管理实体所需的基本知识。在本章的过程中,我们将学习:

  • 如何使用Maven下载所需的依赖项

  • 如何使用编程配置配置 Spring 应用程序上下文

  • 如何配置我们的 Web 应用程序以通过编程方式配置(而不使用web.xml)加载 Spring 应用程序上下文

  • 如何使用 Spring Data JPA 为实体类实现CRUD创建读取更新删除)功能

使用 Maven 下载依赖项

本书涵盖了 Spring Data JPA 的 1.2.0.RELEASE 版本,这是在撰写本书时可用的最新版本。Spring Data JPA 所需的其他组件在以下表中描述:

组件 描述 版本
数据源 BoneCP 是一个快速连接池库,用作我们应用程序的数据源。 0.7.1.RELEASE
JPA 提供程序 JPA 提供程序是实现 Java 持久化 API 的库。我们将使用 Hibernate 作为 JPA 提供程序。 4.1.4.Final
Spring 框架 Spring 框架用于开发现代企业应用程序的 Java。 3.1.2.RELEASE
数据库 H2 是一个支持标准 SQL 和 JDBC API 的内存中嵌入式数据库。 1.3.166

我们将使用我们应用程序的其他依赖项的最新可用版本。

我们可以通过在POM文件中声明它们来使用 Maven 下载所需的依赖项。为此,我们必须将以下依赖项声明添加到pom.xml文件的依赖项部分:

<!-- Spring Data JPA -->
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-jpa</artifactId>
  <version>1.2.0.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>4.1.4.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-entitymanager</artifactId>
  <version>4.1.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.3.166</version>
</dependency>
<!-- BoneCP -->
<dependency>
  <groupId>com.jolbox</groupId>
  <artifactId>bonecp</artifactId>
  <version>0.7.1.RELEASE</version>
</dependency>

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册以直接通过电子邮件接收文件。

配置 Spring 应用程序上下文

传统上,我们会使用 XML 配置文件进行声明式配置,但在 Spring Framework 3.0 发布后,可以使用编程配置来配置 Spring 应用程序上下文。这是我们在配置应用程序上下文时的首选方法。

我们可以通过以下步骤配置 Spring 应用程序上下文:

  1. 为配置参数的值创建一个属性文件。

  2. 创建应用程序上下文配置类。

创建属性文件

配置参数的实际值存储在一个名为application.properties的属性文件中。该文件包含数据库连接详细信息、Hibernate 配置以及我们实体的基本包。该文件的内容如下:

#Database Configuration
db.driver=org.h2.Driver
db.url=jdbc:h2:mem:datajpa
db.username=sa
db.password=

#Hibernate Configuration
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.format_sql=true
hibernate.hbm2ddl.auto=create-drop
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.show_sql=true

#EntityManager
entitymanager.packages.to.scan=com.packtpub.springdata.jpa.model

#General Spring configuration is added here.

创建应用程序上下文配置类

我们可以通过以下步骤创建应用程序上下文配置类:

  1. 创建一个包含应用程序的一般配置的应用程序上下文配置骨架。

  2. 配置数据源 bean。

  3. 配置实体管理器工厂 bean。

  4. 配置事务管理器 bean。

创建应用程序上下文配置骨架

创建 Spring 应用程序上下文配置类的骨架配置类所需的步骤如下:

  1. @Configuration注解将该类标识为应用程序上下文配置类。

  2. 组件扫描指令使用@ComponentScan注解进行配置。在我们的示例中,Spring IoC 容器配置为扫描包含我们控制器和服务类的包。

  3. @EnableJpaRepositories注解用于启用 Spring Data JPA 并配置我们的存储库的基本包。

  4. @EnableTransactionManagement注解启用了 Spring Framework 的基于注解的事务管理。

  5. @EnableWebMcv注解导入了 Spring MVC 的默认配置。

  6. 包含配置参数值的属性文件是通过使用@PropertySource注解导入的。我们可以通过使用 Spring IoC 容器注入的Environment接口的实现来访问存储在此文件中的属性值。

我们的应用程序上下文配置骨架的源代码如下所示:

@Configuration
@ComponentScan(basePackages = {
        "com.packtpub.springdata.jpa.controller",
        "com.packtpub.springdata.jpa.service"
})
@EnableJpaRepositories("com.packtpub.springdata.jpa.repository")
@EnableTransactionManagement
@EnableWebMvc
@PropertySource("classpath:application.properties")
public class ApplicationContext extends WebMvcConfigurerAdapter {

    @Resource
    private Environment env;

//Add configuration here
}

注意

我们还可以通过使用 XML 配置 Spring Data JPA。我们可以通过向应用程序上下文配置文件添加 Spring Data JPA 的repositories命名空间元素来实现这一点。

配置数据源 bean

我们将通过向ApplicationContext类添加一个名为dataSource()的方法并用@Bean注解对该方法进行注解来开始配置数据源 bean。该方法的实现如下:

  1. 创建BoneCPDataSource类的实例。

  2. 设置数据库连接详细信息。

  3. 返回创建的对象。

数据源 bean 的配置如下所示:

@Bean
public DataSource dataSource() {
  BoneCPDataSource ds = new BoneCPDataSource();   

  ds.setDriverClass(env.getRequiredProperty("db.driver")); 
  ds.setJdbcUrl(env.getRequiredProperty("db.url")); 
  ds.setUsername(env.getRequiredProperty("db.username"));  
  ds.setPassword(env.getRequiredProperty("db.password"));

  return ds;
}

配置实体管理器工厂 bean

我们可以通过向ApplicationContext类添加一个名为entityManagerFactory()的方法并用@Bean注解对该方法进行注解来配置实体管理器工厂 bean。该方法的实现如下:

  1. 创建LocalContainerEntityManagerFactoryBean类的实例。

  2. 将所使用的数据源 bean 的引用传递给创建的对象。

  3. 将 Hibernate 的默认配置设置为实体管理器工厂 bean。我们可以通过创建一个新的HibernateJpaVendorAdapter对象并将其传递给实体管理器工厂 bean 来实现这一点。

  4. 设置我们实体的基本包。

  5. 设置从我们的属性文件中获取的附加配置。

  6. 返回创建的对象。

创建方法的源代码如下所示:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

    em.setDataSource(dataSource());
    em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());em.setPackagesToScan(env.getRequiredProperty("entitymanager.packages.to.scan"));

    Properties p = new Properties();
    p.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
    p.put("hibernate.format_sql", env.getRequiredProperty("hibernate.format_sql"));
    p.put("hibernate.hbm2ddl.auto", env.getRequiredProperty("hibernate.hbm2ddl.auto"));
    p.put("hibernate.ejb.naming_strategy", env.getRequiredProperty("hibernate.ejb.naming_strategy"));
    p.put("hibernate.show_sql", env.getRequiredProperty("hibernate.show_sql");
    em.setJpaProperties(p);

    return em;
}

配置事务管理器 bean

我们可以通过向ApplicationContext类添加一个名为transactionManager()的方法并用@Bean注解对该方法进行注解来配置事务管理器 bean。该方法的实现如下:

  1. 创建一个新的JpaTransactionManager对象。

  2. 设置所使用的实体管理器工厂的引用。

  3. 返回创建的对象。

事务管理器 bean 配置的源代码如下所示:

@Bean
public JpaTransactionManager transactionManager() {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
    return transactionManager;
}

加载应用程序上下文配置

加载我们应用程序的应用程序上下文配置的旧方法是使用更常见的web.xml文件,也就是更常见的web 应用程序部署描述符文件。然而,因为我们在 Servlet 3.0 环境中使用 Spring Framework 3.1,我们可以通过实现WebApplicationInitializer接口来创建一个 Web 应用程序配置类。这样可以确保 Spring Framework 在启动 Servlet 容器时自动检测到我们的配置类。

我们将使用我们的 Web 应用程序配置类来:

  1. 加载我们的应用程序上下文配置类。

  2. 配置调度程序 servlet

  3. 创建上下文加载程序监听器并将其添加到我们的servlet 上下文中。

我们的配置类的源代码如下所示:

public class DataJPAExampleInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //Loading application context
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(ApplicationContext.class);

        //Dispatcher servlet
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");

        //Context loader listener
        servletContext.addListener(new ContextLoaderListener(rootContext));
    }
}

为实体实现 CRUD 功能

我们现在已经配置了 Spring 应用程序上下文并配置了我们的 Web 应用程序在启动时加载它。我们现在将为一个简单的实体实现 CRUD 功能。我们的示例应用程序用于查看和管理联系信息,我们可以通过以下步骤来实现它:

  1. 创建领域模型。

  2. 为实体创建一个存储库。

  3. 实现 CRUD 功能。

注意

本章仅描述了我们应用程序中理解 Spring Data JPA 工作所需的部分。

领域模型

我们的应用程序的领域模型由两个类组成:ContactAddress。本小节将涉及以下事项:

  • 每个类的信息内容

  • 我们如何使用建造者模式创建新对象(参见:Effective Java第二版),Joshua BlochAddison-Wesley

  • 我们如何更新对象的信息

联系人

Contact类是我们领域模型中唯一的实体,它包含单个联系人的信息。这些信息主要由简单的属性组成。唯一的例外是Address类,用于存储地址信息。Contact类源代码的相关部分如下所示:

@Entity
@Table(name = "contacts")
public class Contact {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private Address address;

    @Column(name = "email_address", length = 100)
    private String emailAddress;

    @Column(name = "first_name", nullable=false, length = 50)
    private String firstName;

    @Column(name = "last_name", nullable=false, length = 100)
    private String lastName;

    @Column(name = "phone_number", length = 30)
    private String phoneNumber;

    @Version
    private long version;

//Getters and other methods
}

让我们继续找出如何创建新联系人并更新联系人信息。

创建新的联系人对象

我们将使用建造者模式来创建新的联系人。为了做到这一点,我们必须按照以下步骤进行:

  1. 实现一个用于构建新Contact对象的静态内部类。

  2. Contact类中添加一个静态的getBuilder()方法。此方法用于获取对所使用的构建器的引用。

我们将首先通过以下步骤向Contact类添加一个静态内部类:

  1. 实现一个以所需属性作为参数的构造函数。联系人的必需属性是名字和姓氏。

  2. 为可选属性实现属性方法。这些属性包括电子邮件地址、电话号码和地址信息。每个属性方法都返回对所使用的构建器对象的引用。

  3. 实现一个build()方法,返回构建的对象。

Contact.Builder类的源代码如下所示:

public static class Builder {

        private Contact built;

        public Builder (String firstName, String lastName) {
            built = new Contact();
            built.firstName = firstName;
            built.lastName = lastName;
        }

        public Builder address(String streetAddress, String postCode, String postOffice, String state, String country) {
            Address address = Address.getBuilder(streetAddress, postCode, postOffice)
                    .state(state)
                    .country(country)
                    .build();
            built.address = address;
            return this;
        }

        public Builder emailAddress(String emailAddress) {
            built.emailAddress = emailAddress;
            return this;
        }

        public Builder phoneNumber(String phoneNumber) {
            built.phoneNumber = phoneNumber;
            return this;
        }

        public Contact build() {
            return built;
        }
    }
}

我们还必须在Contact类中添加一个静态的getBuilder()方法。我们的实现非常简单。我们创建一个新的Contact.Builder对象并返回创建的对象。此方法的源代码如下所示:

public static Builder getBuilder(String firstName, String lastName) {
    return new Builder(firstName, lastName);
}

更新联系人信息

Contact类有两个方法可用于更新联系人信息:update()方法用于更新联系人信息,updateAddress()方法用于更新联系人的地址信息。这些方法的源代码如下所示:

public void update(final String firstName, final String lastName, final String emailAddress, final String phoneNumber) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.emailAddress = emailAddress;
    this.phoneNumber = phoneNumber;
}

public void updateAddress(final String streetAddress, final String postCode, final String postOffice, final String state, final String country) {
    if (address == null) {
        address = new Address();
    }
    address.update(streetAddress, postCode, postOffice, state, country);
}

地址

Address类是一个嵌入类,用于存储地址信息。嵌入类是一个只能与其父类一起持久化的类。嵌入类通常用于呈现领域模型的常见概念,并强调其面向对象的特性。Address类的源代码如下所示:

@Embeddable
public class Address {

    @Column(name = "country", length = 20)
    private String country;

    @Column(name = "street_address", length =150)
    private String streetAddress;

    @Column(name = "post_code", length = 10)
    private String postCode;

    @Column(name = "post_office", length = 40)
    private String postOffice;

    @Column(name = "state", length = 20)
    private String state;

  //The default constructor and other methods
}

接下来,我们将找出如何创建新的Address对象并更新现有对象的地址信息。

创建新的地址

我们将通过建造者模式创建新的Address对象。我们可以通过以下步骤实现建造者模式:

  1. 实现一个用于构建新Address对象的静态内部类。

  2. Address类中添加一个静态的getBuilder()方法。此方法用于获取对所使用的构建器的引用。

我们可以通过以下步骤实现静态内部类:

  1. Address类实现一个以所需属性作为参数的构造函数。Address类的必需属性是streetAddresspostCodepostOffice

  2. 实现用于设置可选地址信息的属性方法。这些信息包括州和国家。每个属性方法都返回对所使用的构建器的引用。

  3. 实现一个build()方法,返回构建的对象。

Address.Builder类的源代码如下所示:

public static class Builder {

  private Address built;

  public Builder(String streetAddress, String postCode, String postOffice) {
    built = new Address();
    built.streetAddress = streetAddress;
    built.postCode = postCode;
    built.postOffice = postOffice;
  }

  public Builder country(String country) {
    built.country = country;
    return this;
  }

  public Builder state(String state) {
    built.state = state;
    return this;
  }

  public Address build() {
    return built;
   }
}

我们还必须实现一个方法,用于获取对所使用的构建器对象的引用。我们可以通过简单地创建一个新的Address.Builder对象并返回创建的对象来实现这一点。Address类的静态getBuilder()方法的源代码如下所示:

public static Builder getBuilder(String streetAddress, String postCode, String postOffice) {
    return new Builder(streetAddress, postCode, postOffice);
}

更新地址信息

我们可以通过调用其update()方法来更新Address对象的信息。该方法的源代码如下:

public void update(final String streetAddress, final String postCode, final String postOffice, final String state, final String country) {
    this.streetAddress = streetAddress;
    this.postCode = postCode;
    this.postOffice = postOffice;
    this.state = state;
    this.country = country;
}

创建一个自定义存储库

为了真正理解 Spring Data JPA 的简单性,我们必须回顾一下不太久远的过去,了解在 Spring Data JPA 发布之前如何创建具体存储库。这应该让我们清楚地了解 Spring Data JPA 的好处。

以老式方式创建自定义存储库

传统上,创建具体存储库是一个包括六个步骤的过程。它们如下:

  1. 创建一个为其子类提供属性映射的基类。通常用于为我们的实体提供 ID、版本和时间戳映射。

  2. 创建一个声明所有存储库共享方法的通用存储库接口。通常,这些方法为我们的实体提供 CRUD 操作。

  3. 创建一个通用存储库。

  4. 创建一个实体类。

  5. 创建一个特定实体的存储库接口。

  6. 创建一个特定实体的具体存储库。

首先,我们必须创建一个抽象基类,每个实体类都要扩展这个基类。我们可以通过以下步骤创建这个类:

  1. 创建一个抽象类,将实体 ID 类型作为类型参数。

  2. @MappedSuperclass注解注释创建的类。它用于说明从这个类中找到的映射应用于它的子类。

  3. 创建一个抽象的getId()方法,返回具体类的 ID。

BaseEntity类的源代码如下:

@MappedSuperclass
public abstract class BaseEntity<ID> {

    @Version
    private Long version;

    public abstract ID getId();
}

其次,我们必须创建一个声明所有具体存储库共享方法的通用存储库接口。我们可以通过以下步骤创建这个接口:

  1. 添加实体类型和实体 ID 类型作为类型参数。

  2. 声明所有具体存储库共享的方法。

BaseRepository接口的源代码如下:

public interface BaseRepository<T extends BaseEntity, ID extends Serializable> {

    public T deleteById(ID id);
    public List<T> findAll();
    public T findById(ID id);
    public void persist(T entity);
}

第三,我们必须创建一个抽象的通用存储库。我们可以通过以下步骤实现这一点:

  1. 创建一个抽象类,将具体实体的类型和实体 ID 类型作为类型参数。

  2. 使用@PersistenceContext注解获取对使用的实体管理器的引用。

  3. 实现BaseRepository接口。

  4. 实现一个构造函数,从类型参数中获取实体类的类型。

  5. 提供一个getEntityManager()方法,返回一个用于引用的实体管理器。这个类的子类将使用这个方法来获取用于构建数据库查询的实体管理器引用。

  6. 提供一个getEntityClass()方法,返回实体的类型。子类使用这个方法来通过使用 Criteria API 构建数据库查询。

BaseRepositoryImpl类的源代码如下:

public abstract class BaseRepositoryImpl<T extends BaseEntity, ID extends Serializable> implements BaseRepository<T, ID> {

    private Class<T> entityClass;

    @PersistenceContext(unitName = "pu")
    private EntityManager em;

    public BaseDAOImpl() {
        this.entityClass = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public T deleteById(ID id) {
        T entity = findById(id);
        if (entity != null) {
            em.remove(entity);
        }
        return entity;
    }

    @Override
    public List<T> findAll() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<T> query = cb.createQuery(entityClass);
        Root<T> root = query.from(entityClass);
        return em.createQuery(query).getResultList();
    }

    @Override
    public T findById(ID id) {
        return em.find(getEntityClass(), id);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void persist(T entity) {
        em.persist(entity);
    }

    protected Class<T> getEntityClass() {
        return entityClass;
    }

    protected EntityManager getEntityManager() {
        return em;
    }
}

接下来,我们必须创建一个实体类。我们可以通过以下步骤创建这个类:

  1. 扩展BaseEntity类,并将实体 ID 类型作为类型参数。

  2. 实现getId()方法,返回实体的 ID。

Contact类的源代码如下:

@Entity
@Table(name = "contacts")
public class Contact extends BaseEntity<Long> {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO) private Long id;

    @Override
    public Long getId() {
        return id;
    }
}

接下来,我们必须为我们的实体特定存储库创建一个接口。我们可以通过扩展BaseRepository接口并提供实体类型和其 ID 类型作为类型参数来实现这一点。ContactRepository接口的源代码如下:

public interface ContactRepository extends BaseRepository<Contact, Long> {
//Declare custom methods here.
}

接下来,我们必须创建特定实体的具体存储库。我们可以通过以下步骤创建一个具体的存储库:

  1. @Repository注解注释具体的存储库类,将创建的类标识为存储库类。

  2. 扩展BaseRepositoryImpl类,并将实体类型和实体 ID 类型作为类型参数。

  3. 实现ContactRepository接口。

ContactRepositoryImpl类的源代码如下:

@Repository
public class ContactRepositoryImpl extends BaseRepositoryImpl<Contact, Long> implements ContactRepository {
  //Add custom query methods here
}

恭喜!我们现在以传统方式创建了一个具体的存储库。我们的存储库实现结构如下图所示:

以传统方式创建自定义存储库

正如我们注意到的,实现具体存储库是一个复杂的过程,需要花费大量时间,而我们本可以用来实际生产。幸运的是,这不是为我们的实体创建存储库的唯一方法。接下来我们将学习一种更简单、更容易的创建自定义存储库的方法。当然,我们说的是 Spring Data JPA。

使用 Spring Data JPA 创建自定义存储库

Spring Data JPA 能够从特殊的存储库接口自动创建具体的存储库实现。这种能力简化了自定义存储库的创建过程。

我们可以通过创建一个接口来为实体创建一个 JPA 存储库,该接口扩展了JpaRepository接口。当我们扩展JpaRepository接口时,我们必须提供两个类型参数:实体的类型和实体的对象标识符的类型。

在我们的情况下,我们需要为Contact实体创建一个存储库。其对象标识符的类型是Long。因此,ContactRepository接口的源代码应该如下所示:

public interface ContactRepository extends JpaRepository<Contact, Long> {
}

就是这样。我们现在已经为Contact实体创建了一个存储库。我们的存储库实现结构如下图所示:

使用 Spring Data JPA 创建自定义存储库

正如我们所看到的,创建自定义存储库的过程不可能比这更简单了。Spring Data JPA 减少了我们需要编写和维护的代码量,以及编写所需的时间。换句话说,我们可以通过更简单的代码在更短的时间内获得相同的功能。这个优势相当难以超越。

通过扩展JpaRepository接口,我们现在已经获得了实现 CRUD 应用程序所需的四种方法。这些方法在下表中描述:

方法 描述
Void delete(Contact contact) 从数据库中删除单个联系人。
Contact findOne(Long id) 返回单个联系人,如果找不到联系人则返回 null。
List<Contact> findAll() 返回存储在数据库中的所有联系人。
Contact save(Contact contact) 将给定的联系人保存到数据库并返回保存的联系人。

CRUD

我们现在已经配置了 Spring 应用程序上下文,实现了应用程序的领域模型,并为Contact实体创建了存储库。我们终于准备好为Contact实体提供 CRUD 功能的服务类的源代码。

让我们从为我们的服务创建一个接口开始。它的作用是声明用于处理联系信息的方法。让我们称这个接口为ContactService。我们的接口的源代码如下所示:

public interface ContactService {

    public Contact add(ContactDTO added);
    public Contact deleteById(Long id) throws NotFoundException;
    public List<Contact> findAll();
    public Contact findById(Long id) throws NotFoundException;
    public Contact update(ContactDTO updated) throws NotFoundException;
}

ContactService接口提到了一个名为ContactDTO的类。它是用于将信息传递给我们的服务实现的数据传输对象DTO)。这个 DTO 在我们应用程序的 Web 层中用作表单对象,它只包含添加或更新联系信息所需的信息。换句话说,它里面没有逻辑。这就是为什么它的源代码在这里没有讨论。

注意

数据传输对象的概念在martinfowler.com/eaaCatalog/dataTransferObject.html中有描述。

我们的下一步是创建一个实现ContactService接口的实现。让我们首先创建一个虚拟实现,稍后再添加实际逻辑。虚拟服务实现描述如下:

  1. @Service注解用于标记我们的实现为服务类。通过添加这个注解,我们确保该类将在类路径扫描期间自动检测到。

  2. 我们使用@Resource注解告诉 Spring IoC 容器必须将创建的存储库实现注入到服务的repository字段中。

注意

通过使用@Transactional注解,服务类的每个方法都被标记为事务性。rollbackFor配置选项确保如果抛出了任何配置的异常,则事务将被回滚。

我们的虚拟服务类的源代码如下所示:

@Service
public class RepositoryContactService implements ContactService {

    @Resource
    private ContactRepository repository;

    //Empty method skeletons
}

接下来我们将动手学习如何使用 Spring Data JPA 创建、读取、更新和删除实体。

创建

我们可以通过以下步骤创建一个新的实体:

  1. 使用构建器模式创建一个新的Contact对象。

  2. 将创建的对象传递给我们存储库的save()方法。

  3. 返回创建的对象。

add()方法的源代码如下所示:

@Transactional
@Override
public Contact add(ContactDTO added) {
    //Creates an instance of a Contact by using the builder pattern
    Contact contact = Contact.getBuilder(added.getFirstName(), added.getLastName())
            .address(added.getStreetAddress(), added.getPostCode(), added.getPostOffice(), added.getState(), added.getCountry())
            .emailAddress(added.getEmailAddress())
            .phoneNumber(added.getPhoneNumber())
            .build();
    return repository.save(contact);
}

读取

我们的应用程序必须向用户提供所有联系人的列表以及单个联系人的信息。ContactService接口声明了两个与这些用例相关的方法。这些方法是:findAll()findById()

我们的findAll()方法的实现非常简单。我们只是将方法调用委托给存储库。findAll()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> findAll() {
    return repository.findAll();
}

现在我们必须创建findById()方法的实现。我们的实现包含以下步骤:

  1. 通过调用我们存储库的findOne()方法来查找联系人。

  2. 如果找不到联系人,则抛出NotFoundException

  3. 返回找到的联系人。

findById()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public Contact findById(Long id) throws NotFoundException {
    Contact found = repository.findOne(id);

    if (found == null) {
        throw new NotFoundException("No contact found with id: " + id);
    }

    return found;
}

更新

我们可以通过以下步骤更新联系人的信息:

  1. 使用服务的findById()方法查找更新后的联系人。因此,如果找不到联系人,则会抛出NotFoundException

  2. 更新联系信息。

  3. 更新地址信息。

  4. 返回更新后的联系人。

update()方法的源代码如下所示:

@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact update(ContactDTO updated) throws NotFoundException {
    Contact found = findById(updated.getId());

    found.update(updated.getFirstName(), updated.getLastName(), updated.getEmailAddress(), updated.getPhoneNumber());

    found.updateAddress(updated.getStreetAddress(), updated.getPostCode(), updated.getPostOffice(), updated.getState(), updated.getCountry());

    return found;
}

注意

如果我们在读写事务中,更新实体信息后不需要显式调用存储库的save()方法。当事务提交时,对持久实体所做的所有更改都会自动更新到数据库中。

删除

我们可以通过以下步骤删除联系人:

  1. 通过调用findById()方法来查找已删除的联系人,如果找不到联系人,则抛出NotFoundException

  2. 将联系人作为参数传递给我们存储库的delete()

  3. 返回已删除的联系人。

deleteById()方法的源代码如下所示:

@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact deleteById(Long id) throws NotFoundException {
    Contact deleted = findById(id);
    repository.delete(deleted);
    return deleted;
}

摘要

在本章中,我们已经学到了:

  • Maven 提供了一种简单的方法来设置 Spring Data JPA 项目

  • 如果我们使用 Spring Framework 3.0 或更新版本,可以通过编程配置来配置应用程序的应用程序上下文

  • 如果我们在 Servlet 3.0 环境中使用 Spring Framework 3.1,可以在没有web.xml的情况下配置我们的 Web 应用程序

  • Spring Data JPA 简化了自定义存储库的创建,因为它可以自动创建我们存储库接口的具体实现。

构建 CRUD 应用程序是一个很好的开始,但它并不能帮助我们创建现实生活中的应用程序。在下一章中,我们将解决这个问题,并描述如何使用 Spring Data JPA 创建数据库查询。

第三章:使用 Spring Data JPA 构建查询

我们已经学会了如何配置 Spring Data JPA 并实现了一个简单的 CRUD 应用程序。现在是时候学习一些技能,这些技能将帮助我们实现真实的应用程序。在本章中,我们将涵盖:

  • 我们如何使用查询方法创建查询

  • 我们如何使用 JPA Criteria API 创建动态查询

  • 我们如何使用 Querydsl 创建动态查询

  • 我们如何对查询结果进行排序和分页

在本章中,我们将通过向联系人管理应用程序添加搜索功能来扩展它。搜索功能的要求如下:

  • 搜索功能必须返回所有名字或姓氏以给定搜索词开头的联系人

  • 搜索必须不区分大小写

  • 搜索结果必须按姓氏和名字按升序排序

  • 搜索功能必须能够对搜索结果进行分页

我们还将学习如何对应用程序主页上显示的联系人列表进行排序和分页。

构建查询

我们可以使用 Spring Data JPA 构建查询的三种选项:查询方法,JPA Criteria API 和 Querydsl。在本节中,我们将学习如何使用它们并开始实现我们的搜索功能。我们还将看一下每个选项的优缺点,并得到关于选择正确的查询创建技术的具体建议。

在我们继续之前,我们必须向ContactService接口添加一个search()方法,该方法用作我们搜索功能的起点。search()方法的签名如下代码片段所示:

public List<Contact> search(String searchTerm);

查询方法

使用 Spring Data JPA 创建查询的最简单方法是使用查询方法。查询方法是在存储库接口中声明的方法。我们可以使用三种技术来创建查询方法:

  • 从方法名称生成查询

  • 命名查询

  • @Query注解

从方法名称生成查询

从方法名称生成查询是一种查询生成策略,其中执行的查询是从查询方法的名称中解析出来的。用于创建查询方法名称的命名约定有三个重要组件:方法前缀属性表达式关键字。接下来,我们将学习这些组件的基本用法并实现我们的搜索功能。我们还将看一下这种方法的优缺点。

方法前缀

每个方法的名称必须以特殊前缀开头。这确保该方法被识别为查询方法。支持的前缀是findByfindreadByreadgetByget。所有前缀都是同义词,对解析的查询没有影响。

属性表达式

属性表达式用于引用托管实体的直接属性或嵌套属性。我们将使用Contact实体来演示以下表中属性表达式的用法:

属性表达式 引用的属性
LastName Contact类的lastName属性。
AddressStreetAddress Address类的streetAddress属性。

让我们通过使用AddressStreetAddress属性表达式来了解属性解析算法是如何工作的。该算法有三个阶段:

  1. 首先,它将检查实体类是否具有与属性表达式匹配的名称的属性,当属性表达式的第一个字母转换为小写时。如果找到匹配项,则使用该属性。如果在Contact类中找不到名为addressStreetAddress的属性,则算法将移至下一个阶段。

  2. 属性表达式从右向左按驼峰命名部分分割为头部和尾部。完成后,算法尝试从实体中找到匹配的属性。如果找到匹配,算法会尝试按照属性表达式的部分从头到尾找到引用的属性。在这个阶段,我们的属性表达式被分成两部分:AddressStreetAddress。由于Contact实体没有匹配的属性,算法继续到第三阶段。

  3. 分割点向左移动,算法尝试从实体中找到匹配的属性。属性表达式被分成两部分:AddressStreetAddress。从Contact类中找到匹配的属性address。此外,由于Address类有一个名为streetAddress的属性,也找到了匹配。

注意

如果Contact类有一个名为addressStreetAddress的属性,属性选择算法会选择它而不是Address类的streetAddress属性。我们可以通过在属性表达式中使用下划线字符手动指定遍历点来解决这个问题。在这种情况下,我们应该使用属性表达式Address_StreetAddress

关键词

关键词用于指定针对属性值的约束,这些属性由属性表达式引用。有两条规则用于将属性表达式与关键词组合在一起:

  • 我们可以通过在属性表达式后添加关键字来创建约束

  • 我们可以通过在它们之间添加AndOr关键字来组合约束

Spring Data JPA 的参考手册(static.springsource.org/spring-data/data-jpa/docs/current/reference/html/)描述了如何使用属性表达式和关键词创建查询方法:

关键词 示例 JPQL 片段
And findByLastNameAndFirstName where x.lastname = ?1 and x.firstname = ?2
Or findByLastNameOrFirstName where x.lastname = ?1 or x.firstname = ?2
Between findByStartDateBetween where x.startDate between 1? and ?2
LessThan findByAgeLessThan where x.age < ?1
GreaterThan findByAgeGreaterThan where x.age > ?1
After findByStartDateAfter where x.startDate > ?1
Before findByStartDateBefore where x.startDate < ?1
IsNull findByAgeIsNull where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull where x.age is not null
Like findByFirstNameLike where x.firstname like ?1
NotLike findByFirstNameNotLike where x.firstname not like ?1
StartingWith findByFirstNameStartingWith where x.firstname like ?1(参数绑定为附加%
EndingWith findByFirstNameEndingWith where x.firstname like ?1(参数绑定为前置%
Containing findByFirstNameContaining where x.firstname like ?1(参数绑定包裹在%中)
OrderBy findByAgeOrderByLastNameDesc where x.age = ?1 order by x.lastname desc
Not findByLastNameNot where x.lastname <> ?1
In findByAgeIn(Collection ages) where x.age in ?1
NotIn findByAgeNotIn(Collection ages) where x.age not in ?1
True findByActiveTrue where x.active = true
False findByActiveFalse where x.active = false

实现搜索功能

现在是时候运用我们学到的技能,为我们的联系人管理应用程序添加搜索功能了。我们可以通过以下步骤来实现搜索功能:

  1. 我们按照描述的命名约定向ContactRepository接口添加查询方法。

  2. 我们实现一个使用查询方法的服务方法。

首先,我们必须创建查询方法。我们的查询方法的签名如下:

public List<Contact> findByFirstNameStartingWithOrLastNameStartingWith(String firstName, String lastName);

其次,我们必须将search()方法添加到RepositoryContactService类中。这个方法简单地将方法调用委托给存储库,并将使用的搜索词作为参数。实现方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
  return repository.findByFirstNameStartingWithOrLastNameStartingWith(searchTerm, searchTerm);
}

优点和缺点

我们现在已经学会了如何使用方法名称策略生成查询。这种策略的优缺点在下表中描述:

优点 缺点

|

  • 这是创建简单查询的快速方法

  • 它为方法名称提供了一致的命名策略

|

  • 方法名称解析器的特性决定了我们可以创建什么样的查询

  • 复杂查询方法的方法名称又长又难看

  • 查询在运行时进行验证

  • 不支持动态查询

|

方法名称解析器的限制的一个很好的例子是缺少Lower关键字。这意味着我们无法通过使用这种策略来实现不区分大小写的搜索。接下来我们将学习创建不受此限制的查询的替代策略。

命名查询

使用 Spring Data JPA 创建查询方法的第二种方法是使用命名查询。如果我们想要使用命名查询创建查询方法,我们必须:

  1. 创建一个命名查询。

  2. 创建执行命名查询的查询方法。

  3. 创建一个使用创建的查询方法的服务方法。

这些步骤在下一节中有更详细的描述。我们还将讨论命名查询的优缺点。

创建命名查询

Spring Data JPA 支持使用 JPQL 或 SQL 创建的命名查询。所使用的查询语言的选择决定了创建的命名查询是如何声明的。

我们可以通过以下步骤创建一个 JPA 命名查询:

  1. @NamedQueries注解添加到实体类中。这个注解以@NamedQuery注解的数组作为其值,并且如果我们指定了多个命名查询,必须使用它

  2. 我们使用@NamedQuery注解来创建命名查询。这个注解有两个对我们有关系的属性:name属性存储了命名查询的名称,query属性包含了执行的 JPQL 查询。

我们的使用 JPQL 的命名查询的声明如下:

@Entity
@NamedQueries({
@NamedQuery(name = "Contact.findContacts",
        query = "SELECT c FROM Contact c WHERE LOWER(c.firstName) LIKE LOWER(:searchTerm) OR LOWER(c.lastName) LIKE LOWER(:searchTerm)")
})
@Table(name = "contacts")
public class Contact

注意

我们也可以使用 XML 声明命名查询。在这种情况下,我们必须使用named-query元素,并在实体映射 XML 文件中声明查询。

我们可以通过以下步骤创建一个命名的本地查询:

  1. 我们将@NamedNativeQueries注解添加到实体类中。这个注解接受@NamedNativeQuery注解的数组作为其值,并且如果我们指定了多个本地命名查询,必须使用它。

  2. 我们通过使用@NamedNativeQuery注解来创建本地命名查询。创建的本地命名查询的名称存储在name属性中。query属性的值是执行的 SQL 查询。resultClass属性包含了查询返回的实体类。

注意

如果命名本地查询不返回实体或实体列表,我们可以使用@SqlResultSetMapping注解将查询结果映射到正确的返回类型。

我们的命名本地查询的声明如下代码片段:

@Entity
@NamedNativeQueries({
@NamedNativeQuery(name = "Contact.findContacts",
        query = "SELECT * FROM contacts c WHERE LOWER(c.first_name) LIKE LOWER(:searchTerm) OR LOWER(c.last_name) LIKE LOWER(:searchTerm)",
        resultClass = Contact.class)
})
@Table(name = "contacts")
public class Contact

注意

我们也可以使用 XML 创建命名本地查询。在这种情况下,我们必须使用named-native-query元素,并在实体映射 XML 文件中声明 SQL 查询。

创建查询方法

我们的下一步是将查询方法添加到联系人存储库中。我们将不得不:

  1. 确定查询方法的正确名称。Spring Data JPA 通过假装托管实体的简单名称和方法名称之间的点来将方法名称解析回命名查询。我们的命名查询的名称是Contact.findContacts。因此,我们必须在ContactRepository接口中添加一个名为findContacts的方法。

  2. 使用@Param注解将方法参数标识为我们查询中使用的命名参数的值。

添加的查询方法的签名如下所示:

public List<Contact> findContacts(@Param("searchTerm") String searchTerm);

创建服务方法

接下来,我们必须将search()方法添加到RepositoryContactService类中。我们的实现包括以下步骤:

  1. 构建使用的 like 模式。

  2. 通过调用创建的查询方法来获取搜索结果。

search()方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
String likePattern = buildLikePattern(searchTerm);
   return repository.findContacts(likePattern);
}

private String buildLikePattern(String searchTerm) {
   return searchTerm + "%";
}

优缺点

现在我们可以使用命名查询来创建查询方法。这种方法的优缺点在下表中描述:

优点 缺点

|

  • 支持 JPQL 和 SQL

  • 使得迁移现有应用程序使用命名查询到 Spring Data JPA 更容易

  • 本地查询的返回类型不限于实体或实体列表

|

  • 查询验证在运行时完成

  • 不支持动态查询

  • 查询逻辑使我们的实体类的代码混乱

|

@Query 注解

@Query注解用于指定调用查询方法时执行的查询。我们可以使用@Query注解来实现 JPQL 和 SQL 查询:

  1. 向存储库添加一个新的方法,并用@Query注解进行注释。

  2. 创建使用查询方法的服务方法。

注意

如果使用@Query注解的方法名称与命名查询的名称冲突,则将执行注解的查询。

接下来,我们将得到具体的指导说明,以指导我们完成所描述的步骤,并了解这种技术的优缺点。

创建查询方法

首先我们必须将查询方法添加到ContactRepository类中。正如我们已经知道的,我们可以使用 JPQL 或 SQL 来创建实际的查询。使用的查询语言对查询方法的创建有一些影响。

我们可以通过以下方式创建使用 JPQL 的查询方法:

  1. ContactRepository接口添加一个新的方法。

  2. 使用@Param注解将方法的参数标识为命名参数的值。

  3. @Query注解注释方法,并将执行的 JPQL 查询设置为其值。

我们的查询方法的声明,满足搜索功能的要求,如下所示:

@Query("SELECT c FROM Contact c WHERE LOWER(c.firstName) LIKE LOWER(:searchTerm) OR LOWER(c.lastName) LIKE LOWER(:searchTerm)")
public Page<Contact> findContacts(@Param("searchTerm") String searchTerm);

为了创建一个使用 SQL 的查询方法,我们必须:

  1. ContactRepository接口添加一个新的方法。

  2. 使用@Param注解将方法参数标识为 SQL 查询中使用的命名参数的值。

  3. @Query注解注释创建的方法,并将 SQL 查询设置为其值。将nativeQuery属性的值设置为 true。

注意

使用@Query注解创建的本地查询只能返回实体或实体列表。如果我们需要不同的返回类型,必须使用命名查询,并使用@SqlResultSetMapping注解映射查询结果。

实现满足搜索功能要求的查询方法的声明如下代码片段所示:

@Query(value = "SELECT * FROM contacts c WHERE LOWER(c.first_name) LIKE LOWER(:searchTerm) OR LOWER(c.last_name) LIKE LOWER(:searchTerm), nativeQuery = true)
public List<Contact> findContacts(@Param("searchTerm") String searchTerm);

注意

Spring Data JPA 不支持使用@Query注解创建的本地查询的动态排序或分页支持,因为没有可靠的方法来操作 SQL 查询。

创建服务方法

我们的下一步是向RepositoryContactService类添加search()方法的实现。我们可以通过以下方式实现:

  1. 获取使用的 like 模式。

  2. 通过调用创建的查询方法来获取搜索结果。

实现的search()方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
   String likePattern = buildLikePattern(searchTerm);
   return repository.findContacts(likePattern);
}

private String buildLikePattern(String searchTerm) {
   return searchTerm + "%";
}

优缺点

我们现在已经学会了如何使用@Query注解来创建查询方法。这种方法自然地具有优缺点,如下表所述:

优点 缺点

|

  • 支持 JPQL 和 SQL

  • 方法名称没有命名约定

|

  • 本地查询只能返回实体或实体列表

  • 不支持动态查询

  • 查询验证在运行时完成

|

JPA Criteria API

JPA Criteria API 为我们提供了以面向对象的方式创建动态和类型安全查询的方法。我们可以通过以下步骤创建条件查询

  1. 我们向存储库添加 JPA Criteria API 支持。

  2. 我们创建了执行的条件查询。

  3. 我们创建了一个执行创建的查询的服务方法。

这些步骤以及使用 JPA Criteria API 的优缺点将在以下部分中描述。

将 JPA Criteria API 支持添加到存储库

我们可以通过扩展JpaSpecificationExecutor<T>接口向存储库添加 JPA Criteria API 支持。当我们扩展这个接口时,我们必须将受管实体的类型作为类型参数给出。ContactRepository接口的源代码如下所示:

public interface ContactRepository extends JpaRepository<Contact, Long>, JpaSpecificationExecutor<Contact> {

}

扩展JpaSpecificationExecutor<T>接口使我们可以访问以下方法,这些方法可用于执行条件查询:

方法 描述
返回与给定搜索条件匹配的实体数量。
List<Contact> findAll(Specification<Contact> s) 返回与给定搜索条件匹配的所有实体。
返回与给定搜索条件匹配的单个联系人。

创建条件查询

正如我们所学的,Spring Data JPA 使用Specification<T>接口来指定条件查询。这个接口声明了Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb)方法,我们可以使用它来创建执行的条件查询。

为了为Contact实体创建条件查询,我们必须:

  1. Contact实体创建一个静态元模型类。

  2. 创建构建Specification<Contact>对象的方法。

创建静态元模型类

静态元模型类提供对描述实体属性的元数据的静态访问,并用于使用 JPA Criteria API 创建类型安全查询。静态元模型类通常是自动生成的,但在这里,我们将为了示例而手动创建一个。我们可以通过遵循以下规则创建一个静态元模型类:

  • 静态元模型类应放置在与相应实体相同的包中

  • 静态元模型类的名称是通过在相应实体的简单名称后附加下划线字符来创建的

由于我们在构建条件查询时只使用Contact实体的firstNamelastName属性,我们可以忽略其他属性。Contact_类的源代码如下所示:

@StaticMetamodel(Contact.class)
public class Contact_ {
    public static volatile SingularAttribute<Contact, String> firstName;
    public static volatile SingularAttribute<Contact, String> lastName;
}

创建规范

我们可以通过创建一个规范构建器类并使用静态方法来构建实际的规范,以清晰的方式创建规范。用于构建所需 like 模式的逻辑也移动到了这个类中。我们规范构建器类的实现在以下步骤中解释:

  1. 我们创建了一个getLikePattern()方法,用于从搜索词创建 like 模式。

  2. 我们创建一个静态的firstOrLastNameStartsWith()方法,返回一个新的Specification<Contact>对象。

  3. 我们在Specification<Contact>toPredicate()方法中构建条件查询。

我们的规范构建器类的源代码如下所示:

public class ContactSpecifications {

    public static Specification<Contact> firstOrLastNameStartsWith(final String searchTerm) {
        return new Specification<Contact>() {
        //Creates the search criteria
        @Override
        public Predicate toPredicate(Root<Contact> root, CriteriaQuery<?> criteriaQuery, cb cb) {
            String likePattern = getLikePattern(searchTerm);
            return cb.or(
            //First name starts with given search term
            cb.like(cb.lower(root.<String>get(Contact_.firstName)), likePattern),
            //Last name starts with the given search term

            cb.like(cb.lower(root.<String>get(Contact_.lastName)), likePattern)
                );
            }

      private String getLikePattern(final String searchTerm) {
          return searchTerm.toLowerCase() + "%";
            }
        };
    }
}

创建服务方法

我们RepositoryContactService类的search()方法的实现包含以下两个步骤:

  1. 我们通过使用我们的规范构建器获得Specification<Contact>对象。

  2. 我们通过调用存储库的findAll()方法并将Specification<Contact>对象作为参数传递来获取搜索结果。

我们的实现的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
Specification<Contact> contactSpec = firstOrLastNameStartsWith(searchTerm);
    return repository.findAll(contactSpec);
}

优缺点

我们现在已经学会了如何使用 JPA Criteria API 实现动态查询。在我们可以在实际应用程序中使用这些技能之前,我们应该了解这种方法的优缺点。这些在下表中描述:

优点 缺点

|

  • 支持动态查询

  • 语法验证在编译期间完成

  • 使得迁移使用 JPA Criteria API 的应用程序到 Spring Data JPA 更容易

|

  • 复杂查询难以实现和理解

|

Querydsl

Querydsl是一个框架,通过类似 SQL 的 API 实现类型安全的动态查询的构建(要了解更多关于 Querydsl 的信息,请访问www.querydsl.com/)。如果我们想使用 Querydsl 创建查询,我们必须:

  1. 配置 Querydsl Maven 集成。

  2. 生成 Querydsl 查询类型。

  3. 向存储库添加 Querydsl 支持。

  4. 创建执行的查询。

  5. 执行创建的查询。

我们将在下一节中更详细地解释这些步骤,并且我们还将看一下 Querydsl 的优缺点。

配置 Querydsl-Maven 集成

Querydsl-Maven 集成的配置包括两个步骤:

  1. 我们配置所需的依赖项。

  2. 我们配置用于代码生成的 APT Maven 插件。

配置 Querydsl Maven 依赖项

因为我们正在使用 Querydsl 与 JPA,所以必须在pom.xml文件中声明以下依赖项:

  • 提供 Querydsl 核心,提供 Querydsl 的核心功能

  • Querydsl APT,提供基于 APT 的代码生成支持

  • Querydsl JPA,为 JPA 注解添加支持

我们正在使用 Querydsl 版本 2.8.0。因此,我们必须将以下依赖声明添加到pom.xml文件的依赖项部分:

<dependency>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-core</artifactId>
  <version>2.8.0<version>
</dependency>
<dependency>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-apt</artifactId>
  <version>2.8.0</version>
</dependency>
<dependency>
  <groupId>com.mysema.querydsl</groupId>
  <artifactId>querydsl-jpa</artifactId>
  <version>2.8.0</version>
</dependency>

配置代码生成 Maven 插件

我们的下一步是配置 Java 6 的注解处理工具的 Maven 插件,用于生成 Querydsl 查询类型。我们可以通过以下方式配置此插件:

  1. 配置插件以在 Maven 的generate-sources生命周期阶段执行其process目标。

  2. 指定生成查询类型的目标目录。

  3. 配置代码生成器以查找实体类的 JPA 注解。

Maven APT 插件的配置如下:

<plugin>
  <groupId>com.mysema.maven</groupId>
    <artifactId>maven-apt-plugin</artifactId>
  <version>1.0.4</version>
  <executions>
      <execution>
          <phase>generate-sources</phase>
      <goals>
        <goal>process</goal>
      </goals>
      <configuration>
        <outputDirectory>target/generated-sources</outputDirectory>
  <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
      </configuration>
    </execution>
  </executions>
</plugin>

生成 Querydsl 查询类型

如果我们的配置正常工作,使用 Maven 构建项目时,Querydsl 查询类型应该会自动生成。

注意

Maven APT 插件存在一个已知问题,阻止直接从 Eclipse 使用它。Eclipse 用户必须通过在命令提示符下运行命令mvn generate-sources来手动创建 Querydsl 查询类型。

查询类型可以从target/generated-sources目录中找到。生成的查询类型将适用以下规则:

  • 每个查询类型都生成在与相应实体相同的包中。

  • 查询类型类的名称是通过将实体类的简单名称附加到字母"Q"来构建的。例如,由于我们的实体类的名称是Contact,相应的 Querydsl 查询类型的名称是QContact

注意

在我们的代码中使用查询类型之前,我们必须将target/generated-sources目录添加为项目的源目录。

向存储库添加 Querydsl 支持

我们可以通过扩展QueryDslPredicateExecutor<T>接口来向存储库添加 Querydsl 支持。当我们扩展此接口时,必须将托管实体的类型作为类型参数给出。ContactRepository接口的源代码如下:

public interface ContactRepository extends JpaRepository<Contact, Long>, QueryDslPredicateExecutor<Contact> {
}

在我们扩展了QueryDslPredicateExecutor<T>接口之后,我们可以访问以下方法:

方法 描述
long count(Predicate p) 返回与给定搜索条件匹配的实体数量。
Iterable<Contact> findAll(Predicate p) 返回与给定搜索条件匹配的所有实体。
Contact findOne(Predicate p) 返回与给定搜索条件匹配的单个实体。

创建执行的查询

每个查询必须实现 Querydsl 提供的Predicate接口。幸运的是,我们不必手动实现这个接口。相反,我们可以使用查询类型来创建实际的查询对象。一个清晰的方法是创建一个特殊的 predicate 构建器类,并使用静态方法来创建实际的 predicates。让我们称这个类为ContactPredicates。我们实现了创建满足搜索功能要求的 predicates 的静态方法,如下所述:

  1. 我们实现了一个静态的firstOrLastNameStartsWith()方法,返回Predicate接口的实现。

  2. 我们获得了QContact查询类型的引用。

  3. 我们使用QContact查询类型构建我们的查询。

我们的 predicate 构建器类的源代码如下:

public class ContactPredicates {

    public static Predicate firstOrLastNameStartsWith(final String searchTerm) {
        QContact contact = QContact.contact;
        return contact.firstName.startsWithIgnoreCase(searchTerm)
                .or(contact.lastName.startsWithIgnoreCase(searchTerm));
    }
}

执行创建的查询

我们通过以下方式实现了RepositoryContactService类的search()方法:

  1. 通过调用ContactPredicates类的静态firstOrLastNAmeStartsWith()方法获取使用的 predicate。

  2. 通过调用我们的存储库方法并将 predicate 作为参数传递来获取结果。

  3. 使用Commons Collections库中的CollectionUtils类将每个联系人添加到返回的列表中。

我们的实现源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
  Predicate contactPredicate = firstOrLastNameStartsWith(searchTerm);

  Iterable<Contact> contacts = repository.findAll(contactPredicate);
  List<Contact> contactList = new ArrayList<Contact>();
  CollectionUtils.addAll(contactList, contacts.iterator());

  return contactList;
}

优点和缺点

现在我们能够使用 Spring Data JPA 和 Querydsl 创建查询。Querydsl 的优缺点如下表所述:

优点 缺点

|

  • 支持动态查询

  • 清晰易懂的 API

  • 语法验证在编译期间完成

|

  • 需要代码生成

  • Eclipse 集成工作不正常

|

我们应该使用哪种技术?

在本节中,我们已经讨论了使用 Spring Data JPA 创建查询的不同方法。我们也意识到了每种描述的技术的优缺点。这些信息被细化为以下列表中给出的具体指南:

  • 我们应该使用查询方法创建静态查询。

  • 如果创建的查询简单且方法名解析器支持所需的关键字,我们可以使用方法名策略生成查询。否则,我们应该使用@Query注解,因为它灵活,并且不强制我们使用冗长且丑陋的方法名。

  • 如果我们无法使用方法策略生成查询或@Query注解创建查询方法,命名查询是有用的。这种方法也可以在将现有应用程序迁移到 Spring Data JPA 时使用。然而,当我们创建新应用程序时,应该谨慎使用它们,因为它们倾向于在我们的实体中添加查询逻辑。

  • 如果我们无法使用其他描述的技术创建查询,或者需要调整单个查询的性能,原生查询是有用的。然而,我们必须理解使用原生查询会在我们的应用程序和使用的数据库模式之间创建依赖关系。此外,如果我们使用特定于提供程序的 SQL 扩展,我们的应用程序将与使用的数据库提供程序绑定。

  • 如果我们正在将使用 criteria 查询的现有应用程序迁移到 Spring Data JPA,应该使用 JPA Criteria API 来创建动态查询。如果我们无法忍受 Querydsl-Eclipse 集成的问题,JPA Criteria API 也是一个有效的选择。

  • Querydsl 是创建动态查询的绝佳选择。它提供了一个清晰易懂的 API,这是 JPA Criteria API 的巨大优势。Querydsl 应该是我们从头开始创建动态查询的首选。笨拙的 Eclipse 集成自然是 Eclipse 用户的缺点。

排序查询结果

在本节课程中,我们将学习使用 Spring Data JPA 对查询结果进行排序的不同技术。我们还将学习可以用于为每种情况选择适当排序方法的准则。

使用方法名进行排序

如果我们使用从方法名生成查询的策略构建查询,我们可以按照以下步骤对查询结果进行排序:

  1. 创建查询方法

  2. 修改现有的服务方法以使用新的查询方法。

创建查询方法

当我们使用从方法名生成查询的策略构建查询时,我们可以使用OrderBy关键字来对查询结果进行排序,当我们:

  1. OrderBy关键字附加到方法名。

  2. 将与实体属性对应的属性表达式附加到方法名,用于对查询结果进行排序。

  3. 将描述排序顺序的关键字附加到方法名。如果查询结果按升序排序,则应使用关键字Asc。当查询结果按降序排序时,使用Desc关键字。

  4. 如果使用多个属性对查询结果进行排序,则重复步骤 2 和步骤 3。

我们可以通过在查询方法的名称后附加字符串OrderByLastNameAscFirstNameAsc来满足搜索功能的新要求。查询方法的签名如下:

public List<Contact> findByFirstNameStartingWithOrLastNameStartingWithOrderByLastNameAscFirstNameAsc(String firstName, String lastName);

修改服务方法

我们必须修改RepositoryContactService类的search()方法,以将方法调用委托给新的查询方法。该方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
    return repository.findByFirstNameStartingWithOrLastNameStartingWithOrderByLastNameAscFirstNameAsc(searchTerm, searchTerm);
}

使用查询字符串进行排序

在某些情况下,我们必须将排序逻辑添加到实际的查询字符串中。如果我们使用带有@Query注解的命名查询或本地查询,我们必须在实际查询中提供排序逻辑。当我们使用带有 JPQL 查询的@Query注解时,也可以将排序逻辑添加到实际查询中。

JPQL 查询

当我们想对 JPQL 查询的查询结果进行排序时,必须使用 JPQL 的ORDER BY关键字。满足搜索功能的 JPQL 查询如下所示:

SELECT c FROM Contact c WHERE LOWER(c.firstName) LIKE LOWER(:searchTerm) OR LOWER(c.lastName) LIKE LOWER(:searchTerm) ORDER BY c.lastName ASC, c.firstName ASC

SQL 查询

当我们想对本地 SQL 查询的查询结果进行排序时,必须使用 SQL 的ORDER BY关键字。满足搜索功能的 SQL 查询如下所示:

SELECT * FROM contacts c WHERE LOWER(c.first_name) LIKE LOWER(:searchTerm) OR LOWER(c.last_name) LIKE LOWER(:searchTerm) ORDER BY c.last_name ASC, c.first_name ASC

使用 Sort 类进行排序

如果我们使用JpaRepository<T,ID>接口的方法、查询方法或 JPA Criteria API,我们可以使用Sort类对查询结果进行排序。如果我们决定使用这种方法,我们必须:

  1. 创建Sort类的实例。

  2. 将创建的实例作为参数传递给所使用的存储库方法。

注意

我们不能使用Sort类对带有@Query注解声明的命名查询或本地查询的查询结果进行排序。

由于后面描述的所有技术都需要获得Sort类的实例,我们将不得不为RepositoryContactService类添加一种创建这些对象的方法。我们将通过创建一个私有的sortByLastNameAndFirstNameAsc()方法来实现这一点。该方法的源代码如下:

private Sort sortByLastNameAndFirstNameAsc() {
  return new Sort(new Sort.Order(Sort.Direction.ASC, "lastName"),
        new Sort.Order(Sort.Direction.ASC, "firstName")
    );
}

JpaRepository

我们使用了JpaRepository<T,ID>接口的findAll()方法来获取存储在数据库中的所有实体的列表。然而,当我们扩展了JpaRepository<T,ID>接口时,我们还可以访问List<Contact> findAll(Sort sort)方法,我们可以使用它来对存储在数据库中的实体列表进行排序。

举例来说,我们将按照姓氏和名字的字母顺序对所有实体的列表进行排序。我们可以通过以下方式实现:

  1. 获取一个新的Sort对象。

  2. 通过调用我们的存储库的findAll()方法并将创建的Sort对象作为参数传递来获取排序后的实体列表。

RepositoryContactServicefindAll()方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> findAll() {
  Sort sortSpec = sortByLastNameAndFirstNameAsc();
  return repository.findAll(sortSpec);
}

从方法名生成查询

我们还可以使用这种方法来对使用方法名称生成查询的查询结果进行排序。如果我们想使用这种技术,我们必须修改查询方法的签名,以接受Sort对象作为参数。我们的查询方法的签名,实现了搜索功能的新排序要求,如下所示:

public Page<Contact> findByFirstNameStartingWithOrLastNameStartingWith(String firstName, String lastName, Sort sort);

我们的下一步是更改RepositoryContactService类的search()方法的实现。新的实现在以下步骤中解释:

  1. 我们获得一个Sort对象的引用。

  2. 我们调用我们的新存储库方法并提供所需的参数。

我们实现的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
  Sort sortSpec = sortByLastNameAndFirstNameAsc();
  return repository.findByFirstNameStartingWithOrLastNameStartingWith(searchTerm, searchTerm, sortSpec);
}

@Query 注解

如果我们使用@Query注解来使用 JPQL 构建查询,我们不必将排序逻辑添加到实际查询中。我们还可以修改查询方法的签名,以接受Sort对象作为参数。我们的查询方法的声明如下所示:

@Query("SELECT c FROM Contact c WHERE LOWER(c.firstName) LIKE LOWER(:searchTerm) OR LOWER(c.lastName) LIKE LOWER(:searchTerm)")
public Page<Contact> findContacts(@Param("searchTerm") String searchTerm, Sort sort);

下一步是修改RepositoryContactService类的search()方法。我们对该方法的实现如下所述:

  1. 我们创建所使用的 like 模式。

  2. 我们获得一个Sort对象的引用。

  3. 我们调用我们的存储库方法并提供所需的参数。

search()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
    String likePattern = buildLikePattern(dto.getSearchTerm());
    Sort sortSpec = sortByLastNameAndFirstNameAsc();
    return repository.findContacts(likePattern, sortSpec);
}

JPA Criteria API

为了使用 JPA Criteria API 创建查询,我们必须修改ContactRepository接口以扩展JpaSpecificationExecutor<T>接口。这使我们可以访问List<Contact> findAll(Specification spec, Sort sort)方法,该方法返回与给定搜索条件匹配的实体的排序列表。

我们对RepositoryContactService类的search()方法的实现如下所述:

  1. 我们通过使用我们的规范构建器类获取所使用的搜索条件。

  2. 我们获取所使用的Sort对象。

  3. 我们将调用ContactRepositoryfindAll()方法并提供必要的参数。

我们的search()方法如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
    Specification<Contact> contactSpec = firstOrLastNameStartsWith(searchTerm);
    Sort sortSpec = sortByLastNameAndFirstNameAsc();
    return repository.findAll(contactSpec, sortSpec);
}

使用 Querydsl 进行排序

在我们的联系人存储库中扩展QuerydslPredicateExecutor<T>接口使我们可以访问Iterable<Contact> findAll(Predicate predicate, OrderSpecifier<?>... orders)方法,该方法返回与给定搜索条件匹配的所有实体的排序列表。

首先,我们必须创建一个服务方法,该方法创建一个OrderSpecifier对象数组。sortByLastNameAndFirstNameAsc()方法的源代码如下所示:

private OrderSpecifier[] sortByLastNameAndFirstNameAsc() {
  OrderSpecifier[] orders = {QContact.contact.lastName.asc(), QContact.contact.firstName.asc()};
  return orders;
}

我们的下一步是修改RepositoryContactService类的search()方法的实现,以满足给定的要求。我们对search()方法的实现如下所述:

  1. 我们获取所使用的搜索条件。

  2. 我们通过调用我们之前创建的sortByLastNameAndFirstNameAsc()方法来获取所使用的OrderSpecifier数组。

  3. 我们调用ContactRepositoryfindAll()方法并提供所需的参数。

  4. 我们使用从Commons Collections库中找到的CollectionUtils类将所有联系人添加到返回的列表中。

search()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(String searchTerm) {
  Predicate contactPredicate = firstOrLastNameStartsWith(searchTerm);
  OrderSpecifier[] orderSpecs = sortByLastNameAndFirstNameAsc();

  Iterable<Contact> contacts = repository.findAll(contactPredicate, orderSpecs);
  List<Contact> contactList = new ArrayList<Contact>();
  CollectionUtils.addAll(contactList, contacts.iterator());

  return contactList;
}

我们应该使用什么技术?

最好的方法是尽可能将查询生成和排序逻辑放在同一个地方。这样,我们只需查看一个地方,就可以检查我们查询的实现。这个一般指导方针可以细化为以下具体说明:

  • 如果我们正在使用方法名称生成查询,我们应该使用这种方法来对查询结果进行排序。如果方法名称变得太长或太丑,我们总是可以使用Sort类来对查询结果进行排序,但这不应该是我们的首选。相反,我们应该考虑使用@Query注解来构建我们的查询。

  • 如果我们使用 JPQL 或 SQL,我们应该在查询字符串中添加排序逻辑。这样我们就可以从同一个地方检查我们的查询逻辑和排序逻辑。

  • 如果我们使用带有@Query注解的命名查询或本地查询,我们必须将排序逻辑添加到我们的查询字符串中。

  • 当我们使用 JPA Criteria API 构建查询时,我们必须使用Sort类,因为这是JpaSpecificationExecutor<T>接口提供的唯一方法。

  • 当我们使用 Querydsl 构建查询时,我们必须使用OrderSpecifier类来对查询结果进行排序,因为这是QueryDslPredicateExecutor<T>接口所要求的。

分页查询结果

对于几乎每个呈现某种数据的应用程序来说,对查询结果进行分页是一个非常常见的需求。Spring Data JPA 分页支持的关键组件是Pageable接口,它声明了以下方法:

方法 描述
int getPageNumber() 返回请求页面的编号。页面编号是从零开始的。因此,第一页的编号是零。
int getPageSize() 返回单个页面上显示的元素数量。页面大小必须始终大于零。
int getOffset() 根据给定的页码和页面大小返回所选偏移量。
Sort getSort() 返回用于对查询结果进行排序的排序参数。

我们可以使用这个接口来通过 Spring Data JPA 对查询结果进行分页:

  1. 创建一个新的PageRequest对象。我们可以使用PageRequest类,因为它实现了Pageable接口。

  2. 将创建的对象作为参数传递给存储库方法。

如果我们使用查询方法来创建我们的查询,我们有两种选项可以作为查询方法的返回类型:

  • 如果我们需要访问请求页面的元数据,我们可以使我们的查询方法返回Page<T>,其中T是受管理实体的类型。

  • 如果我们只对获取请求页面的联系人感兴趣,我们应该使我们的查询方法返回List<T>,其中T是受管理实体的类型。

为了向我们的联系人管理应用程序添加分页,我们必须对应用程序的服务层进行更改,并实现分页。这两个任务在以下子节中有更详细的描述。

改变服务层

由于 Spring Data JPA 存储库只是接口,我们必须在服务层创建PageRequest对象。这意味着我们必须找到一种方法将分页参数传递到服务层,并使用这些参数创建PageRequest对象。我们可以通过以下步骤实现这个目标:

  1. 我们创建了一个存储分页参数和搜索词的类。

  2. 改变服务接口的方法签名。

  3. 我们实现了创建PageRequest对象的方法。

创建一个用于分页参数的类

首先,我们必须创建一个用于存储分页参数和使用的搜索词的类。Spring Data 提供了一个名为PageableArgumentResolver的自定义参数解析器,它将通过解析请求参数自动构建PageRequest对象。有关这种方法的更多信息,请访问static.springsource.org/spring-data/data-jpa/docs/current/reference/html/#web-pagination

我们不会使用这种方法,因为我们不想在我们的 Web 层和 Spring Data 之间引入依赖关系。相反,我们将使用一个只有几个字段、getter 和 setter 的简单 DTO。SearchDTO的源代码如下:

public class SearchDTO {

    private int pageIndex;
    private int pageSize;
    private String searchTerm;

   //Getters and Setters
}

改变服务接口

我们需要修改示例应用程序的ContactService接口,以便为联系人列表和搜索结果列表提供分页支持。所需的更改如下所述:

  • 我们必须用findAllForPage()方法替换findAll()方法,并将页码和页面大小作为参数传递

  • 我们必须修改search()方法的签名,以将SearchDTO作为参数

变更方法的签名如下:

public List<Contact> findAllForPage(int pageIndex, int pageSize);

public List<Contact> search(SearchDTO dto);

创建 PageRequest 对象

在我们可以继续实际实现之前,我们必须向RepositoryContactService类添加一个新方法。这个方法用于创建作为参数传递给我们的存储库的PageRequest对象。buildPageSpecification()方法的实现如下所述:

  1. 我们使用sortByLastNameAndFirstNameAsc()方法来获取对使用的Sort对象的引用。

  2. 我们使用页码、页面大小和 Sort 对象来创建一个新的PageRequest对象。

相关方法的源代码如下:

private Pageable buildPageSpecification(int pageIndex, int pageSize) {
  Sort sortSpec = sortByLastNameAndFirstNameAsc();
  return new PageRequest(pageIndex, pageSize, sortSpec);
}

private Sort sortByLastNameAndFirstNameAsc() {
  return new Sort(new Sort.Order(Sort.Direction.ASC, "lastName"),
        new Sort.Order(Sort.Direction.ASC, "firstName")
    );
}

实现分页

为了对查询结果进行分页,我们必须将创建的PageRequest对象传递给正确的存储库方法。这个方法取决于我们用来构建查询的方法。这些方法中的每一种都在本小节中描述。

JpaRepository

因为ContactRepository扩展了JpaRepository<T,ID>接口,我们可以访问Page<Contact> findAll(Pageable page)方法,用于对所有实体的列表进行分页。RepositoryContactService类的findAllForPage()方法的实现如下所述:

  1. 我们得到了使用的PageRequest对象。

  2. 通过调用存储库方法并将PageRequest对象作为参数传递来获取Page<Contact>的引用。

  3. 我们返回一个联系人列表。

我们的findAllForPage()方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> findAllForPage(int pageIndex, int pageSize) {
  Pageable pageSpecification = buildPageSpecification(pageIndex, pageSize); 

  Page<Contact> page = repository.findAll(pageSpecification);

  return page.getContent();
}

从方法名生成查询

如果我们使用从方法名生成查询的策略来构建查询,我们可以对查询结果进行分页:

  1. 为查询方法添加分页支持。

  2. 从服务方法调用查询方法。

为查询方法添加分页支持

为我们的查询方法添加分页支持相当简单。我们只需要对查询方法的签名进行以下更改:

  1. Pageable接口添加为查询方法的参数。

  2. 确定查询方法的返回类型。

因为我们对页面元数据不感兴趣,所以我们的查询方法的签名如下所示:

public List<Contact> findByFirstNameStartingWithOrLastNameStartingWith(String firstName, String lastName, Pageable page);

修改服务类

RepositoryContactServicesearch()方法需要的修改相当简单。我们得到一个PageRequest对象的引用,并将其作为参数传递给我们的查询方法。修改后的方法源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    Pageable pageSpecification = buildPageSpecification(dto.getPageIndex(), dto.getPageSize());

    return repository.findByFirstNameStartingWithOrLastNameStartingWith(dto.getSearchTerm(), dto.getSearchTerm(), pageSpecification);
}

命名查询

如果我们想要对命名查询的查询结果进行分页,我们必须:

  1. 为查询方法添加分页支持。

  2. 从服务方法调用查询方法。

为查询方法添加分页支持

我们可以通过将Pageable接口作为查询方法的参数来为命名查询支持分页。此时,我们不需要页面元数据。因此,我们的查询方法的签名如下所示:

public List<Contact> findContacts(@Param("searchTerm") String searchTerm, Pageable page);

修改服务类

我们对RepositoryContactService类的search()方法的实现如下所述:

  1. 我们得到了使用的模式。

  2. 我们得到所需的PageRequest对象。

  3. 通过调用修改后的查询方法来获取联系人列表。

我们修改后的search()方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    String likePattern = buildLikePattern(dto.getSearchTerm());

    Pageable pageSpecification = buildPageSpecification(dto.getPageIndex(), dto.getPageSize());

    return repository.findContacts(likePattern, pageSpecification);
}

@Query 注解

我们可以通过@Query注解构建的 JPQL 查询来对查询结果进行分页:

  1. 为查询方法添加分页支持。

  2. 从服务方法调用查询方法。

为查询方法添加分页支持

我们可以通过对方法签名进行以下更改,为使用@Query注解注释的查询方法添加分页支持:

  1. 我们将Pageable接口添加为方法的参数。

  2. 我们确定方法的返回类型。

在这一点上,我们对返回页面的元数据不感兴趣。因此,查询方法的声明如下所示:

@Query("SELECT c FROM Contact c WHERE LOWER(c.firstName) LIKE LOWER(:searchTerm) OR LOWER(c.lastName) LIKE LOWER(:searchTerm)")
public List<Contact> findContacts(@Param("searchTerm") String searchTerm, Pageable page);

修改服务方法

RepositoryContactService类的search()方法的实现如下所述:

  1. 我们得到了使用的 like 模式。

  2. 我们得到了使用过的PageRequest对象的引用。

  3. 通过调用查询方法并将 like 模式和创建的PageRequest对象作为参数,我们可以得到联系人列表。

search()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    String likePattern = buildLikePattern(dto.getSearchTerm());

    Pageable pageSpecification = buildPageSpecification(dto.getPageIndex(), dto.getPageSize());

    return repository.findContacts(likePattern, pageSpecification);
}

JPA Criteria API

为了使用 JPA Criteria API 构建查询,ContactRepository接口必须扩展JpaSpecificationExecutor<T>接口。这使我们可以访问Page<Contact> findAll(Specification spec, Pageable page)方法,该方法可用于对标准查询的查询结果进行分页。我们唯一需要做的就是修改RepositoryContactService类的search()方法。我们的实现如下所述:

  1. 我们得到了使用过的规范。

  2. 我们得到了使用过的PageRequest对象。

  3. 通过调用存储库方法并将规范和PageRequest对象作为参数传递,我们得到了Page的实现。

  4. 通过调用Page类的getContent()方法,我们返回了请求的联系人列表。

我们的搜索方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    Specification<Contact> contactSpec = firstOrLastNameStartsWith(dto.getSearchTerm());
    Pageable pageSpecification = buildPageSpecification(dto.getPageIndex(), dto.getPageSize());

    Page<Contact> page = repository.findAll(contactSpec, pageSpecification);

    return page.getContent();
}

Querydsl

由于ContactRepository接口扩展了QueryDslPredicateExecutor<T>接口,我们可以访问Page<Contact> findAll(Predicate predicate, Pageable page)方法,我们可以用它来对查询结果进行分页。为了为我们的搜索函数添加分页支持,我们必须对RepositoryContactService类的现有search()方法进行一些更改。这个方法的新实现在以下步骤中描述:

  1. 我们得到了使用过的Predicate的引用。

  2. 我们得到了使用过的PageRequest对象。

  3. 通过调用存储库方法并将PredicatePageRequest对象作为参数传递,我们得到了一个Page引用。

  4. 我们返回请求的联系人。

我们的新search()方法的源代码如下所示:

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    Predicate contactPredicate = firstOrLastNameStartsWith(dto.getSearchTerm());
    Pageable pageSpecification = buildPageSpecification(dto.getPageIndex(), dto.getPageSize());

    Page<Contact> page = repository.findAll(contactPredicate, pageSpecification);

    return page.getContent();
}

总结

在本章中,我们已经学到了:

  • 我们可以使用方法名称的查询生成,命名查询或@Query注解来创建 Spring Data JPA 的查询方法

  • 我们可以通过使用 JPA Criteria API 或 Querydsl 来创建动态查询

  • 有三种不同的方法可以用来对查询结果进行排序

  • 如果我们对查询方法的查询结果进行分页,方法的返回类型可以是ListPage

  • 每种查询创建方法都有其优势和劣势,我们在选择当前问题的正确解决方案时必须考虑这些。

有时我们需要向我们的存储库添加自定义函数。这个问题在下一章中解决。

第四章:向 JPA 存储库添加自定义功能

我们已经学会了如何使用 Spring Data JPA 管理实体并创建数据库查询。我们还学会了如何对查询结果进行排序和分页。然而,如果我们从纯粹的架构观点出发,我们会注意到所描述的解决方案并没有遵循关注点分离原则。事实上,我们的服务层包含了揭示存储库层内部工作原理的代码。

这是架构纯度和生产力之间的权衡。和往常一样,这个选择有一些后果。如果我们必须将我们的应用程序迁移到 Spring Data JPA 之外,我们必须对服务和存储库层进行更改。然而,我们有多少次听说过应用程序的存储库层必须发生如此根本性的变化?确切地说,这种情况非常罕见。因此,当回报很高时,这种风险是值得承担的。

本章描述的技术可以用来隐藏服务层的实现细节,但它们还有其他应用。在本章中,我们将涵盖以下主题:

  • 如何向单个存储库添加自定义功能

  • 如何向所有存储库添加自定义功能

我们将使用第三章使用 Spring Data JPA 构建查询中创建的 Querydsl 示例应用程序作为起点。让我们首先刷新一下记忆,并花一点时间审查我们示例应用程序的结构。我们的服务层由一个名为RepositoryPersonService的单个类组成,该类使用我们的名为ContactRepository的存储库接口。我们应用程序的分页和查询构建逻辑位于服务层。这种情况在以下图表中有所说明:

向 JPA 存储库添加自定义功能

向单个存储库添加自定义功能

向单个存储库添加自定义功能是一个有用的功能,当添加的功能只与单个实体相关时。在本节中,我们将探讨如何实现这一点,并将分页和搜索逻辑从服务层移动到存储库层。

如果我们想向单个存储库添加自定义功能,我们必须遵循以下步骤:

  1. 创建声明自定义方法的自定义接口。

  2. 实现创建的接口。

  3. 创建存储库接口。

  4. 创建使用自定义功能的服务实现。

创建自定义接口

我们的第一步是创建一个声明自定义存储库方法的接口。由于我们的目标是将分页和搜索逻辑移动到存储库层,我们必须向创建的接口添加以下方法:

方法 描述
List<Contact> findAllForPage(int pageIndex, int pageSize) 返回属于请求页面的所有联系人。
List<Contact> findContactsForPage(String searchTerm, int pageIndex, int pageSize) 返回与给定搜索词匹配并属于请求页面的所有联系人。

PaginatingContactRepository接口的源代码如下:

public interface PaginatingContactRepository {

    public List<Contact> findAllForPage(int pageIndex, int pageSize);

    public List<Contact> findContactsForPage(String searchTerm, int pageIndex, int pageSize);
}

实现创建的接口

我们现在已经创建了一个指定自定义存储库方法的接口。我们的下一步是创建这个接口的实现,并将所有分页和查询构建代码从服务层移动到这个类中。

存储库基础设施会从与接口位于同一包中的位置寻找我们自定义接口的实现。它正在寻找一个类,其名称与实际存储库接口的简单名称附加一个后缀后创建的字符串匹配。默认情况下,此后缀的值为Impl

注意

我们可以使用 Spring Data JPA 的repositories命名空间元素的repository-impl-postfix属性或@EnableJpaRepositories注解的repositoryImplementationPostfix属性来设置后缀。

目前我们对默认后缀非常满意。因此,实现我们自定义接口的类的名称必须是ContactRepositoryImpl。我们可以按照以下步骤实现这个类:

  1. 编写一些必要的管道代码来配置QueryDslJpaRepository<T, ID>类,用于执行我们的查询。

  2. 实现自定义接口中声明的方法。

配置存储库类

在这个阶段,我们将编写获取QueryDslJpaRepository<Contact, Long>类实例所需的代码。这个过程有以下步骤:

  1. 使用@PersistenceContext注解获取对使用的实体管理器的引用。

  2. 创建一个init()方法,并用@PostConstruct注解进行注释。这样可以确保在 bean 构造后调用该方法,并注入实体管理器引用。

  3. 实现init()方法并创建一个新的QueryDslJpaRepository<Contact, Long>对象。

我们的实现源代码如下:

public class ContactRepositoryImpl implements PaginatingContactRepository {

    @PersistenceContext
    private EntityManager entityManager;

    private QueryDslJpaRepository<Contact, Long> repository;

    //Add methods here

    @PostConstruct
    public void init() {
        JpaEntityInformation<Contact, Long> contactEntityInfo = new JpaMetamodelEntityInformation<Contact, Long>(Contact.class, entityManager.getMetamodel());
        repository = new QueryDslJpaRepository<Contact, Long>(contactEntityInfo, entityManager);
    }
}

实现自定义方法

目前创建的类无法编译,因为我们还没有实现自定义方法。在实现这些方法之前,我们必须将分页逻辑从服务层移动到ContactRepositoryImpl类。因此,这个过程有以下两个步骤:

  1. 将分页相关的代码添加到我们的存储库实现中。

  2. 实现自定义存储库方法。

首先,我们必须将分页相关的代码添加到我们的存储库中。这意味着我们必须将sortByLastNameAndFirstNameAsc()buildPageSpecification()方法添加到ContactRepositoryImpl类中。这些方法的实现保持不变,如下所示:

private Pageable buildPageSpecification(int pageIndex, int pageSize) {
  return new PageRequest(pageIndex, pageSize, sortByLastNameAndFirstNameAsc());
}

private Sort sortByLastNameAndFirstNameAsc() {
  return new Sort(new Sort.Order(Sort.Direction.ASC, "lastName"),
        new Sort.Order(Sort.Direction.ASC, "firstName")
  );
}

下一步是编写findAllForPage()方法的实现,该方法用于获取所请求页面上的联系人列表。这意味着我们必须:

  1. 使用私有的buildPageSpecification()方法获取页面规范。

  2. 通过调用存储库的findAll()方法并将页面规范作为参数传递,获取所请求页面的内容。

  3. 返回联系人列表。

findAllForPage()方法的源代码如下:

@Override
public List<Contact> findAllForPage(int pageIndex, int pageSize) {
    Pageable pageSpec = buildPageSpecification(pageIndex, pageSize);
    Page wanted = repository.findAll(pageSpec);

    return wanted.getContent();
}

我们的最后一个任务是为findContactsForPage()方法提供实现。这个方法的实现有以下步骤:

  1. 通过调用ContactPredicates类的静态firstOrLastNameStartsWith()方法获取使用的搜索条件。

  2. 通过调用私有的buildPageSpecification()方法获取页面规范。

  3. 通过调用存储库的findAll()方法并提供必要的参数,获取所请求页面的内容。

  4. 返回联系人列表。

findContactsForPage()方法的源代码如下:

@Override
public List<Contact> findContactsForPage(String searchTerm, int pageIndex, int pageSize) {
    Predicate searchCondition = firstOrLastNameStartsWith(searchTerm);
    Pageable pageSpec = buildPageSpecification(pageIndex, pageSize);
    Page wanted = repository.findAll(searchCondition, pageSpec);

    return wanted.getContent();
}

创建存储库接口

我们现在已经实现了自定义功能,是时候将这个功能添加到我们的存储库中了。我们需要对现有的ContactRepository接口进行两处更改。具体如下:

  1. 通过扩展PaginatingContactRepository接口,我们可以使自定义方法对我们存储库的用户可用。

  2. 因为服务层不再需要 Querydsl 库的特定方法,我们可以从扩展接口列表中移除QueryDslPredicateExecutor接口。

我们的新存储库接口的源代码如下:

public interface ContactRepository extends JpaRepository<Contact, Long>, PaginatingContactRepository {
}

创建服务实现

最后一步是修改RepositoryContactService类以使用自定义功能。这一步有以下两个阶段:

  1. 移除buildPageSpecification()sortByLastNameAndFirstNameAsc()方法。

  2. 修改findAllForPage()search()方法,将方法调用委托给我们的存储库。

修改后的方法的源代码如下:

@Transactional(readOnly = true)
@Override
public List<Contact> findAllForPage(int pageIndex, int pageSize) {
    return repository.findAllForPage(pageIndex, pageSize);
}

@Transactional(readOnly = true)
@Override
public List<Contact> search(SearchDTO dto) {
    return repository.findContactsForPage(dto.getSearchTerm(), dto.getPageIndex(), dto.getPageSize());
}

我们刚刚做了什么?

我们刚刚将分页和搜索逻辑从RepositoryContactService类移动到ContactRepositoryImpl类,并消除了我们的服务层与 Querydsl 之间的依赖。我们行动的结果如下图所示:

我们刚刚做了什么?

向所有存储库添加自定义功能

有时我们必须向所有存储库添加自定义功能。在本节中,我们将学习如何做到这一点,并创建一个用于通过 ID 删除实体的自定义存储库方法。

我们可以通过以下步骤向所有存储库添加自定义功能:

  1. 创建一个声明自定义方法的基础接口。

  2. 实现创建的接口。

  3. 创建一个存储库工厂 bean。

  4. 配置 Spring Data JPA 以使用我们的存储库工厂 bean。

  5. 创建一个存储库接口。

  6. 实现使用自定义功能的服务类。

创建基础存储库接口

首先创建一个声明了实际存储库中可用方法的基础存储库接口。我们可以通过以下方式实现:

  1. 创建一个接口,将受管实体的类型和其 ID 的类型作为类型参数。

  2. 在我们的基础存储库接口中同时扩展JpaRepository<T, ID>QueryDslPredicateExecutor<T>接口。

  3. @NoRepositoryBean注解标注接口。这可以确保 Spring Data JPA 不会为该接口创建存储库实现。另一个解决方案是将该接口从存储库基础包中移出,但由于很难找到一个合适的逻辑位置,我们暂时不这样做。

  4. 向该接口添加一个T deleteById(ID id)方法。该方法返回被删除的实体,并且如果没有找到给定 ID 的实体,则抛出NotFoundException

让我们称这个接口为BaseRepository。其源代码如下:

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, QueryDslPredicateExecutor<T> {

    public T deleteById(ID id) throws NotFoundException;
}

实现基础存储库接口

接下来我们必须编写BaseRepository<T, ID>接口的实现。这个过程包括以下步骤:

  1. 创建一个实现BaseRepository<T, ID>接口并扩展QueryDslJpaRepository<T, ID>类的类。这确保了该类可以访问JpaRepository<T, ID>接口提供的方法,并且可以使用 Querydsl。

  2. 添加一个构造函数,用于简单地将所需的信息传递给超类。

  3. 实现deleteById()方法。首先,该方法获取被删除的实体。如果找不到实体,该方法会抛出NotFoundException。否则,该方法会删除找到的实体并返回被删除的实体。

创建的GenericBaseRepository类的源代码如下:

public class GenericBaseRepository<T, ID extends Serializable> extends QueryDslJpaRepository<T, ID> implements BaseRepository<T, ID> {

    public GenericBaseRepository(JpaEntityInformation<T, ID> entityMetadata, EntityManager entityManager) {
        super(entityMetadata, entityManager);
    }

    @Override
    public T deleteById(ID id) throws NotFoundException {
        T deleted = findOne(id);
        if (deleted == null) {
            throw new NotFoundException();
        }

        delete(deleted);
        return deleted;
    }
}

创建存储库工厂 bean

现在我们已经实现了自定义功能,我们必须确保在创建具体存储库实现时使用它。这意味着我们必须创建一个自定义存储库工厂 bean 来替换默认的存储库工厂 bean。我们的存储库工厂 bean 有一个单一目的:它将GenericBaseRepository作为Repository接口的所有扩展接口的实现。我们可以通过以下步骤创建一个自定义存储库工厂 bean:

  1. 创建存储库工厂 bean 类的框架。

  2. 创建一个存储库工厂类。

  3. 创建一个用于构建新存储库工厂的构建方法。

创建存储库工厂 bean 类的框架

首先,我们必须创建存储库工厂 bean 类。这个类必须扩展 Spring Data JPA 的默认存储库工厂 beanJpaRepositoryFactoryBean<R, T, I>类。这个类有三个类型参数:存储库的类型,实体的类型和实体的 ID 的类型。类骨架的源代码如下:

public class BaseRepositoryFactoryBean <R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> {

}

创建存储库工厂内部类

第二步是创建实际的存储库工厂类。这个类的实现包括以下步骤:

  1. BaseRepositoryFactory类作为BaseRepositoryFactoryBean类的受保护内部类添加进去。

  2. 使创建的类扩展JpaRepositoryFactory类。

  3. 重写JpaRepositoryFactory类的getTargetRepository()方法。这个方法负责创建实际的存储库实现。

  4. 重写JpaRepositoryFactory类的getRepositoryBaseClass()方法,该方法简单地返回基本存储库实现的类。我们可以忽略作为参数给出的元数据,因为该信息由JpaRepositoryFactory用于决定它是否应该返回SimpleJpaRepositoryQueryDslJpaRepository类。

存储库工厂内部类的源代码如下:

protected static class BaseRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {

  private EntityManager entityManager;

    public BaseRepositoryFactory(EntityManager entityManager) {
      super(entityManager);
        this.entityManager = entityManager;
  }

    @Override
    protected Object getTargetRepository(RepositoryMetadata metadata) {
      return new GenericBaseRepository<T, I>((JpaEntityInformation<T,I>) getEntityInformation(metadata.getDomainType()), entityManager);
  }

    @Override
    protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return GenericBaseRepository.class;
  }
}

为存储库工厂创建构建方法

我们可以通过重写BaseRepositoryFactoryBean类中的createRepositoryFactory()方法来创建我们自定义存储库工厂类的新实例。这个方法简单地创建了BaseRepositoryFactory类的一个新实例,并将实体管理器引用作为构造函数参数传递。重写方法的源代码如下:

@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    return new BaseRepositoryFactory(entityManager);
}

配置 Spring Data JPA

接下来,我们必须配置 Spring Data JPA 在创建存储库接口的具体实现时使用自定义存储库工厂 bean。我们可以通过使用@EnableJpaRepositories注解的repositoryFactoryBeanClass属性来实现这一点。换句话说,我们必须将以下注解添加到ApplicationContext类中:

@EnableJpaRepositories(basePackages = {"com.packtpub.springdata.jpa.repository"}, repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class)

注意

如果我们在使用 XML 配置我们的应用程序,我们可以使用 Spring Data JPA 的repositories命名空间元素的factory-class属性。

创建存储库接口

现在我们已经使自定义功能对所有存储库可用。现在我们必须为Contact实体创建一个存储库接口。我们可以按照以下步骤来做到这一点:

  1. 从扩展接口的列表中移除JpaRepositoryQueryDslPredicateExecutor接口。

  2. 扩展BaseRepository<T, ID>接口。

ContactRepository接口的源代码如下:

public interface ContactRepository extends BaseRepository<Contact, Long> {
}

实现服务层

因为RepositoryContactService类的delete()方法的旧实现包含与我们的新deleteById()存储库方法相同的功能,所以我们必须将RepositoryContactService类的delete()方法更改为将方法调用委托给新的存储库方法。我们的新delete()方法的源代码如下:

@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact deleteById(Long id) throws NotFoundException {
    return repository.deleteById(id);
}

我们刚刚做了什么?

我们实现了一个通用的删除方法,该方法自动对我们应用程序的所有存储库可用。这消除了将特定于实体的删除逻辑添加到服务层的需要,并减少了代码重复。我们还创建了一个自定义存储库工厂,为我们的存储库接口提供GenericBaseRepository作为实现。我们的工作结果如下图所示:

我们刚刚做了什么?

总结

在本章中,我们已经学会了如何向单个存储库或所有存储库添加自定义功能。然而,本章还有另一个非常重要的教训。我们注意到,向存储库添加自定义功能会增加应用程序的复杂性,并迫使我们编写样板代码,这可能会让实现和维护都变得繁琐。这就是为什么我们应该只在绝对必要的时候使用本章描述的技术。

这是最后一章,描述了 Spring Data JPA 的用法。在下一章中,我们将学习如何在运行类 Unix 操作系统的计算机上安装 Redis,并设置一个使用 Redis 作为数据存储的 Web 应用项目。

第五章:使用 Spring Data Redis 入门

在 Spring Data Redis 项目诞生之前,与 Redis 通信的常规方式是使用可以与关系数据库的 JDBC 驱动程序进行比较的客户端库。不同之处在于这些客户端没有实现使得从一个连接器切换到另一个连接器变得困难的标准 API。显然,百万美元的问题是,Spring Data Redis 如何使我们的生活更轻松?

Spring Data Redis 将不同客户端库的 API 隐藏在一个干净且易于使用的单一 API 后面。理论上,这确保我们可以在不对应用程序进行任何更改的情况下更改所使用的 Redis 连接器。尽管这确实是一个有用的功能,如果我们必须更改所使用的连接器,但如果仅因为这个功能就开始使用 Spring Data Redis,这将是天真的。我们必须记住,我们很可能会在应用程序的生命周期中坚持使用一个 Redis 连接器。

然而,我们必须记住,应用程序基本上是通过将不同组件组合在一起构建的。Spring Data Redis 与 Spring 框架提供了无缝集成,后者是用于创建企业应用程序的流行工具。这对于任何使用 Redis 的 Spring 应用程序的开发人员来说自然是一个巨大的好处。

注意

有关 Spring Data Redis 及其功能的更多信息,请访问www.springsource.org/spring-data/redis/

本章将指导我们通过初始配置阶段,并帮助我们设置一个使用 Spring Data Redis 的 Web 应用程序项目。在本章中,我们将涵盖以下主题:

  • 如何在运行类 Unix 操作系统的计算机上安装 Redis

  • 如何使用 Maven 获取 Spring Data Redis 所需的依赖

  • 如何通过使用编程配置来配置我们应用程序的应用程序上下文

  • 如何在应用程序上下文配置类中配置我们的 Redis 连接

安装 Redis

Spring Data Redis 要求使用 Redis 2.0 或更高版本,并建议使用 Redis 2.2。但是,即使可能使用更新的 Redis 版本,新功能可能尚不受支持。本书假定我们使用的是 Redis 版本 2.6.0-rc6。

目前 Redis 并不正式支持 Windows,但有一些非官方的端口可用。如果要将 Redis 安装到 Windows 计算机上,请下载其中一个非官方源包,并按照其安装说明进行操作。非官方 Windows 端口的下载链接可在redis.io/download上找到。

注意

Redis 的唯一依赖是一个可用的 GCC 编译器和 libc。安装这些依赖的最佳方法是使用所用 Linux 发行版的软件包管理器。如果在使用 OS X 操作系统的计算机上编译 Redis,则应确保安装了 Xcode 及其命令行工具。

我们可以通过以下步骤将 Redis 安装到运行类 Unix 操作系统的计算机上:

  1. 下载 Redis 源包。我们使用一个称为wget的命令行实用程序来检索源包。

  2. 解压源包。

  3. 编译 Redis。

我们可以通过在命令行上运行以下命令来完成安装过程:

wget http://redis.googlecode.com/files/redis-2.6.0-rc6.tar.gz
tar xzf redis-2.6.0-rc6.tar.gz
cd redis-2.6.0-rc6
make

注意

目前,源包托管在 Google Code 上。如果包被移动到不同的主机上,或者安装了不同的 Redis 版本,这些命令必须相应地进行修改。

编译成功后,我们可以通过在命令提示符下运行以下命令来启动 Redis 服务器:

./src/redis-server

如果我们的安装成功,我们应该看到如下截图所示的输出:

安装 Redis

注意

官方 Redis 主页有全面的文档,提供有关 Redis 的使用和配置的更多信息。此文档可在redis.io/documentation上找到。

获取所需的依赖项

获取 Spring Data Redis 所需的依赖项相当容易。唯一需要的依赖项是 Spring Data Redis,我们可以通过将以下依赖声明添加到pom.xml文件中来获取它:

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-redis</artifactId>
  <version>1.0.1.RELEASE</version>
</dependency>

配置 Spring 应用程序上下文

我们将使用 Java 配置来配置我们应用程序的应用程序上下文。我们应用程序上下文配置类的名称是ApplicationContext,其实现在以下几点中进行了解释:

  1. @Configuration注解用于将类标识为应用程序上下文配置类。

  2. @ComponentScan注解用于配置我们控制器的基本包。

  3. @EnableWebMvc注解用于启用 Spring MVC。

  4. 配置参数的值是从一个属性文件中获取的,该文件是通过使用@PropertySource注解导入的。Environment接口用于访问存储在该文件中的属性值。

  5. redisConnectionFactory()方法用于配置 Redis 连接工厂 bean。此方法的实现取决于所使用的 Redis 连接器。

我们应用程序上下文配置骨架类的源代码如下:

@Configuration
@ComponentScan(basePackages = {
        "com.packtpub.springdata.redis.controller"
})
@EnableWebMvc
@PropertySource("classpath:application.properties")
public class ApplicationContext extends WebMvcConfigurerAdapter {

    @Resource
    private Environment env;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
      //Add implementation
    }

    //Add other configuration here   
}

application.properties文件的内容如下:

redis.host = localhost
redis.port = 6379

配置 Redis 连接

Spring Data Redis 支持四种不同的连接器,可用于与 Redis 服务器交互。这些连接器在以下表格中描述:

连接器 描述
Jedis Jedis 是与 Redis 2.0.0 命令完全兼容的 Redis 连接器。该项目托管在 GitHub 上,有关更多信息,请访问github.com/xetorthio/jedis
JRedis JRedis 是一个 Redis 连接器,尚未正式支持 Redis 2.x。但是,可以使用此库的分支版本添加对 Redis 2.x 的支持。JRedis 库的分支版本托管在 GitHub 上,其主页位于github.com/anthonylauzon/jredis
RJC RJC 是与 Redis 2.X 兼容的 Redis 连接器。有关 RJC 连接器的更多信息,请访问github.com/e-mzungu/rjc
SRP SRP 是支持 Redis 2.6 命令的 Redis 连接器。该项目的主页位于github.com/spullara/redis-protocol

不幸的是,目前一些支持的连接器处于早期开发阶段,它们不支持 Redis 的所有可用功能。如果底层连接器不支持执行的操作,则会抛出异常UnsupportedOperationException

此外,我们可以使用的 Spring Data Redis 的配置选项取决于所使用的连接器。以下表格描述了支持的 Redis 连接器之间的差异(X 表示支持配置选项,-表示不支持):

连接器 密码 连接池
Jedis X X
JRedis X X
RJC X X
SRP - -

Jedis 是 Spring Data Redis 的默认连接器,目前应该是我们的首选,因为它是支持的连接器中最成熟的,并且正在积极开发。但是,我们将查看每个支持的连接器的配置过程,因为情况可能会在将来发生变化,如果发生变化,我们也应该知道我们还有其他选择。

每个支持的 Redis 连接器的配置始终有以下两个步骤:

  1. 配置正确的 Maven 依赖项。

  2. ApplicationContext类的redisConnectionFactory()方法中配置正确的 Redis 连接工厂 bean。

配置 Jedis 连接器

因为 Jedis 是 Spring Data Redis 的默认连接器,我们不必对我们的pom.xml文件进行任何更改。我们唯一需要做的是在应用程序上下文配置中添加 Redis 连接工厂 bean。Jedis 连接器的正确 Redis 连接工厂 bean 类是JedisConnectionFactory类,它具有以下配置属性:

属性 描述
dataBase 使用的数据库的索引。
hostName 使用的 Redis 服务器的主机名。
password 用于与 Redis 服务器进行身份验证的密码。
poolConfig 通过使用redis.clients.jedis.JedisPoolConf类给出的连接池配置。
port 使用的 Redis 服务器的端口。
shardInfo 配置JedisConnectionFactory对象的替代方法。通过使用redis.clients.jedis.JedisShardInfo类给出配置。此方法优先于其他冲突的配置属性。
timeout 连接超时。
usePool 描述是否使用连接池的boolean值。

我们将在ApplicationContext类的redisConnectionFactory()方法中配置 Redis 连接工厂 bean。我们的实现包括以下步骤:

  1. 创建一个新的JedisConnectionFactory对象。

  2. 配置 Redis 连接工厂 bean。

  3. 返回创建的对象。

实现的redisConnectionFactory()方法的源代码如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
  JedisConnectionFactory cf = new JedisConnectionFactory();

  cf.setHostName(env.getRequiredProperty("redis.host"));
  cf.setPort(Integer.parseInt(env.getRequiredProperty("redis.port")));

  return cf;
}

配置 JRedis 连接器

第一步是配置 JRedis 的 Maven 依赖项。我们可以通过以下步骤配置所需的依赖项:

  1. 从我们的构建中排除传递的 Jedis 依赖项。

  2. 将分叉的 JRedis 连接器作为依赖项添加。

在我们按照描述的步骤进行之后,我们应该在pom.xml文件中有以下依赖声明:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.0.1.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.jredis</groupId>
    <artifactId>jredis-anthonylauzon</artifactId>
    <version>03122010</version>
</dependency>

第二步是配置使用的 Redis 连接工厂 bean。因为我们想要使用 JRedis 连接器,我们必须使用JRedisConnectionFactory类。该类的配置属性在下表中描述:

属性 描述
dataBase 使用的数据库的索引。
hostName 使用的 Redis 服务器的主机名。
password 用于与 Redis 服务器进行身份验证的密码。
poolSize 连接池的大小。
port 使用的 Redis 服务器的端口。
usePool 描述是否使用连接池的boolean值。

为了配置 Redis 连接器,我们必须将redisConnectionFactory()方法的实现添加到ApplicationContext类中。我们可以通过以下步骤来实现:

  1. 创建一个新的JRedisConnectionFactory对象。

  2. 配置 Redis 连接工厂 bean。

  3. 返回创建的对象。

我们的 Redis 连接工厂 bean 配置的源代码如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
    JredisConnectionFactory cf = new JredisConnectionFactory();

    cf.setHostName(env.getRequiredProperty("redis.host"));   
    cf.setPort(Integer.parseInt(env.getRequiredProperty("redis.port")));

    return cf;
}

配置 RJC 连接器

首先,我们必须配置所需的 Maven 依赖项。此过程包括以下步骤:

  1. 从我们的构建中排除传递的 Jedis 依赖项。

  2. 将 RJC 连接器作为依赖项添加。

必须添加到我们的pom.xml文件的依赖声明如下:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.0.1.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.idevlab</groupId>
    <artifactId>rjc</artifactId>
    <version>0.7</version>
</dependency>

最后一步是将使用的 Redis 连接工厂 bean 的配置添加到我们的应用程序上下文配置类中。因为我们使用的是 RJC 连接器,正确的 Redis 连接工厂类是RjcConnectionFactory。该类具有以下配置属性:

属性 描述
dataBase 使用的数据库的索引。
hostName 使用的 Redis 服务器的主机名。
password 用于与 Redis 服务器进行身份验证的密码。
port 所使用的 Redis 服务器的端口。
timeout 连接超时的值。
usePool 描述是否使用连接池的boolean值。

我们的redisConnectionFactory()方法的实现包括以下步骤:

  1. 创建一个新的RjcConnectionFactory对象。

  2. 配置 Redis 连接工厂 bean。

  3. 返回创建的对象。

我们的 Redis 连接工厂 bean 配置的源代码如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
    RjcConnectionFactory cf = new RjcConnectionFactory();

    cf.setHostName(env.getRequiredProperty("redis.host"));    
    cf.setPort(Integer.parseInt(env.getRequiredProperty("redis.port")));

    return cf;
}

配置 SRP 连接器

第一步是配置 SRP Redis 连接器的 Maven 依赖项。我们可以通过以下步骤配置所需的依赖项:

  1. 从我们的构建中排除传递的 Jedis 依赖项。

  2. 将 SRP 连接器添加为依赖项。

这导致以下依赖声明:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.0.1.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.github.spullara.redis</groupId>
    <artifactId>client</artifactId>
    <version>0.2</version>
</dependency>

第二步是配置 Redis 连接工厂 bean。SRP 连接器的正确连接工厂 bean 类是SrpConnectionFactory,它具有以下配置属性:

属性 描述
hostName 所使用的 Redis 服务器的主机名。
port 所使用的 Redis 服务器的端口。

我们可以通过编写一个实现redisConnectionFactory()方法的实现来配置 SRP 连接器。我们的实现有以下步骤:

  1. 创建一个新的SrpConnectionFactory对象。

  2. 配置 Redis 连接工厂 bean。

  3. 返回创建的对象。

我们的 Redis 连接工厂 bean 配置的源代码如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
    SrpConnectionFactory cf = new SrpConnectionFactory();

    cf.setHostName(env.getRequiredProperty("redis.host"));
    cf.setPort(Integer.parseInt(env.getRequiredProperty("redis.port")));

    return cf;
}

摘要

在本章中,我们已经学到:

  • Redis 没有复杂的依赖关系,很容易将 Redis 安装到运行类 Unix 操作系统的计算机上

  • 一些支持的连接器尚不支持 Redis 的所有功能

  • 在配置 Redis 连接时,我们可以使用的配置选项取决于所使用的连接器

  • 当我们使用 Spring Data Redis 编写应用程序时,应该使用 Jedis 连接器

我们现在已经学会了如何设置使用 Spring Data Redis 的 Web 应用程序项目。在下一章中,我们将编写一些代码,并使用 Spring Data Redis 实现联系人管理应用程序。

第六章:使用 Spring Data Redis 构建应用程序

我们已经学会了如何设置我们的项目并配置所使用的 Redis 连接。现在是扩展我们的知识并学习如何在应用程序中使用 Spring Data Redis 的时候了。我们还将证明可以将 Redis 用作 Web 应用程序的数据存储。

注意

Salvatore Sanfilippo是 Redis 项目的贡献者,他写了一篇精彩的博客文章,描述了我们如何在应用程序中使用 Redis。这篇博客文章可以在antirez.com/post/take-advantage-of-redis-adding-it-to-your-stack.html找到。

在本章中,我们将涵盖:

  • Redis 数据模型的基本设计原则

  • Spring Data Redis 的关键组件

  • 我们如何可以实现 CRUD 应用程序

  • 我们如何可以使用发布/订阅消息模式

  • 我们如何可以使用 Spring Data Redis 作为 Spring Framework 3.1 提供的缓存抽象的实现

设计 Redis 数据模型

设计 Redis 数据模型的最重要规则是:Redis 不支持特设查询,也不支持关系数据库中的关系。因此,设计 Redis 数据模型与设计关系数据库的数据模型完全不同。Redis 数据模型设计的基本指导原则如下:

  • 我们不仅要对存储在数据模型中的信息进行建模,还要考虑如何从中搜索信息。这经常导致我们不得不复制数据以满足给定的要求。不要害怕这样做。

  • 我们不应该专注于规范化我们的数据模型。相反,我们应该将需要处理的数据作为一个单元合并成一个聚合。

  • 由于 Redis 不支持关系,我们必须使用支持的数据结构来设计和实现这些关系。这意味着当这些关系发生变化时,我们必须手动维护这些关系。因为这可能需要大量的工作和代码,所以简单地复制信息而不使用关系可能是明智的。

  • 花一点时间验证我们是否使用了正确的工具总是明智的。

提示

NoSQL Distilled,由Martin Fowler编写,解释了不同的 NoSQL 数据库及其用例,可以在martinfowler.com/books/nosql.html找到。

正如我们在第一章中学到的,入门,Redis 支持多种数据结构。然而,一个问题仍然没有得到解答:我们应该使用哪种数据结构来存储我们的数据?这个问题在下表中得到了解答:

数据类型 描述
字符串 字符串是存储已转换为文本形式的信息的好选择。例如,如果我们想存储 HTML、JSON 或 XML,字符串应该是我们的选择。
列表 如果我们只会在开始或结束附近访问它,列表是一个不错的选择。这意味着我们应该用它来表示队列或堆栈。
集合 如果我们需要获取集合的大小或检查某个项目是否属于它,我们应该使用集合。此外,如果我们想表示关系,集合是一个很好的选择(例如,“约翰的朋友是谁?”)。
有序集合 当项目的排序对我们很重要时,应该在与集合相同的情况下使用有序集合。
哈希 哈希是表示复杂对象的完美数据结构。

关键组件

Spring Data Redis 提供了一些组件,它们是使用它的每个应用程序的基石。本节简要介绍了我们稍后将用来实现示例应用程序的组件。

原子计数器

原子计数器对于 Redis 来说就像序列对于关系数据库一样。原子计数器保证客户端接收的值是唯一的。这使得这些计数器成为在 Redis 中创建唯一 ID 的完美工具。目前,Spring Data Redis 提供了两种原子计数器:RedisAtomicIntegerRedisAtomicLong。这些类为整数和长整数提供了原子计数器操作。

RedisTemplate

RedisTemplate<K,V>类是 Spring Data Redis 的核心组件。它提供了我们可以用来与 Redis 实例通信的方法。在实例化时,这个类要求给定两个类型参数:用于 Redis 键的类型和 Redis 值的类型。

操作

RedisTemplate类提供了两种我们可以用来存储、获取和删除 Redis 实例中的数据的操作:

  1. 需要每次操作都提供键和值的操作。当我们必须使用键和值执行单个操作时,这些操作非常方便。

  2. 绑定到只给定一次的特定键的操作。当我们必须使用相同的键执行多个操作时,应该使用这种方法。

需要每次操作都提供键和值的方法在以下列表中描述:

  • HashOperations<K,HK,HV> opsForHash(): 此方法返回对哈希执行的操作

  • ListOperations<K,V> opsForList(): 此方法返回对列表执行的操作

  • SetOperations<K,V> opsForSet(): 此方法返回对集合执行的操作

  • ValueOperations<K,V> opsForValue(): 此方法返回对简单值执行的操作

  • ZSetOperations<K,HK,HV> opsForZSet(): 此方法返回对有序集合执行的操作

RedisTemplate类的方法允许我们使用相同的键执行多个操作,在以下列表中描述:

  • BoundHashOperarations<K,HK,HV> boundHashOps(K key): 此方法返回绑定到给定键的哈希操作

  • BoundListOperations<K,V> boundListOps(K key): 此方法返回绑定到给定键的列表操作

  • BoundSetOperations<K,V> boundSetOps(K key): 此方法返回绑定到给定键的集合操作

  • BoundValueOperations<K,V> boundValueOps(K key): 此方法返回绑定到给定键的简单值的操作

  • BoundZSetOperations<K,V> boundZSetOps(K key): 此方法返回绑定到给定键的有序集合的操作

当我们开始构建示例应用程序时,这些操作之间的差异就变得清晰起来了。

序列化器

因为数据以字节形式存储在 Redis 中,我们需要一种方法将数据转换为字节,反之亦然。Spring Data Redis 提供了一个名为RedisSerializer<T>的接口,用于序列化过程。这个接口有一个类型参数,描述了序列化对象的类型。Spring Data Redis 提供了这个接口的几种实现。这些实现在下表中描述:

序列化器 描述
GenericToStringSerializer<T> 将字符串序列化为字节,反之亦然。使用 Spring 的ConversionService将对象转换为字符串,反之亦然。
JacksonJsonRedisSerializer<T> 将对象转换为 JSON,反之亦然。
JdkSerializationRedisSerializer 为对象提供基于 Java 的序列化。
OxmSerializer 使用 Spring Framework 3 的对象/XML 映射支持。
StringRedisSerializer 将字符串转换为字节,反之亦然。

我们可以使用描述的序列化器来自定义RedisTemplate类的序列化过程。RedisTemplate类提供了灵活的配置选项,可用于设置用于序列化值键、值、哈希键、哈希值和字符串值的序列化器。

RedisTemplate类的默认序列化器是JdkSerializationRedisSerializer。但是,字符串序列化器是一个例外。StringRedisSerializer是默认用于序列化字符串值的序列化器。

实现 CRUD 应用程序

本节描述了实现用于管理联系信息的 CRUD 应用程序的两种不同方式。首先,我们将学习如何使用RedisTemplate类的默认序列化器来实现 CRUD 应用程序。其次,我们将学习如何使用值序列化器并实现以 JSON 格式存储数据的 CRUD 应用程序。

这两个应用程序还将共享相同的领域模型。这个领域模型包括两个类:ContactAddress。这些类的信息内容已经在第二章中描述,使用 Spring Data JPA 入门。但是,我们对这些类进行了以下更改:

  • 我们从中删除了 JPA 特定的注解

  • 我们在我们的 Web 层中使用这些类作为表单对象,它们不再具有除了 getter 和 setter 之外的任何其他方法

领域模型不是这些示例共享的唯一内容。它们还共享了声明Contact类的服务方法的接口。ContactService接口的源代码如下:

public interface ContactService {
    public Contact add(Contact added);
    public Contact deleteById(Long id) throws NotFoundException;
    public List<Contact> findAll();
    public Contact findById(Long id) throws NotFoundException;
    public Contact update(Contact updated) throws NotFoundException;
}

这两个应用程序将使用在第五章中描述的 Jedis 连接器与所使用的 Redis 实例进行通信,使用 Spring Data Redis 入门

无论用户的方法如何,我们都可以通过以下步骤实现使用 Spring Data Redis 的 CRUD 应用程序:

  1. 配置应用程序上下文。

  2. 实现 CRUD 功能。

让我们开始并找出如何实现联系信息的 CRUD 功能。

使用默认序列化器

本小节描述了如何使用RedisTemplate类的默认序列化器来实现 CRUD 应用程序。这意味着StringRedisSerializer用于序列化字符串值,而JdkSerializationRedisSerializer用于序列化其他对象。

配置应用程序上下文

我们可以通过对ApplicationContext类进行以下更改来配置我们应用程序的应用程序上下文:

  1. 配置 Redis 模板 bean。

  2. 配置 Redis 原子长整型 bean。

配置 Redis 模板 bean

我们可以通过向ApplicationContext类添加一个redisTemplate()方法并使用@Bean注解对该方法进行注解来配置 Redis 模板 bean。我们可以通过以下步骤实现此方法:

  1. 创建一个新的RedisTemplate对象。

  2. 将使用的连接工厂设置为创建的RedisTemplate对象。

  3. 返回创建的对象。

redisTemplate()方法的源代码如下:

@Bean
public RedisTemplate redisTemplate() {
  RedisTemplate<String, String> redis = new RedisTemplate<String, String>();

  redis.setConnectionFactory(redisConnectionFactory());

  return redis;
}

配置 Redis 原子长整型 bean

我们通过向ApplicationContext类添加一个名为redisAtomicLong()的方法,并使用@Bean注解对该方法进行注解来开始配置 Redis 原子长整型 bean。我们的下一个任务是通过以下步骤实现此方法:

  1. 创建一个新的RedisAtomicLong对象。将所使用的 Redis 计数器的名称和 Redis 连接工厂作为构造函数参数传递。

  2. 返回创建的对象。

redisAtomicLong()方法的源代码如下:

@Bean
public RedisAtomicLong redisAtomicLong() {
  return new RedisAtomicLong("contact", redisConnectionFactory());
}

注意

如果我们需要为不同类的实例创建 ID,我们可以使用相同的 Redis 计数器。因此,我们只需配置一个 Redis 原子长整型 bean。

CRUD

在我们可以开始为Contact类实现 CRUD 函数之前,我们必须先讨论一下我们应用程序的 Redis 数据模型。我们使用两种不同的数据类型来将联系人信息存储到 Redis 中。单个联系人的信息存储在哈希中,因为我们知道,哈希是存储复杂对象信息的很好的结构。此外,我们将每个联系人的密钥存储在一个集合中,因为集合在检查联系人是否存在时为我们提供了快速的能力。当我们从 Redis 中获取所有联系人的列表时,我们也使用这个集合。

我们的下一步是实现ContactService接口,该接口声明了联系人的 CRUD 操作。让我们首先创建一个虚拟服务实现,然后稍后添加实际的 CRUD 方法。该类的实现包括以下步骤:

  1. 实现ContactService接口。

  2. @Service注解创建的类。

  3. 将所需的依赖项添加为创建的类的私有成员,并使用@Resource注解对这些成员进行注解。我们需要引用RedisTemplateRedisAtomicLong对象。

我们虚拟实现的源代码如下:

@Service
public class RedisContactService implements ContactService {

    @Resource
    private RedisAtomicLong contactIdCounter;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    //Add methods here.
}

下一步是实现ContactService接口声明的方法所使用的通用方法。这些私有方法在下表中描述:

方法 描述
String buildKey(Long contactId) 返回联系人的密钥。
Contact buildContact(String key) 获取联系人的信息并返回找到的联系人。
Contact buildContact(Long id) 获取联系人的信息并返回找到的联系人。
boolean contactDoesNotExist(Long id) 如果找到具有给定 ID 的联系人,则返回 false,否则返回 true。
String persist(Contact persisted) 保存联系人信息并返回联系人的密钥。

首先,我们必须实现用于构建联系人密钥的方法。我们的buildKey()方法的实现非常简单。我们通过将作为参数给定的联系人 ID 附加到字符串contact并返回结果字符串来构建密钥。buildKey()方法的源代码如下:

private String buildKey(Long contactId) {
    return "contact" + contactId;
}

其次,我们必须实现使用联系人密钥获取联系人信息的方法。我们可以通过以下步骤实现buildContact(String key)方法:

  1. 创建一个新的Contact对象。

  2. 从哈希中获取联系人的信息。

注意

我们使用绑定的哈希操作,因为这样我们只需要提供一次密钥。

  1. 返回创建的对象。

实现方法的源代码如下:

private Contact buildContact(String key) {
    Contact contact = new Contact();

    BoundHashops ops = redisTemplate.boundHashOps(key);

    contact.setId((Long) ops.get("id"));
    contact.setEmailAddress((String) ops.get("emailAddress"));
    contact.setFirstName((String) ops.get("firstName"));
    contact.setLastName((String) ops.get("lastName"));
    contact.setPhoneNumber((String) ops.get("phoneNumber"));

    Address address = new Address();
    address.setStreetAddress((String) ops.get("streetAddress"));
    address.setPostCode((String) ops.get("postCode"));
    address.setPostOffice((String) ops.get("postOffice"));
    address.setState((String) ops.get("state"));
    address.setCountry((String) ops.get("country"));
    contact.setAddress(address);

    return contact;
}

第三,我们必须实现使用联系人 ID 获取联系人信息的方法。我们的buildContact(Long id)方法相当简单,包括以下步骤:

  1. 构建联系人的密钥。

  2. 使用创建的密钥获取联系人。

  3. 返回找到的联系人。

该方法的源代码如下:

private Contact buildContact(Long id) {
    String key = buildKey(id);
    return buildContact(key);
}

第四,我们必须实现用于验证所讨论的联系人是否存在的方法。我们的contactDoesNotExist()方法的实现包括以下步骤:

  1. 创建联系人的密钥。

  2. 通过调用SetOperations类的isMember()方法,并传递集合的名称和密钥作为参数,检查密钥是否在联系人集合中找到。

注意

我们使用setOperations因为我们只执行一个命令。

  1. 反转isMember()方法的返回值并返回反转后的值。

该方法的源代码如下:

private boolean contactDoesNotExist(Long id) {
    String key = buildKey(id);
    return !redisTemplate.opsForSet().isMember("contacts", key);
}

第五,我们必须实现保存单个联系人信息的方法。我们的persist()方法的实现包括以下步骤:

  1. 如果持久化的Contact对象没有 ID,则调用RedisAtomicLong类的incrementAndGet()方法创建一个 ID,并将接收到的Long对象设置为联系人 ID。

  2. 为持久化的联系人构建一个键。

  3. 将联系人保存在哈希中。

  4. 返回持久化的联系人。

persist()方法的源代码如下:

private String persist(Contact persisted) {
    Long id = persisted.getId();
    if (id == null) {
        id = contactIdCounter.incrementAndGet();
        persisted.setId(id);
    }

    String contactKey = buildKey(id);

    BoundHashops ops = redisTemplate.boundHashOps(contactKey);

    ops.put("id", persisted.getId());
    ops.put("emailAddress", persisted.getEmailAddress());
    ops.put("firstName", persisted.getFirstName());
    ops.put("lastName", persisted.getLastName());
    ops.put("phoneNumber", persisted.getPhoneNumber());

    Address address = persisted.getAddress();

    ops.put("streetAddress", address.getStreetAddress());
    ops.put("postCode", address.getPostCode());
    ops.put("postOffice", address.getPostOffice());
    ops.put("state", address.getState());
    ops.put("country", address.getCountry());

    return contactKey;
}

我们现在已经实现了RedisContactService类的常用方法。让我们继续找出如何为联系信息提供 CRUD 操作。

创建

我们可以通过以下步骤创建一个新的联系人:

  1. 将添加的联系人保存到哈希中。

  2. 将联系人的键添加到我们的联系人集合中。

  3. 返回添加的联系人。

add()方法的源代码如下:

@Override
public Contact add(Contact added) {
  String key = persist(added);
  redisTemplate.opsForSet().add("contacts", key);
  return added;
}

读取

我们必须提供两种方法,用于从 Redis 中获取联系人信息。第一种方法用于返回现有联系人的列表,第二种方法用于查找单个联系人的信息。

首先,我们必须实现一个方法,用于返回现有联系人的列表。我们可以通过以下步骤实现findAll()方法:

  1. 创建一个新的ArrayList对象,用于存储找到的Contact对象。

  2. 从联系人集合中获取现有联系人的键。

  3. 从哈希中获取每个现有联系人的信息,并将它们添加到创建的ArrayList对象中。

  4. 返回联系人列表。

实现方法的源代码如下:

@Override
public List<Contact> findAll() {
  List<Contact> contacts = new ArrayList<Contact>();

  Collection<String> keys = redisTemplate.opsForSet().members("contacts");

  for (String key: keys) {
    Contact contact = buildContact(key);
    contacts.add(contact);
  }

  return contacts;
}

其次,我们必须实现一个方法,用于返回单个联系人的信息。我们可以通过以下步骤实现findById()方法:

  1. 检查联系人是否存在。如果联系人不存在,则抛出NotFoundException

  2. 从哈希中获取联系人。

  3. 返回找到的联系人。

我们方法的源代码如下:

@Override
public Contact findById(Long id) throws NotFoundException {
  if (contactDoesNotExist(id)) {
    throw new NotFoundException("No contact found with id: " + id);
    }
  return buildContact(id);
}

更新

我们可以通过以下步骤更新现有联系人的信息:

  1. 检查该联系人是否存在。如果找不到联系人,则抛出NotFoundException

  2. 将更新后的联系信息保存在哈希中。

  3. 返回更新后的联系人。

update()方法的源代码如下:

@Override
public Contact update(Contact updated) throws NotFoundException {
  if (contactDoesNotExist(updated.getId())) {
    throw new NotFoundException("No contact found with id: " + updated.getId());
  }
  persist(updated);
  return updated;
}

删除

我们可以通过以下步骤删除联系人的信息:

  1. 获取已删除联系人的引用。

注意

我们使用findById()方法,因为如果找不到联系人,它会抛出NotFoundException

  1. 构建已删除联系人的键。

  2. 从我们的联系人集合中删除联系人。

  3. 从哈希中删除联系人的信息。

  4. 返回已删除的联系人。

deleteById()方法的源代码如下:

@Override
public Contact deleteById(Long id) throws NotFoundException {
  Contact deleted = findById(id);
  String key = buildKey(id);

  redisTemplate.opsForSet().remove("contacts", key);

  BoundHashOperations operations = redisTemplate.boundHashOps(key);

  operations.delete("id");
  operations.delete("emailAddress");
  operations.delete("firstName");
  operations.delete("lastName");
  operations.delete("phoneNumber");

  operations.delete("streetAddress");
  operations.delete("postCode");
  operations.delete("postOffice");
  operations.delete("state");
  operations.delete("country");

  return deleted;
}

将数据存储为 JSON

如果我们将对象信息存储在哈希中,我们必须编写大量样板代码,用于保存、读取和删除联系人信息。本小节描述了我们如何减少所需代码量并实现一个以 JSON 格式存储联系人信息的 CRUD 应用程序。这意味着StringRedisSerializer用于序列化字符串值,而JacksonJsonRedisSerializer将我们的Contact对象转换为 JSON。

配置应用程序上下文

我们可以通过以下步骤配置应用程序的应用程序上下文:

  1. 配置值序列化器 bean。

  2. 配置 Redis 模板。

  3. 配置 Redis 原子长整型 bean。

配置值序列化器 bean

我们可以通过向ApplicationContext类添加contactSerializer()方法并用@Bean注解对其进行注释来配置值序列化器 bean。我们可以通过以下步骤实现此方法:

  1. 创建一个新的JacksonJsonRedisSerializer对象,并将Contact类的类型作为构造函数参数传递。

  2. 返回创建的对象。

contactSerializer()方法的源代码如下:

@Bean
public RedisSerializer<Contact> valueSerializer() {
    return new JacksonJsonRedisSerializer<Contact>(Contact.class);
}

配置 Redis 模板 bean

我们可以通过向ApplicationContext类添加redisTemplate()方法,对其进行@Bean注解,并在其实现中配置 Redis 模板来配置 Redis 模板。我们可以按照以下步骤实现此方法:

  1. 创建一个新的RedisTemplate对象,并将我们的键和值的类型作为类型参数。

  2. 设置使用的连接工厂。

  3. 设置使用的值序列化程序。

  4. 返回创建的对象。

redisTemplate()方法的源代码如下所示:

@Bean
public RedisTemplate redisTemplate() {
    RedisTemplate<String, Contact> redisTemplate = new RedisTemplate<String, Contact>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    redisTemplate.setValueSerializer(valueSerializer());

    return redisTemplate;
}

配置 Redis 原子长整型 bean

我们将通过向ApplicationContext类添加redisAtomicLong()方法并使用@Bean注解对其进行注解来开始配置 Redis 原子长整型 bean。我们的下一步是按照以下步骤实现此方法:

  1. 创建一个新的RedisAtomicLong对象。将使用的 Redis 计数器的名称和 Redis 连接工厂作为构造函数参数传递。

  2. 返回创建的对象。

redisAtomicLong()方法的源代码如下所示:

@Bean
public RedisAtomicLong redisAtomicLong() {
    return new RedisAtomicLong("contact", redisConnectionFactory());
}

CRUD

首先,我们必须谈论一下我们的 Redis 数据模型。我们使用两种不同的数据类型将联系人信息存储到 Redis 中。我们将单个联系人的信息存储到 Redis 中作为字符串值。这是有道理的,因为在保存之前,联系人信息会被转换为 JSON 格式。我们还将使用一个包含Contact对象的 JSON 表示的集合。我们必须复制信息,否则我们将无法显示联系人列表。

我们可以通过实现ContactService接口为Contact对象提供 CRUD 操作。让我们开始创建一个虚拟服务实现,并稍后添加或实现实际的 CRUD 操作。创建虚拟服务实现所需的步骤如下所述:

  1. 实现ContactService接口。

  2. @Service注解注释创建的类。

  3. 将所需的依赖项作为创建的类的私有成员添加,并使用@Resource注解对这些成员进行注解。我们需要引用RedisTemplateRedisAtomicLong对象。

我们的虚拟服务实现的源代码如下所示:

@Service
public class RedisContactService implements ContactService {

    @Resource
    private RedisAtomicLong contactIdCounter;

    @Resource
    private RedisTemplate<String, Contact> redisTemplate;

    //Add methods here
}

我们还必须实现一些实用方法,这些方法由ContactService接口声明的方法使用。这些私有方法在以下表中描述:

方法 描述
String buildKey(Long contactId) 返回联系人的键。
void persist(Contact persisted) 将联系人信息保存为字符串值。

首先,我们必须实现一个用于构建持久化Contact对象键的方法。buildKey()方法的实现很简单。我们通过将作为参数给定的联系人 ID 附加到字符串contact并返回结果字符串来构建键。buildKey()方法的源代码如下所示:

private String buildKey(Long contactId) {
    return "contact" + contactId;
}

其次,我们必须实现一个persist()方法来保存联系人信息。我们可以通过执行以下步骤来实现这一点:

  1. 如果联系人 ID 为空,则获取新 ID 并将接收到的Long对象设置为Contact对象的 ID。

  2. 为联系人创建一个键。

  3. 将联系人信息保存为字符串值。

注意

我们使用值操作,因为我们只需要执行一个操作。

persist()方法的源代码如下所示:

private void persist(Contact persisted) {
  Long id = persisted.getId();
  if (id == null) {
      id = contactIdCounter.incrementAndGet();
      persisted.setId(id);
    }
  String key = buildKey(persisted.getId());
  redisTemplate.opsForValue().set(key, persisted);
}

我们现在准备开始为联系人实现 CRUD 操作。让我们继续并找出如何完成。

创建

我们可以通过以下步骤实现一个添加新联系人的方法:

  1. 保存添加的联系人。

  2. 将联系人信息添加到联系人集合中。

  3. 返回添加的联系人。

add()方法的源代码如下所示:

@Override
public Contact add(Contact added) {
    persist(added);
    redisTemplate.opsForSet().add("contacts", added);
    return added;
}

读取

我们的应用程序有两个视图,显示联系人信息:第一个显示联系人列表,第二个显示单个联系人的信息。

首先,我们必须实现一个从 Redis 获取所有联系人的方法。我们可以按照以下步骤实现findAll()方法:

  1. 从联系人集合中获取所有联系人。

  2. 创建一个新的ArrayList对象并返回该对象。

findAll()方法的源代码如下:

@Override
public List<Contact> findAll() {
    Collection<Contact> contacts = redisTemplate.opsForSet().members("contacts");
    return new ArrayList<Contact>(contacts);
}

其次,我们必须实现一个返回单个联系人信息的方法。我们的findById()方法的实现包括以下步骤:

  1. 创建联系人的键。

  2. 从 Redis 获取Contact对象。

  3. 如果未找到联系人,则抛出NotFoundException

  4. 返回找到的对象。

findById()方法的源代码如下:

@Override
public Contact findById(Long id) throws NotFoundException {
    String key = buildKey(id);
    Contact found = redisTemplate.opsForValue().get(key);

    if (found == null) {
        throw new NotFoundException("No contact found with id: {}" + id);
    }

    return found;
}

更新

我们可以按照以下步骤更新现有联系人的信息:

  1. 从 Redis 获取旧的联系人信息。

  2. 保存更新后的联系人信息。

  3. 从联系人集合中删除旧的联系人信息。这样可以确保我们的集合不包含相同联系人的重复条目。

  4. 将更新后的联系人信息添加到联系人集合中。

  5. 返回更新后的联系人。

update()方法的源代码如下:

@Override
public Contact update(Contact updated) throws NotFoundException {
    Contact old = findById(updated.getId());

    persist(updated);
    redisTemplate.opsForSet().remove("contacts", old);
    redisTemplate.opsForSet().add("contacts", updated);

    return updated;
}

删除

我们可以按照以下步骤删除联系人信息:

  1. 通过调用findById()方法找到已删除的联系人。这样可以确保如果联系人未找到,则会抛出NotFoundException

  2. 构建用于获取联系人信息的键。

  3. 从联系人集合中删除已删除的联系人。

  4. 删除已删除联系人的 JSON 表示。

  5. 返回已删除的联系人。

delete()方法的源代码如下:

@Override
public Contact deleteById(Long id) throws NotFoundException {
    Contact deleted = findById(id);

    String key = buildKey(id);
    redisTemplate.opsForSet().remove("contacts", deleted);
    redisTemplate.opsForValue().set(key, null);

    return deleted;
}

发布/订阅消息模式

Redis 还包括发布/订阅消息模式的实现。本节演示了我们如何使用 Spring Data Redis 来发送和接收消息。例如,我们将修改将联系人信息存储为 JSON 的 CRUD 应用程序,以便在添加新联系人时发送通知,更新联系人信息以及删除联系人时发送通知。

我们可以通过执行以下步骤来实现此要求:

  1. 创建处理接收到的消息的消息监听器。

  2. 配置我们应用程序的应用程序上下文。

  3. 使用RedisTemplate类发送消息。

本节还描述了我们如何确保我们的实现工作正常。

创建消息监听器

使用 Spring Data Redis 创建消息监听器有两种方法:我们可以实现MessageListener接口,或者我们可以创建一个 POJO 消息监听器并使用MessageListenerAdapter类将消息委派给它。这两种方法都在本小节中讨论。

实现 MessageListener 接口

创建消息监听器的第一种方法是实现MessageListener接口。我们的实现包括以下步骤:

  1. 创建一个用于记录接收到的消息的新Logger对象。

  2. 创建一个用于将字节数组转换为String对象的新StringRedisSerializer对象。

  3. 实现MessageListener接口声明的onMessage()方法。此方法简单地记录接收到的消息。

ContactListener类的源代码如下:

public class ContactMessageListener implements MessageListener {

    private final static Logger LOGGER = LoggerFactory.getLogger(ContactMessageListener.class);

    private RedisSerializer<String> stringSerializer = new StringRedisSerializer();

    @Override
    public void onMessage(Message message, byte[] pattern) {
        LOGGER.debug("MessageListener - received message: {} on channel: {}", stringSerializer.deserialize(message.getBody()), stringSerializer.deserialize(message.getChannel()));
    }
}

创建一个 POJO 消息监听器

创建消息监听器的第二种方法是创建一个普通的 Java 类。我们可以按照以下步骤来做到这一点:

  1. 创建一个用于记录接收到的消息的新Logger对象。

  2. 创建一个名为handleMessage()的消息处理方法,该方法接受Contact对象和String对象作为参数。

  3. 实现handleMessage()方法。此方法记录接收到的消息。

ContactPOJOMessageListener类的源代码如下:

public class ContactPOJOMessageListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(ContactPOJOMessageListener.class);

    public void handleMessage(Contact contact, String channel) {
        LOGGER.debug("Received contact: {} on channel: {}", contact, channel);
    }
}

配置应用程序上下文

我们必须对应用程序上下文配置进行以下更改:

  1. 配置消息监听器 bean。

  2. 配置消息监听适配器 bean。

  3. 配置消息监听器容器 bean。

配置消息监听器 bean

首先,我们必须配置我们的消息监听器 bean。配置相当简单。我们只需创建新的消息监听器对象并返回创建的对象。消息监听器 bean 配置的源代码如下:

@Bean
public ContactMessageListener contactMessageListener() {
    return new ContactMessageListener();
}

@Bean
public ContactPOJOMessageListener contactPOJOMessageListener() {
    return new ContactPOJOMessageListener();
}

配置消息监听器适配器 bean

接下来,我们必须配置消息监听器适配器 bean,该 bean 用于将消息转发给我们的 POJO 消息监听器。我们可以通过以下步骤配置此 bean:

  1. 创建一个新的MessageListenerAdapter对象,并将ContactPOJOMessageListener对象作为构造函数参数传递。

  2. 设置用于将接收到的消息转换为Contact对象的序列化器。

  3. 返回创建的对象。

messageListenerAdapter()方法的源代码如下:

@Bean
public MessageListenerAdapter messageListenerAdapter() {
    MessageListenerAdapter adapter = new MessageListenerAdapter(contactPOJOMessageListener());
    adapter.setSerializer(contactSerializer());
    return adapter;
}

注意

MessageListenerAdapter类的defaultListenerMethod属性用于配置消息处理程序方法的名称。此属性的默认值为handleMessage

配置消息监听器容器 bean

消息监听器容器是一个组件,它监听通过不同通道发送的消息,并将这些消息转发给注册的消息监听器。我们可以通过以下步骤配置此组件:

  1. 创建一个新的RedisMessageListenerContainer对象。

  2. 设置已使用的 Redis 连接工厂。

  3. 注册消息监听器并指定订阅的通道。

  4. 返回创建的对象。

我们的配置源代码如下:

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();

    container.setConnectionFactory(redisConnectionFactory());
    container.addMessageListener(messageListenerAdapter(), 
    Arrays.asList(
            new ChannelTopic("newContacts"),
            new ChannelTopic("updatedContacts"),
            new ChannelTopic("removedContacts")
    ));
    container.addMessageListener(contactMessageListener(), 
    Arrays.asList(
            new ChannelTopic("newContacts"),
            new ChannelTopic("updatedContacts"),
            new ChannelTopic("removedContacts")
    ));

    return container;
}

使用 RedisTemplate 发送消息

我们可以使用RedisTemplate类的convertAndSend(String channel, Object message)方法向不同的通道发送发布消息。本小节描述了如何使用此方法发送有关新联系人、更新联系人和删除联系人的通知。

创建

为了发送有关新联系人的更改通知,我们必须修改RedisContactService类的add()方法,在成功保存新联系人信息后调用RedisTemplate类的convertAndSend()方法。我们的新add()方法的源代码如下:

@Override
public Contact add(Contact added) {
    persist(added);
    redisTemplate.opsForSet().add("contacts", added);
 redisTemplate.convertAndSend("newContacts", added);

    return added;
}

更新

通过修改RedisContactService类的update()方法,我们可以发送关于更新联系人的通知。在联系信息更新后,我们只需调用RedisTemplate类的convertAndSend()方法。新update()方法的源代码如下:

@Override
public Contact update(Contact updated) throws NotFoundException {
    Contact old = findById(updated.getId());

    persist(updated);
    redisTemplate.opsForSet().remove("contacts", old);
    redisTemplate.opsForSet().add("contacts", updated);
 redisTemplate.convertAndSend("updatedContacts", updated);

    return updated;
}

删除

通过对RedisContactService类的deleteById()方法进行小修改,我们可以发送有关已删除联系人的通知。在联系信息被删除后,我们将调用RedisTemplate类的convertAndSend()方法,该方法发送通知消息。修改后的deleteById()方法的源代码如下:

@Override
public Contact deleteById(Long id) throws NotFoundException {
    Contact deleted = findById(id);

    String key = buildKey(id);

    redisTemplate.opsForSet().remove("contacts", deleted);
    redisTemplate.opsForValue().set(key, null);
 redisTemplate.convertAndSend("removedContacts", deleted);

    return deleted;
}

验证所需的行为

我们现在已经实现了我们的消息监听器,并修改了我们的应用程序,以便在联系信息更改时每次发送通知消息。我们的下一步是验证我们的实现是否按预期工作。

我们可以通过更改联系信息并确保我们的消息监听器写入应用程序日志中的日志行来确认这一点。当添加新联系人时写入的日志行如下:

DEBUG - ContactMessageListener     - Received message: {"id":9,"address":{"country":"","streetAddress":"","postCode":"","postOffice":"","state":""},"emailAddress":"","firstName":"Foo","lastName":"Bar","phoneNumber":""} on channel: newContacts
DEBUG - ContactPOJOMessageListener - Received contact: com.packtpub.springdata.redis.model.Contact@543d8ee8[id=9,address=com.packtpub.springdata.redis.model.Address@15714c8d[country=,streetAddress=,postCode=,postOffice=,state=],emailAddress=,firstName=Foo,lastName=Bar,phoneNumber=] on channel: null

注意

请注意,传递给 POJO 消息处理程序的通道信息始终为null。这是 Spring Data Redis 的已知错误。有关更多信息,请访问jira.springsource.org/browse/DATAREDIS-98

使用 Spring Data Redis 的 Spring 缓存抽象

Spring Framework 3.1 的缓存抽象将缓存应用于 Java 方法。当调用缓存的方法时,缓存抽象将从缓存中检查该方法是否早期使用相同的参数调用过。如果是这种情况,则从缓存中获取返回值,并且不执行该方法。否则,执行该方法并将其返回值存储在缓存中。

注意

Spring Framework 3.1 的缓存抽象在static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/cache.html中有更详细的解释。

Spring Data Redis 提供了 Spring 缓存抽象的实现。使用 Redis 作为缓存比使用 Ehcache 等本地缓存实现有两个好处:

  • 它可以作为一个集中的缓存,被运行我们的应用程序的每个 servlet 容器或应用程序服务器共享。这减少了数据库查询的总体数量,减少了数据库服务器的负载,并提高了所有服务器的性能。

  • 缓存不会被清空,直到我们清空它。这意味着我们可以重新启动我们的 servlet 容器或应用程序服务器,而不会丢失缓存中存储的信息。在服务器重新启动后,它可以立即充分利用缓存的信息。无需预热缓存。

本节描述了我们如何使用 Spring Data Redis 来为使用 JPA Criteria API 的应用程序添加缓存支持。该应用程序最初是在第三章中介绍的,使用 Spring Data JPA 构建查询。我们缓存示例的要求如下:

  • 从数据库中查找单个联系人信息的方法调用必须被缓存

  • 当联系人的信息被更新时,缓存中存储的信息也必须更新

  • 当联系人被删除时,必须从缓存中删除已删除的联系人

我们可以通过以下步骤为我们的示例应用程序添加缓存支持:

  1. 配置 Spring 缓存抽象。

  2. 识别缓存的方法。

我们还将学习如何验证 Spring 缓存抽象是否正常工作。

配置 Spring 缓存抽象

我们可以通过对应用程序的应用程序上下文配置进行以下更改来配置 Spring 缓存抽象:

  1. 启用缓存注解。

  2. 在所使用的属性文件中配置所使用的 Redis 实例的主机和端口。

  3. 配置 Redis 连接工厂 bean。

  4. 配置 Redis 模板 bean。

  5. 配置缓存管理器 bean。

启用缓存注解

我们可以通过使用@EnableCaching注解来注解我们的应用程序上下文配置类来启用缓存注解。ApplicationContext类的相关部分如下所示:

@Configuration
@ComponentScan(basePackages = {
        "com.packtpub.springdata.jpa.controller",
        "com.packtpub.springdata.jpa.service"
})
@EnableCaching
@EnableTransactionManagement
@EnableWebMvc
@EnableJpaRepositories("com.packtpub.springdata.jpa.repository")
@PropertySource("classpath:application.properties")
public class ApplicationContext extends WebMvcConfigurerAdapter {

    @Resource
    private Environment env;

    //Bean declarations
}

配置所使用的 Redis 实例的主机和端口

为了配置所使用的 Redis 实例的主机和端口,我们必须在application.properties文件中添加以下行:

redis.host = localhost
redis.port = 6379

配置 Redis 连接工厂 bean

我们可以通过在ApplicationContext类中添加一个redisConnectionFactory()方法,并用@Bean注解对该方法进行注解来配置 Redis 连接工厂 bean。我们可以通过以下步骤实现这个方法:

  1. 创建一个新的JedisConnectionFactory对象。

  2. 配置所使用的 Redis 实例的主机和端口。

  3. 返回创建的对象。

给出redisConnectionFactory()方法的源代码如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
    JedisConnectionFactory cf = new JedisConnectionFactory();

    cf.setHostName(env.getRequiredProperty("redis.host")); cf.setPort(Integer.parseInt(env.getRequiredProperty("redis.port")));

    return cf;
}

配置 Redis 模板 bean

为了配置 Redis 模板 bean,我们必须在ApplicationContext类中添加一个redisTemplate()方法,并用@Bean注解对该方法进行注解。我们对这个方法的实现包括以下步骤:

  1. 创建一个新的RedisTemplate对象。

  2. 设置使用的 Redis 连接工厂。

  3. 返回创建的对象。

redisTemplate()方法的源代码如下:

@Bean
public RedisTemplate redisTemplate() {
    RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());

    return redisTemplate;
}

配置缓存管理器 bean

我们的最后一步是配置缓存管理器 bean。我们可以通过在ApplicationContext类中添加cacheManager()方法,并用@Bean注解对此方法进行注释来实现这一点。我们可以通过以下步骤实现这个方法:

  1. 创建一个新的RedisCacheManager对象,并将使用的 Redis 模板作为构造函数参数。

  2. 返回创建的对象。

cacheManager()方法的源代码如下:

@Bean
public RedisCacheManager cacheManager() {
    return new RedisCacheManager(redisTemplate());
}

识别缓存方法

我们现在已经配置了 Spring 缓存抽象,并且准备好识别缓存方法。本小节描述了我们如何在缓存中添加联系信息,更新已经存储在缓存中的联系信息,并从缓存中删除联系信息。

将联系信息添加到缓存

为了将联系信息添加到缓存中,我们必须缓存RepositoryContactService类的findById()方法的方法调用。我们可以通过使用@Cacheable注解对方法进行注释并提供缓存的名称来实现这一点。这告诉缓存抽象应该使用提供的 ID 作为键,将返回的联系人添加到contacts缓存中。findById()方法的源代码如下:

@Cacheable("contacts")
@Transactional(readOnly = true)
@Override
public Contact findById(Long id) throws NotFoundException {
    //Implementation remains unchanged.
}

将联系信息更新到缓存

我们可以通过在RepositoryContactService类的update()方法上注释@CachePut注解来更新存储在缓存中的联系信息。我们还必须提供缓存的名称,并指定当此方法的返回值更新到缓存时,ContactDTO对象的id属性将被用作键。update()方法的源代码如下:

@CachePut(value = "contacts", key="#p0.id")
@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact update(ContactDTO updated) throws NotFoundException {
    //Implementation remains unchanged.
}

从缓存中删除联系信息

我们可以通过在deleteById()方法上注释@CacheEvict注解并提供缓存的名称作为其值来从缓存中删除联系信息。这意味着在方法执行后,缓存抽象会从缓存中删除已删除的联系人。被删除的联系人由作为方法参数给定的 ID 标识。deleteById()方法的源代码如下:

@CacheEvict("contacts")
@Transactional(rollbackFor = NotFoundException.class)
@Override
public Contact deleteById(Long id) throws NotFoundException {
  //Implementation remains unchanged
}

验证 Spring 缓存抽象是否正常工作

我们现在已经成功地将缓存添加到我们的示例应用程序中。我们可以通过使用缓存方法并查找我们应用程序的日志文件中的以下行来验证 Spring 缓存抽象是否正常工作:

DEBUG - RedisConnectionUtils       - Opening Redis Connection
DEBUG - RedisConnectionUtils       - Closing Redis Connection

如果在日志文件中找到这些行,可能意味着:

  • 从缓存中获取联系信息而不是使用的数据库

  • 联系信息已更新到缓存

  • 联系信息已从缓存中删除

总结

在本章中,我们已经学到:

  • 设计 Redis 数据模型与设计关系数据库的数据模型完全不同

  • 我们可以将 Redis 用作 Web 应用程序的数据存储

  • Spring Data Redis 与 Redis 发布/订阅实现提供了清晰的集成

  • 我们可以通过使用 Spring 缓存抽象将 Redis 作为我们应用程序的集中式缓存来使用

posted @ 2024-05-24 10:55  绝不原创的飞龙  阅读(111)  评论(0编辑  收藏  举报