前言

Spring Data Commons 项目将核心 Spring 概念应用于使用许多关系型和非关系型数据存储的解决方案开发。

1. 项目元数据

参考文档

2. 使用 Spring Data 仓库

Spring Data 仓库抽象的目标是显著减少实现各种持久化存储的数据访问层所需的样板代码量。

Spring Data 仓库文档和您的模块

本章解释了 Spring Data 仓库的核心概念和接口。本章中的信息来自 Spring Data Commons 模块。它使用 Java 持久化 API (JPA) 模块的配置和代码示例。将 XML 命名空间声明和要扩展的类型调整为您正在使用的特定模块的等效项。命名空间参考涵盖了所有支持仓库 API 的 Spring Data 模块都支持的 XML 配置,仓库查询关键字涵盖了仓库抽象普遍支持的查询方法关键字。有关模块特定功能的详细信息,请参阅本文档中该模块的章节。

2.1. 核心概念

Spring Data 仓库抽象中的核心接口是Repository(可能并不令人意外)。它以要管理的域类以及域类的 ID 类型作为类型参数。此接口主要充当标记接口,用于捕获要处理的类型并帮助您发现扩展此接口的接口。CrudRepository 为正在管理的实体类提供了复杂的 CRUD 功能。

示例 1. CrudRepository 接口
public interface CrudRepository<T, ID extends Serializable>
    extends Repository<T, ID> {

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

    T findOne(ID primaryKey);       (2)

    Iterable<T> findAll();          (3)

    Long count();                   (4)

    void delete(T entity);          (5)

    boolean exists(ID primaryKey);  (6)

    // … more functionality omitted.
}
1 保存给定的实体。
2 返回由给定 ID 标识的实体。
3 返回所有实体。
4 返回实体的数量。
5 删除给定的实体。
6 指示是否存在具有给定 ID 的实体。
我们还提供特定于持久化技术的抽象,例如JpaRepositoryMongoRepository。这些接口扩展CrudRepository,除了相当通用的持久化技术无关接口(例如CrudRepository)之外,还公开了底层持久化技术的 capabilities。

CrudRepository之上,有一个PagingAndSortingRepository抽象,它添加了其他方法来简化对实体的分页访问。

示例 2. PagingAndSortingRepository
public interface PagingAndSortingRepository<T, ID extends Serializable>
  extends CrudRepository<T, ID> {

  Iterable<T> findAll(Sort sort);

  Page<T> findAll(Pageable pageable);
}

通过页面大小为 20 访问User的第二页,您可以简单地执行以下操作

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

除了查询方法外,还提供了针对计数和删除查询的查询派生。

示例 3. 派生计数查询
public interface UserRepository extends CrudRepository<User, Long> {

  Long countByLastname(String lastname);
}
示例 4. 派生删除查询
public interface UserRepository extends CrudRepository<User, Long> {

  Long deleteByLastname(String lastname);

  List<User> removeByLastname(String lastname);

}

2.2. 查询方法

标准 CRUD 功能仓库通常对底层数据存储有查询。使用 Spring Data,声明这些查询成为一个四步过程

  1. 声明一个扩展 Repository 或其子接口的接口,并将其类型化为它将处理的域类和 ID 类型。

    interface PersonRepository extends Repository<User, Long> {  }
  2. 在接口上声明查询方法。

    interface PersonRepository extends Repository<User, Long> {
      List<Person> findByLastname(String lastname);
    }
  3. 设置 Spring 以创建这些接口的代理实例。可以通过JavaConfig

    import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
    
    @EnableJpaRepositories
    class Config {}

    或通过XML 配置

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

    本示例中使用了 JPA 命名空间。如果您将仓库抽象用于任何其他存储,则需要将其更改为存储模块的相应命名空间声明,该声明应将jpa替换为例如mongodb。此外,请注意,JavaConfig 变体不会显式配置包,因为注释类的包默认为使用。要自定义要扫描的包

  4. 获取注入的仓库实例并使用它。

    public class SomeClient {
    
      @Autowired
      private PersonRepository repository;
    
      public void doSomething() {
        List<Person> persons = repository.findByLastname("Matthews");
      }
    }

以下部分将详细解释每个步骤。

2.3. 定义仓库接口

第一步是定义特定于域类的仓库接口。该接口必须扩展 Repository 并将其类型化为域类和 ID 类型。如果要公开该域类型的 CRUD 方法,请扩展CrudRepository而不是Repository

2.3.1. 微调仓库定义

通常,您的仓库接口将扩展RepositoryCrudRepositoryPagingAndSortingRepository。或者,如果您不想扩展 Spring Data 接口,您也可以使用@RepositoryDefinition注释您的仓库接口。扩展CrudRepository公开了操作实体的完整方法集。如果您希望选择要公开的方法,只需将您想要公开的方法从CrudRepository复制到您的域仓库即可。

