跳到主要内容

自定义仓储实现

QWen Flash 中英对照 Custom Repository Implementations

Spring Data 提供了多种选项来创建查询方法,几乎无需编写代码。但当这些选项不符合你的需求时,你也可以为 repository 方法提供自己的自定义实现。本节描述如何实现这一点。

自定义单个仓库​

若要为 repository 添加自定义功能,你必须首先定义自定义功能的 fragment interface 和实现,如下所示:

interface CustomizedUserRepository {
void someCustomMethod(User user);
}
java
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

@Override
public void someCustomMethod(User user) {
// Your custom implementation
}
}
java
备注

与 fragment interface 对应的类名中,最重要的部分是 Impl 后缀。你可以通过设置 @Enable<StoreModule>Repositories(repositoryImplementationPostfix = …) 来定制 store-specific 后缀。

注意

历史上,Spring Data 自定义仓库实现发现遵循一种 命名模式,该模式从仓库派生自定义实现类名称,从而实际上只允许一个自定义实现。

与仓库接口位于同一个包中、名称匹配 仓库接口名称 后接 实现后缀 的类型,会被视为自定义实现,并将按自定义实现处理。遵循该名称的类可能导致不期望的行为。

我们认为单一自定义实现命名已废弃,并建议不要使用此模式。请迁移到基于片段的编程模型。

实现本身不依赖于 Spring Data,可以是一个普通的 Spring bean。因此,你可以使用标准的依赖注入行为来注入对其他 bean 的引用(例如 JdbcTemplate),参与切面等。

然后,你可以让你的 repository 接口继承 fragment 接口,如下所示:

interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

// Declare query methods here
}
java

使用 repository interface 扩展 fragment interface 会结合 CRUD 和自定义功能,并使其可供客户端使用。

Spring Data repositories 通过使用形成 repository composition 的 fragments 来实现。Fragments 是 base repository、功能方面(例如 Querydsl),以及自定义接口及其实现。每次向 repository 接口添加一个接口时,都会通过添加一个 fragment 来增强该 composition。base repository 和 repository aspect 实现由每个 Spring Data module 提供。

以下示例展示了自定义接口及其实现:

interface HumanRepository {
void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

@Override
public void someHumanMethod(User user) {
// Your custom implementation
}
}

interface ContactRepository {

void someContactMethod(User user);

User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

@Override
public void someContactMethod(User user) {
// Your custom implementation
}

@Override
public User anotherContactMethod(User user) {
// Your custom implementation
}
}
java

以下示例展示了扩展 CrudRepository 的自定义仓储接口:

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

// Declare query methods here
}
java

Repository 可由多个 Custom implementations 组成,这些 Custom implementations 会按照其声明顺序导入。Custom implementations 的优先级高于 base implementation 和 repository aspects。这种顺序允许你覆盖 base repository 和 aspect methods,并在两个 fragments 提供相同的 method signature 时解决歧义。Repository fragments 不限于在单个 repository interface 中使用。多个 Repository 可以使用同一个 fragment interface,从而让你在不同 Repository 之间复用 customizations。

以下示例展示了一个 repository 片段及其实现:

interface CustomizedSave<T> {
<S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

@Override
public <S extends T> S save(S entity) {
// Your custom implementation
}
}
java

以下示例展示了一个使用上述 repository 片段的 repository:

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}
java

配置​

Repository 基础设施会尝试通过扫描发现 Repository 所在包下的类,来自动检测自定义实现片段。这些类需要遵循命名约定,即追加一个默认值为 Impl 的后缀。

以下示例展示了一个使用默认 postfix 的 repository,以及一个为 postfix 设置了自定义值的 repository:

示例 1. 配置示例

@EnableLdapRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
java

上述示例中的第一个配置尝试查找名为 com.acme.repository.CustomizedUserRepositoryImpl 的类,以将其用作自定义 repository 实现。第二个示例尝试查找 com.acme.repository.CustomizedUserRepositoryMyPostfix。

歧义的消除​

如果在不同包中找到多个类名相同的实现,Spring Data 会使用 bean 名称来识别使用哪一个。

在以下两个针对前面展示的 CustomizedUserRepository 的自定义实现中,使用的是第一个实现。其 bean 名称为 customizedUserRepositoryImpl,这与片段接口( CustomizedUserRepository )的名称加上后缀 Impl 相匹配。

示例 2. 歧义实现的解析

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

// Your custom implementation
}
java
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

// Your custom implementation
}
java

如果你使用 @Component("specialCustom") 注解 UserRepository 接口,那么 bean 名称加上 Impl 就会与 com.acme.impl.two 中为 repository 实现定义的 bean 名称匹配,因此会使用它而不是第一个。

手动接线​

如果自定义实现仅使用基于 annotation 的 configuration 和 autowiring,则前面展示的方法工作良好,因为它被视为任何其他 Spring bean。如果 implementation fragment bean 需要特殊 wiring,你可以声明该 bean,并按照 上一节 中描述的约定为其命名。随后,基础设施按名称引用手动定义的 bean definition,而不是自行创建。以下示例展示如何手动 wiring 自定义实现:

示例 3. 手动接线自定义实现

class MyClass {
MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
…
}
}
java

使用 spring.factories 注册 Fragments​

正如在 配置 部分中已经提到的,基础设施只会自动检测 repository base-package 内的片段。因此,位于其他位置或希望由外部归档贡献的片段,如果没有共享公共命名空间,将无法被找到。在 spring.factories 中注册片段可以规避此限制,如下节所述。

想象一下,你希望利用文本搜索索引,为你的组织提供可在多个仓库中使用的自定义搜索功能。

首先,你只需要 fragment interface。注意泛型 <T> 参数,以便将 fragment 与 repository domain type 对齐。