这允许您在提供的 Spring Data 仓库功能之上定义自己的抽象。
示例 5. 选择性公开 CRUD 方法
@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {

  T findOne(ID id);

  T save(T entity);
}

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

在此第一步中,您为所有域仓库定义了一个通用的基本接口,并公开了findOne(…)以及save(…)。这些方法将路由到您选择的存储的仓库基本实现中,例如在 JPA SimpleJpaRepository的情况下,因为它们与CrudRepository中的方法签名匹配。因此,UserRepository现在将能够保存用户,并通过 ID 查找单个用户,以及触发查询以通过其电子邮件地址查找Users

请注意,中间仓库接口使用@NoRepositoryBean进行注释。确保将此注释添加到 Spring Data 不应在运行时为其创建实例的所有仓库接口。

2.4. 定义查询方法

仓库代理有两种方法可以从方法名称派生特定于存储的查询。它可以直接从方法名称派生查询,或者使用手动定义的查询。可用选项取决于实际的存储。但是,必须有一种策略来决定创建哪个实际查询。让我们看看可用的选项。

2.4.1. 查询查找策略

仓库基础架构可以使用以下策略来解析查询。您可以在命名空间中通过query-lookup-strategy属性(在 XML 配置的情况下)或通过Enable${store}Repositories注释的queryLookupStrategy属性(在 Java 配置的情况下)配置策略。某些策略可能不受特定数据存储的支持。

  • CREATE尝试从查询方法名称构造特定于存储的查询。一般方法是从方法名称中删除一组给定的众所周知的 prefix,并解析其余方法。阅读更多关于查询创建中的查询构建。

  • USE_DECLARED_QUERY尝试查找声明的查询,如果找不到则会抛出异常。查询可以通过某个地方的注释定义或通过其他方式声明。请查阅特定存储的文档以查找该存储的可用选项。如果仓库基础架构在引导时找不到该方法的声明查询,则会失败。

  • CREATE_IF_NOT_FOUND(默认值)结合了CREATEUSE_DECLARED_QUERY。它首先查找声明的查询,如果找不到声明的查询,则会创建一个自定义基于方法名称的查询。这是默认查找策略,因此如果您没有显式配置任何内容,则将使用它。它允许通过方法名称快速定义查询,但也允许通过根据需要引入声明的查询来自定义调整这些查询。

2.4.2. 查询创建

Spring Data 存储库基础设施内置的查询构建器机制可用于构建对存储库实体的约束查询。该机制会从方法中去除前缀find…Byread…Byquery…Bycount…Byget…By,并开始解析方法的其余部分。引入子句可以包含其他表达式,例如Distinct,用于在要创建的查询上设置 distinct 标志。但是,第一个By充当分隔符,指示实际条件的开始。在最基本的层面上,您可以定义实体属性上的条件,并使用AndOr将它们连接起来。

示例 6. 从方法名称创建查询
public interface PersonRepository extends Repository<User, Long> {

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

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

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

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

解析方法的实际结果取决于您为其创建查询的持久化存储。但是,有一些一般性的事项需要注意。

  • 表达式通常是属性遍历与可以连接的操作符的组合。您可以将属性表达式与ANDOR组合。您还可以获得对操作符的支持,例如BetweenLessThanGreaterThanLike,用于属性表达式。支持的操作符可能因数据存储而异,因此请查阅参考文档的相应部分。

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

  • 您可以通过将OrderBy子句附加到引用属性的查询方法并提供排序方向(AscDesc)来应用静态排序。要创建支持动态排序的查询方法,请参阅特殊参数处理

2.4.3. 属性表达式

属性表达式只能引用托管实体的直接属性,如前面的示例所示。在查询创建时,您已经确保解析的属性是托管域类的属性。但是,您也可以通过遍历嵌套属性来定义约束。假设一个Person有一个Address,其中包含一个ZipCode。在这种情况下,方法名称为

List<Person> findByAddressZipCode(ZipCode zipCode);

创建属性遍历x.address.zipCode。解析算法首先将整个部分(AddressZipCode)解释为属性,并在域类中查找具有该名称(未大写)的属性。如果算法成功,则使用该属性。否则,算法将源代码在右侧的驼峰式部分处拆分为头部和尾部,并尝试查找相应的属性,在本例中为AddressZipCode。如果算法找到具有该头部名称的属性,则获取尾部并继续从那里向下构建树,以刚刚描述的方式将尾部拆分。如果第一次拆分不匹配,算法会将拆分点向左移动(AddressZipCode)并继续。

尽管这应该适用于大多数情况,但算法可能会选择错误的属性。假设Person类也具有addressZip属性。算法将在第一轮拆分中匹配,并且实际上会选择错误的属性,最终失败(因为addressZip的类型可能没有code属性)。

为了解决这种歧义,您可以在方法名称中使用_来手动定义遍历点。因此,我们的方法名称最终将如下所示

List<Person> findByAddress_ZipCode(ZipCode zipCode);

如果您的属性名称包含下划线(例如first_name),则可以在方法名称中使用第二个下划线转义下划线。对于first_name属性,查询方法必须命名为findByFirst__name(…)

2.4.4. 特殊参数处理

要在查询中处理参数,您只需像上面示例中看到的那样定义方法参数即可。除此之外,基础设施还会识别某些特定类型,例如PageableSort,以动态地将分页和排序应用于您的查询。

示例 7. 在查询方法中使用 Pageable、Slice 和 Sort
Page<User> findByLastname(String lastname, Pageable pageable);

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

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

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

第一个方法允许您将org.springframework.data.domain.Pageable实例传递给查询方法,以动态地将分页添加到您静态定义的查询中。一个Page了解可用的元素和页面的总数。它通过基础设施触发计数查询来计算总数来做到这一点。由于这可能因使用的存储而异而代价高昂,因此可以使用Slice作为替代返回。一个Slice只知道是否存在下一个Slice可用,当遍历更大的结果集时,这可能就足够了。

排序选项也通过Pageable实例处理。如果您只需要排序,只需将org.springframework.data.domain.Sort参数添加到您的方法中即可。正如您所看到的,简单地返回一个List也是可能的。在这种情况下,构建实际Page实例所需的额外元数据将不会创建(这反过来意味着不会发出必要的额外计数查询),而是简单地限制查询以仅查找给定范围内的实体。

要找出查询的总页数,您必须触发一个额外的计数查询。默认情况下,此查询将从您实际触发的查询派生。

2.5. 创建存储库实例

在本节中,您将为定义的存储库接口创建实例和 bean 定义。一种方法是使用随每个支持存储库机制的 Spring Data 模块一起提供的 Spring 命名空间,尽管我们通常建议使用 Java-Config 样式配置。

2.5.1. XML 配置

每个 Spring Data 模块都包含一个 repositories 元素,允许您简单地定义 Spring 为您扫描的基本包。

示例 8. 通过 XML 启用 Spring Data 存储库
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

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

</beans:beans>

在前面的示例中,Spring 被指示扫描com.acme.repositories及其所有子包,以查找扩展Repository或其子接口的接口。对于找到的每个接口,基础设施都会注册特定于持久化技术的FactoryBean,以创建处理查询方法调用的适当代理。每个 bean 都在派生自接口名称的 bean 名称下注册,因此UserRepository的接口将在userRepository下注册。base-package属性允许使用通配符,以便您可以定义要扫描的包的模式。

使用过滤器

默认情况下,基础设施会获取位于配置的基本包下扩展特定于持久化技术的Repository子接口的每个接口,并为其创建 bean 实例。但是,您可能希望更细粒度地控制为哪些接口创建 bean 实例。为此,您可以在<repositories />内部使用<include-filter /><exclude-filter />元素。语义与 Spring 上下文命名空间中的元素完全相同。有关详细信息,请参阅Spring 参考文档中关于这些元素的内容。

例如,要排除某些接口作为存储库实例化,您可以使用以下配置

示例 9. 使用 exclude-filter 元素
<repositories base-package="com.acme.repositories">
  <context:exclude-filter type="regex" expression=".*SomeRepository" />
</repositories>

此示例排除了所有以SomeRepository结尾的接口被实例化。

2.5.2. JavaConfig

存储库基础设施也可以使用 JavaConfig 类上的特定于存储的@Enable${store}Repositories注解来触发。有关 Spring 容器基于 Java 的配置的介绍,请参阅参考文档。[1]

启用 Spring Data 存储库的示例配置如下所示。

示例 10. 基于注解的存储库配置示例
@Configuration
@EnableJpaRepositories("com.acme.repositories")
class ApplicationConfiguration {