public interface SearchExtension<T> {

List<T> search(String text, Limit limit);
}
java

假设实际的全文检索通过一个 SearchService 提供,该 SearchService 在上下文中注册为 Bean,因此你可以在我们的 SearchExtension 实现中使用它。运行检索只需要集合(或索引)名称,以及一个将检索结果转换为实际领域对象的对象映射器,如下所示。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.RepositoryMethodContext;

class DefaultSearchExtension<T> implements SearchExtension<T> {

private final SearchService service;

DefaultSearchExtension(SearchService service) {
this.service = service;
}

@Override
public List<T> search(String text, Limit limit) {
return search(RepositoryMethodContext.getContext(), text, limit);
}

List<T> search(RepositoryMethodContext metadata, String text, Limit limit) {

Class<T> domainType = metadata.getRepository().getDomainType();

String indexName = domainType.getSimpleName().toLowerCase();
List<String> jsonResult = service.search(indexName, text, 0, limit.max());

return jsonResult.stream().map(…).collect(toList());
}
}
java

在上面的示例中,RepositoryMethodContext.getContext() 用于检索实际方法调用的元数据。RepositoryMethodContext 暴露了附加到 repository 的信息,例如 domain type。在本例中,我们使用 repository 的 domain type 来标识要搜索的 index 的名称。

暴露调用元数据的开销较大,因此默认情况下处于禁用状态。要访问 RepositoryMethodContext.getContext(),需要通知负责创建实际 repository 的 repository factory 暴露方法元数据。

将 RepositoryMetadataAccess 标记接口添加到 fragments 实现中会触发基础设施,并为使用该 fragment 的 repository 启用元数据暴露。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit;
import org.springframework.data.repository.core.support.RepositoryMetadataAccess;
import org.springframework.data.repository.core.RepositoryMethodContext;

class DefaultSearchExtension<T> implements SearchExtension<T>, RepositoryMetadataAccess {

// ...
}
java

同时具备片段声明和实现后,您可以在 META-INF/spring.factories 文件中注册该扩展,并在需要时打包相关内容。

com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension
properties

现在你可以使用你的扩展了;只需将该接口添加到你的仓库中。

import com.acme.search.SearchExtension;
import org.springframework.data.repository.CrudRepository;

interface MovieRepository extends CrudRepository<Movie, String>, SearchExtension<Movie> {

}
java

自定义基础仓库​

上一节中描述的方法要求你在想自定义基础 repository 行为以影响所有 repository 时,对每个 repository 接口进行自定义。相反,如果要为所有 repository 更改行为,可以创建一个扩展特定持久化技术的 repository 基类的实现。然后该类作为 repository 代理的自定义基类,如下例所示:

class MyRepositoryImpl<T, ID>
extends SimpleJpaRepository<T, ID> {

private final EntityManager entityManager;

MyRepositoryImpl(JpaEntityInformation entityInformation,
EntityManager entityManager) {
super(entityInformation, entityManager);

// Keep the EntityManager around to used from the newly introduced methods.
this.entityManager = entityManager;
}

@Override
@Transactional
public <S extends T> S save(S entity) {
// implementation goes here
}
}
java
警告

该类需要具有 store-specific repository factory 实现所使用的父类构造函数。如果 repository 基类具有多个构造函数,请重写接受 EntityInformation 以及 store-specific 基础设施对象(例如 EntityManager 或模板类)的那个构造函数。

最后一步是让 Spring Data 基础设施感知到自定义的仓库基类。在配置中,你可以通过使用 repositoryBaseClass 来实现,如下例所示:

示例 4. 配置自定义 repository 基类

@Configuration
@EnableLdapRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
java

自定义 Repository Factory​

通过 RepositoryFactoryCustomizer 自定义 repository factory 可以直接访问参与 repository 实例创建的组件。这种机制在你希望调整代理创建的某些方面,而又不引入完全自定义的 repository factory bean 时非常有用。以下示例演示了如何注册额外的 listeners 和 proxy advisors:

factoryBean.addRepositoryFactoryCustomizer(repositoryFactory -> {
repositoryFactory.addInvocationListener(…);
repositoryFactory.addQueryCreationListener(…);

repositoryFactory.addRepositoryProxyPostProcessor((factory, repositoryInformation) -> factory.addAdvice(…));
});
java

RepositoryFactoryCustomizer 与特定的 repository factory bean 相关联,理想情况下通过 BeanPostProcessor 实现,以便仅影响特定 repositories。注意,customizer beans 不会自动应用,以防止不需要的 wiring,这在多 repository 配置或使用多个 Spring Data modules 时尤其重要。

自定义 Repository Factory Bean​

定制 repository 创建的最强大方式是提供一个自定义 repository factory bean,通常是 RepositoryFactorySupport、TransactionalRepositoryFactoryBeanSupport 或 store-specific repository factory bean 的子类。

自定义 repository factory bean 使您可以完全访问底层 repository factory,从而彻底更改 repository 的创建方式。

请注意,这种方式需要最多的工作量,通常只有当你想要更改 repository 创建的核心方面时才需要。此外,你还需要考虑 repository metadata derivation,它用于识别 query methods、base implementation methods 和 custom implementations。以下摘要概述了关键方面:

  • repositoryBaseClass:仓储基类定义了哪些方法由基类实现,哪些方法需要通过切面或自定义实现进行额外处理。

  • repositoryFragmentsContributor:RepositoryFragmentsContributor 允许在所有标准片段收集完毕后对仓储组合进行贡献。存储模块使用此机制添加 Querydsl 或 Query-by-Example 支持等功能。它也可作为第三方扩展的 SPI。

  • exposeMetadata:控制是否可通过 RepositoryMethodContext.getContext() 获取 调用元数据。