  @Bean
  public EntityManagerFactory entityManagerFactory() {
    // …
  }
}
此示例使用特定于 JPA 的注解,您需要根据实际使用的存储模块进行更改。这同样适用于EntityManagerFactory bean 的定义。请查阅涵盖特定于存储的配置的部分。

2.5.3. 独立使用

您也可以在 Spring 容器之外使用存储库基础设施,例如在 CDI 环境中。您仍然需要类路径中的一些 Spring 库,但通常您也可以以编程方式设置存储库。提供存储库支持的 Spring Data 模块附带一个特定于持久化技术的 RepositoryFactory,您可以按如下方式使用它。

示例 11. 存储库工厂的独立使用
RepositoryFactorySupport factory =  // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

2.6. Spring Data 存储库的自定义实现

通常需要为一些存储库方法提供自定义实现。Spring Data 存储库允许您轻松地提供自定义存储库代码,并将其与通用 CRUD 抽象和查询方法功能集成。

2.6.1. 向单个存储库添加自定义行为

要使用自定义功能丰富存储库,首先定义一个接口和一个自定义功能的实现。使用您提供的存储库接口扩展自定义接口。

示例 12. 自定义存储库功能的接口
interface UserRepositoryCustom {
  public void someCustomMethod(User user);
}
示例 13. 自定义存储库功能的实现
class UserRepositoryImpl implements UserRepositoryCustom {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}
要找到该类,最重要的一点是它与核心存储库接口相比,名称的后缀为Impl(请参见下文)。

实现本身不依赖于 Spring Data,并且可以是普通的 Spring bean。因此,您可以使用标准的依赖注入行为来注入对其他 bean(如 JdbTemplate)的引用,参与方面等等。

示例 14. 对您的基本存储库接口的更改
interface UserRepository extends CrudRepository<User, Long>, UserRepositoryCustom {

  // Declare query methods here
}

让您的标准存储库接口扩展自定义接口。这样做可以结合 CRUD 和自定义功能,并使客户端可以使用它。

配置

如果您使用命名空间配置,则存储库基础设施会尝试通过扫描我们在其中找到存储库的包下方的类来自动检测自定义实现。这些类需要遵循在找到的存储库接口名称后附加命名空间元素的属性repository-impl-postfix的命名约定。此后缀默认为Impl

示例 15. 配置示例
<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="FooBar" />

第一个配置示例将尝试查找一个类com.acme.repository.UserRepositoryImpl来充当自定义存储库实现,而第二个示例将尝试查找com.acme.repository.UserRepositoryFooBar

手动连接

如果您的自定义实现仅使用基于注解的配置和自动连接,则刚刚显示的方法效果很好,因为它将被视为任何其他 Spring bean。如果您的自定义实现 bean 需要特殊连接,则只需声明该 bean 并根据刚刚描述的约定为其命名。然后,基础设施将按名称引用手动定义的 bean 定义,而不是自己创建一个。

示例 16. 自定义实现的手动连接
<repositories base-package="com.acme.repository" />

<beans:bean id="userRepositoryImpl" class="…">
  <!-- further configuration -->
</beans:bean>

2.6.2. 向所有存储库添加自定义行为

当您想要向所有存储库接口添加单个方法时,前面的方法不可行。

  1. 要向所有存储库添加自定义行为,首先添加一个中间接口来声明共享行为。

    示例 17. 声明自定义共享行为的接口
    public interface MyRepository<T, ID extends Serializable>
      extends JpaRepository<T, ID> {
    
      void sharedCustomMethod(ID id);
    }
  2. 现在,您的各个存储库接口将扩展此中间接口,而不是 Repository 接口,以包含声明的功能。

  3. 接下来,创建中间接口的实现,该实现扩展特定于持久化技术的存储库基类。然后,此类将充当存储库代理的自定义基类。

    示例 18. 自定义存储库基类

    public class MyRepositoryImpl<T, ID extends Serializable>
      extends SimpleJpaRepository<T, ID> implements MyRepository<T, ID> {
    
      private EntityManager entityManager;
    
      // There are two constructors to choose from, either can be used.
      public MyRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
    
        // This is the recommended method for accessing inherited class dependencies.
        this.entityManager = entityManager;
      }
    
      public void sharedCustomMethod(ID id) {
        // implementation goes here
      }
    }

    Spring 的<repositories />命名空间的默认行为是为base-package下所有接口提供实现。这意味着,如果保持其当前状态,Spring 将创建 MyRepository 的实现实例。当然,这不是我们想要的,因为它只应该充当 Repository 和您希望为每个实体定义的实际 Repository 接口之间的中介。要排除将扩展 Repository 的接口实例化为 Repository 实例,您可以使用 @NoRepositoryBean 注解它,或者将其移出配置的base-package

  4. 然后创建一个自定义 Repository 工厂来替换默认的 RepositoryFactoryBean,后者将依次生成一个自定义 RepositoryFactory。新的 Repository 工厂将提供您的 MyRepositoryImpl 作为扩展 Repository 接口的任何接口的实现,替换您刚刚扩展的 SimpleJpaRepository 实现。

    示例 19. 自定义 Repository 工厂 Bean
    public class MyRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable>
      extends JpaRepositoryFactoryBean<R, T, I> {
    
      protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    
        return new MyRepositoryFactory(entityManager);
      }
    
      private static class MyRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {
    
        private EntityManager entityManager;
    
        public MyRepositoryFactory(EntityManager entityManager) {
          super(entityManager);
    
          this.entityManager = entityManager;
        }
    
        protected Object getTargetRepository(RepositoryMetadata metadata) {
    
          return new MyRepositoryImpl<T, I>((Class<T>) metadata.getDomainClass(), entityManager);
        }
    
        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
    
          // The RepositoryMetadata can be safely ignored, it is used by the JpaRepositoryFactory
          //to check for QueryDslJpaRepository's which is out of scope.
          return MyRepository.class;
        }
      }
    }
  5. 最后,要么直接声明自定义工厂的 Bean,要么使用 Spring 命名空间的factory-class属性来告诉 Repository 基础结构使用您的自定义工厂实现。

    示例 20. 使用命名空间中的自定义工厂
    <repositories base-package="com.acme.repository"
      factory-class="com.acme.MyRepositoryFactoryBean" />

2.7. Spring Data 扩展

本节记录了一组 Spring Data 扩展,这些扩展可以在各种上下文中启用 Spring Data 的使用。目前大多数集成都针对 Spring MVC。

2.7.1. Web 支持

本节包含 Spring Data Web 支持的文档,因为它是在 Spring Data Commons 1.6 版本中实现的。由于新引入的支持改变了很多东西,因此我们将之前行为的文档保留在旧版 Web 支持中。

如果模块支持 Repository 编程模型,则 Spring Data 模块附带各种 Web 支持。与 Web 相关的功能需要类路径上的 Spring MVC JAR 文件,其中一些甚至提供了与 Spring HATEOAS 的集成[2]。通常,通过在您的 JavaConfig 配置类中使用@EnableSpringDataWebSupport注解来启用集成支持。

示例 21. 启用 Spring Data Web 支持
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration { }

@EnableSpringDataWebSupport注解注册了一些稍后我们将讨论的组件。它还将在类路径上检测 Spring HATEOAS,如果存在,也将为其注册集成组件。

或者,如果您使用 XML 配置,请将SpringDataWebSupportHateoasAwareSpringDataWebSupport注册为 Spring Bean

示例 22. 在 XML 中启用 Spring Data Web 支持
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

<!-- If you're using Spring HATEOAS as well register this one *instead* of the former -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />
基本 Web 支持

上面显示的配置设置将注册一些基本组件

  • 一个DomainClassConverter,使 Spring MVC 能够从请求参数或路径变量中解析 Repository 管理的域类实例。

  • HandlerMethodArgumentResolver实现,让 Spring MVC 从请求参数中解析 Pageable 和 Sort 实例。

DomainClassConverter

DomainClassConverter允许您直接在 Spring MVC 控制器方法签名中使用域类型,这样您就不必通过 Repository 手动查找实例

示例 23. 在方法签名中使用域类型的 Spring MVC 控制器
@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

如您所见,该方法直接接收一个 User 实例,无需进一步查找。可以通过让 Spring MVC 首先将路径变量转换为域类的 id 类型,并最终通过调用为域类型注册的 Repository 实例上的findOne(…)来访问实例,从而解析该实例。

目前,Repository 必须实现CrudRepository才能有资格被发现以进行转换。
用于 Pageable 和 Sort 的 HandlerMethodArgumentResolvers

上面的配置片段还注册了一个PageableHandlerMethodArgumentResolver以及一个SortHandlerMethodArgumentResolver实例。注册使PageableSort成为有效的控制器方法参数

示例 24. 使用 Pageable 作为控制器方法参数
@Controller
@RequestMapping("/users")
public class UserController {

  @Autowired UserRepository repository;

  @RequestMapping
  public String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

此方法签名将导致 Spring MVC 尝试使用以下默认配置从请求参数中派生 Pageable 实例

表 1. 用于 Pageable 实例的请求参数

page

您要检索的页面。

size

您要检索的页面的大小。

sort

应按其排序的属性,格式为property,property(,ASC|DESC)。默认排序方向为升序。如果您想切换方向,请使用多个sort参数,例如?sort=firstname&sort=lastname,asc

要自定义此行为,请扩展SpringDataWebConfiguration或启用 HATEOAS 的等效项,并覆盖pageableResolver()sortResolver()方法,并导入您的自定义配置文件,而不是使用@Enable注解。

如果您需要从请求中解析多个PageableSort实例(例如,用于多个表),则可以使用 Spring 的@Qualifier注解来区分它们。然后请求参数必须以${qualifier}_为前缀。因此,对于这样的方法签名

public String showUsers(Model model,
      @Qualifier("foo") Pageable first,
      @Qualifier("bar") Pageable second) {  }

您必须填充foo_pagebar_page等。

传递给方法的默认Pageable等效于new PageRequest(0, 20),但可以使用Pageable参数上的@PageableDefaults注解进行自定义。

Pageable 的超媒体支持

Spring HATEOAS 附带一个表示模型类PagedResources,它允许使用必要的Page元数据丰富Page实例的内容,以及链接,以便客户端轻松导航页面。PagePagedResources的转换由 Spring HATEOASResourceAssembler接口的实现PagedResourcesAssembler完成。

示例 25. 使用 PagedResourcesAssembler 作为控制器方法参数
@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable,
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

启用如上所示的配置允许将PagedResourcesAssembler用作控制器方法参数。在其上调用toResources(…)将导致以下操作

  • Page的内容将成为PagedResources实例的内容。

  • PagedResources将附加一个PageMetadata实例,该实例使用来自Page和底层PageRequest的信息进行填充。

  • PagedResources会附加prevnext链接,具体取决于页面的状态。这些链接将指向调用方法映射到的 URI。添加到方法的分页参数将与PageableHandlerMethodArgumentResolver的设置匹配,以确保稍后可以解析链接。

假设数据库中有 30 个 Person 实例。您现在可以触发请求GET https://127.0.0.1:8080/persons,您将看到类似以下内容

{ "links" : [ { "rel" : "next",
                "href" : "https://127.0.0.1:8080/persons?page=1&size=20 }
  ],
  "content" : [
      // 20 Person instances rendered here
  ],
  "pageMetadata" : {
    "size" : 20,
    "totalElements" : 30,
    "totalPages" : 2,
    "number" : 0
  }
}

您会看到组装器生成了正确的 URI,并且还拾取了存在的默认配置以将参数解析为即将到来的请求的Pageable。这意味着,如果您更改该配置,链接将自动遵循更改。默认情况下,组装器指向它被调用的控制器方法,但这可以通过传递一个自定义Link来进行自定义,该自定义Link将用作构建分页链接的基础,以重载PagedResourcesAssembler.toResource(…)方法。

2.7.2. Repository 填充器

如果您使用 Spring JDBC 模块,您可能熟悉使用 SQL 脚本填充DataSource的支持。在 Repository 级别可以使用类似的抽象,尽管它不使用 SQL 作为数据定义语言,因为它必须与存储无关。因此,填充器支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充 Repository 的数据。

假设您有一个包含以下内容的文件data.json

示例 26. 在 JSON 中定义的数据
[ { "_class" : "com.acme.Person",
 "firstname" : "Dave",
  "lastname" : "Matthews" },
  { "_class" : "com.acme.Person",
 "firstname" : "Carter",
  "lastname" : "Beauford" } ]

您可以通过使用 Spring Data Commons 中提供的 Repository 命名空间的填充器元素轻松填充您的 Repository。要将前面的数据填充到您的 PersonRepository 中,请执行以下操作

示例 27. 声明 Jackson Repository 填充器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    http://www.springframework.org/schema/data/repository/spring-repository.xsd">

  <repository:jackson-populator locations="classpath:data.json" />

</beans>

此声明会导致读取data.json文件并通过 JacksonObjectMapper反序列化。

将 JSON 对象反序列化到的类型将通过检查 JSON 文档的_class属性来确定。基础结构最终将选择合适的 Repository 来处理刚刚反序列化的对象。

要改为使用 XML 来定义 Repository 将要填充的数据,您可以使用unmarshaller-populator元素。您可以将其配置为使用 Spring OXM 提供的 XML 序列化器选项之一。有关详细信息,请参阅Spring 参考文档

示例 28. 声明反序列化 Repository 填充器(使用 JAXB)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:repository="http://www.springframework.org/schema/data/repository"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/repository
    http://www.springframework.org/schema/data/repository/spring-repository.xsd
    http://www.springframework.org/schema/oxm
    http://www.springframework.org/schema/oxm/spring-oxm.xsd">

  <repository:unmarshaller-populator locations="classpath:data.json"
    unmarshaller-ref="unmarshaller" />

  <oxm:jaxb2-marshaller contextPath="com.acme" />

</beans>

2.7.3. 旧版 Web 支持

Spring MVC 的域类 Web 绑定

假设您正在开发一个 Spring MVC Web 应用程序,您通常必须从 URL 中解析域类 ID。默认情况下,您的任务是将该请求参数或 URL 部分转换为域类,然后将其传递给下面的层,或者直接在实体上执行业务逻辑。这看起来像这样

@Controller
@RequestMapping("/users")
public class UserController {

  private final UserRepository userRepository;

  @Autowired
  public UserController(UserRepository userRepository) {
    Assert.notNull(repository, "Repository must not be null!");
    this.userRepository = userRepository;
  }

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") Long id, Model model) {

    // Do null check for id
    User user = userRepository.findOne(id);
    // Do null check for user

    model.addAttribute("user", user);
    return "user";
  }
}

首先,您为每个控制器声明一个 Repository 依赖项,以查找控制器或 Repository 分别管理的实体。查找实体也是样板代码,因为它始终是findOne(…)调用。幸运的是,Spring 提供了注册自定义组件的方法,这些组件允许在String值与任意类型之间进行转换。

PropertyEditors

对于 3.0 之前的 Spring 版本,必须使用简单的 JavaPropertyEditors。为了与之集成,Spring Data 提供了一个DomainClassPropertyEditorRegistrar,它查找在ApplicationContext中注册的所有 Spring Data Repository,并为管理的域类注册一个自定义PropertyEditor

<bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="webBindingInitializer">
    <bean class="….web.bind.support.ConfigurableWebBindingInitializer">
      <property name="propertyEditorRegistrars">
        <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" />
      </property>
    </bean>
  </property>
</bean>

如果您已如前面的示例中配置 Spring MVC,则可以按如下方式配置您的控制器,这减少了许多混乱和样板代码。

@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

转换服务在 Spring 3.0 及更高版本中,PropertyEditor 支持被新的转换基础设施取代,该基础设施消除了 PropertyEditors 的缺点,并使用无状态的 X 到 Y 转换方法。Spring Data 现在附带了一个 DomainClassConverter,它模拟了 DomainClassPropertyEditorRegistrar 的行为。要进行配置,只需声明一个 bean 实例并将正在使用的 ConversionService 传递到其构造函数中。

<mvc:annotation-driven conversion-service="conversionService" />

<bean class="org.springframework.data.repository.support.DomainClassConverter">
  <constructor-arg ref="conversionService" />
</bean>

如果您使用的是 JavaConfig,则只需扩展 Spring MVC 的 WebMvcConfigurationSupport 并将配置超类提供的 FormatingConversionService 传递到您创建的 DomainClassConverter 实例中。

class WebConfiguration extends WebMvcConfigurationSupport {

  // Other configuration omitted

  @Bean
  public DomainClassConverter<?> domainClassConverter() {
    return new DomainClassConverter<FormattingConversionService>(mvcConversionService());
  }
}
Web 分页

在 Web 层处理分页时,通常必须自己编写大量样板代码才能从请求中提取必要的元数据。下面示例中所示的不太理想的方法需要该方法包含一个 HttpServletRequest 参数,该参数必须手动解析。此示例还省略了适当的错误处理,这将使代码更加冗长。

@Controller
@RequestMapping("/users")
public class UserController {

  // DI code omitted

  @RequestMapping
  public String showUsers(Model model, HttpServletRequest request) {

    int page = Integer.parseInt(request.getParameter("page"));
    int pageSize = Integer.parseInt(request.getParameter("pageSize"));

    Pageable pageable = new PageRequest(page, pageSize);

    model.addAttribute("users", userService.getUsers(pageable));
    return "users";
  }
}

底线是控制器不必处理从请求中提取分页信息的的功能。因此,Spring Data 附带了一个 PageableHandlerMethodArgumentResolver,它将为您完成这项工作。Spring MVC JavaConfig 支持公开了一个 WebMvcConfigurationSupport 帮助类,以如下方式自定义配置

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

  @Override
  protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new PageableHandlerMethodArgumentResolver());
  }
}

如果您坚持使用 XML 配置,则可以按如下方式注册解析器

<bean class="….web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
  <property name="customArgumentResolvers">
    <list>
      <bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
    </list>
  </property>
</bean>

一旦您使用 Spring MVC 配置了解析器,它就可以让您将控制器简化到如下所示

@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping
  public String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", userRepository.findAll(pageable));
    return "users";
  }
}

PageableArgumentResolver 自动解析请求参数以构建 PageRequest 实例。默认情况下,它期望请求参数具有以下结构。

表 2. PageableHandlerMethodArgumentResolver 评估的请求参数

page

您要检索的页面,从 0 开始索引,默认为 0。

size

您要检索的页面的大小,默认为 20。

sort

格式为 ($propertyname,)[asc|desc]? 的排序指令集合。

分页 URL 参数示例

要检索第三页,最大页面大小为 100,数据按电子邮件属性升序排序,请使用以下 URL 参数

?page=2&size=100&sort=email,asc

要按多个属性以不同的排序顺序对数据进行排序,请使用以下 URL 参数

?sort=foo,asc&sort=bar,desc

如果您需要从请求中解析多个 Pageable 实例(例如,对于多个表),则可以使用 Spring 的 @Qualifier 注解来区分它们。然后,请求参数必须以 ${qualifier}_ 为前缀。因此,对于这样的方法签名

public String showUsers(Model model,
  @Qualifier("foo") Pageable first,
  @Qualifier("bar") Pageable second) {  }

您必须填充 foo_pagebar_page 以及相关的子属性。

在 bean 声明上配置全局默认值,PageableArgumentResolver 将默认使用第一页和页面大小为 10 的 PageRequest。如果它无法从请求中解析 PageRequest(例如,由于参数丢失),它将使用该值。您可以在 bean 声明上直接配置全局默认值。如果您可能需要控制器方法特定的 Pageable 默认值,请使用 @PageableDefaults 注解方法参数,并指定页面(通过 pageNumber)、页面大小(通过 value)、sort(要排序的属性列表)和 sortDir(排序方向)作为注解属性

public String showUsers(Model model,
  @PageableDefaults(pageNumber = 0, value = 30) Pageable pageable) {  }

3. 审计

3.1. 基础

Spring Data 提供了复杂的支持,可以透明地跟踪谁创建或更改了实体以及事件发生的时间点。要利用此功能,您必须为实体类配备审计元数据,可以使用注解或实现接口来定义这些元数据。

3.1.1. 基于注解的审计元数据

我们提供 @CreatedBy@LastModifiedBy 来捕获创建或修改实体的用户,以及 @CreatedDate@LastModifiedDate 来捕获事件发生的时间点。

示例 29. 受审计的实体
class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … further properties omitted
}

如您所见,可以根据您想要捕获的信息选择性地应用这些注解。对于捕获时间点的注解,可以在以下类型的属性上使用:JodaTimes DateTime、旧版 Java DateCalendar、JDK8 日期/时间类型以及 long/Long

3.1.2. 基于接口的审计元数据

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

还有一个便捷的基类 AbstractAuditable,您可以扩展它以避免手动实现接口方法的需要。请注意,这会增加域类与 Spring Data 的耦合,这可能是您想要避免的。通常,基于注解的方式定义审计元数据是首选,因为它侵入性更小且更灵活。

3.1.3. AuditorAware

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

这是一个使用 Spring Security 的 Authentication 对象的接口实现示例

示例 30. 基于 Spring Security 的 AuditorAware 实现
class SpringSecurityAuditorAware implements AuditorAware<User> {

  public User getCurrentAuditor() {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication == null || !authentication.isAuthenticated()) {
      return null;
    }

    return ((MyUserDetails) authentication.getPrincipal()).getUser();
  }
}

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

附录

附录 A:命名空间参考

<repositories /> 元素

<repositories /> 元素触发 Spring Data 存储库基础设施的设置。最重要的属性是 base-package,它定义了要扫描 Spring Data 存储库接口的包。[3]

表 3. 属性
名称 描述

base-package

定义要用于扫描扩展 *Repository 的存储库接口的包(实际接口由特定 Spring Data 模块确定)在自动检测模式下。所有配置包下方的包也将被扫描。允许使用通配符。

repository-impl-postfix

定义后缀以自动检测自定义存储库实现。名称以配置的后缀结尾的类将被视为候选对象。默认为 Impl

query-lookup-strategy

确定用于创建查找器查询的策略。有关详细信息,请参阅查询查找策略。默认为 create-if-not-found

named-queries-location

定义查找包含外部定义查询的属性文件的位置。

consider-nested-repositories

控制是否应考虑嵌套存储库接口定义。默认为 false

附录 B:填充程序命名空间参考

<populator /> 元素

<populator /> 元素允许通过 Spring Data 存储库基础设施填充数据存储。[4]

表 4. 属性
名称 描述

locations

查找要从中读取对象的存储库的文件的位置,将使用该文件进行填充。

附录 C:存储库查询关键字

支持的查询关键字

下表列出了 Spring Data 存储库查询派生机制通常支持的关键字。但是,请查阅特定存储的文档以获取支持关键字的确切列表,因为此处列出的一些关键字可能在特定存储中不受支持。

表 5. 查询关键字
逻辑关键字 关键字表达式

AND

And

OR

Or

AFTER

AfterIsAfter

BEFORE

BeforeIsBefore

CONTAINING

ContainingIsContainingContains

BETWEEN

BetweenIsBetween

ENDING_WITH

EndingWithIsEndingWithEndsWith

EXISTS

Exists

FALSE

FalseIsFalse

GREATER_THAN

GreaterThanIsGreaterThan

GREATER_THAN_EQUALS

GreaterThanEqualIsGreaterThanEqual

IN

InIsIn

IS

IsEquals(或无关键字)

IS_NOT_NULL

NotNullIsNotNull

IS_NULL

NullIsNull

LESS_THAN

LessThanIsLessThan

LESS_THAN_EQUAL

LessThanEqualIsLessThanEqual

LIKE

LikeIsLike

NEAR

NearIsNear

NOT

NotIsNot

NOT_IN

NotInIsNotIn

NOT_LIKE

NotLikeIsNotLike

REGEX

RegexMatchesRegexMatches

STARTING_WITH

StartingWithIsStartingWithStartsWith

TRUE

TrueIsTrue

WITHIN

WithinIsWithin


1. Spring 参考文档中的 JavaConfig
2. Spring HATEOAS - https://github.com/SpringSource/spring-hateoas
3. 请参阅XML 配置
4. 请参阅XML 配置