自定義 Repository 實現

Spring Data 提供了多種建立查詢方法的方式,程式碼量很少。但是當這些方式不能滿足你的需求時,你也可以為 repository 方法提供自己的自定義實現。本節將介紹如何實現。

定製單個 Repository

為了使用自定義功能豐富 repository,你必須首先定義一個片段介面和自定義功能的實現,如下所示

自定義 repository 功能的介面
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
自定義 repository 功能的實現
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

與片段介面對應的類名最重要的部分是 Impl 字尾。你可以透過設定 @Enable<StoreModule>Repositories(repositoryImplementationPostfix = …) 來自定義特定於儲存的字尾。

從歷史上看,Spring Data 自定義 repository 實現的發現遵循一個 命名模式,該模式從 repository 派生自定義實現類名,實際上只允許一個自定義實現。

位於與 repository 介面相同包中、匹配 "repository 介面名" 後面跟 "實現字尾" 的型別被認為是自定義實現,並將被視為自定義實現。遵循該名稱的類可能導致意外行為。

我們認為單自定義實現命名已被棄用,並建議不要使用此模式。請轉而遷移到基於片段的程式設計模型。

實現本身不依賴於 Spring Data,並且可以是一個普通的 Spring Bean。因此,你可以使用標準的依賴注入行為來注入對其他 Bean(例如 JdbcTemplate)的引用,參與 AOP 等等。

然後,你可以讓你的 repository 介面擴充套件片段介面,如下所示

對你的 repository 介面的更改
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

將你的 repository 介面擴充套件片段介面,結合了 CRUD 和自定義功能,並使其可供客戶端使用。

Spring Data repository 透過使用構成 repository 組合的片段來實現。片段包括基本 repository、功能方面(例如 Querydsl)以及自定義介面及其實現。每次向 repository 介面新增介面時,都會透過新增片段來增強組合。基本 repository 和 repository 方面實現由每個 Spring Data 模組提供。

以下示例展示了自定義介面及其實現

包含其實現的片段
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
  }
}

以下示例展示了一個擴充套件 CrudRepository 的自定義 repository 的介面

對你的 repository 介面的更改
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

Repository 可以由多個自定義實現組成,這些實現按照宣告順序匯入。自定義實現的優先順序高於基本實現和 repository 方面。這種排序允許你覆蓋基本 repository 和方面的方法,並在兩個片段貢獻相同方法簽名時解決歧義。Repository 片段不僅限於在單個 repository 介面中使用。多個 repository 可以使用同一個片段介面,從而允許你在不同的 repository 之間重用定製。

以下示例展示了一個 repository 片段及其實現

覆蓋 save(…) 方法的片段
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
  }
}

以下示例展示了一個使用前面 repository 片段的 repository

定製的 repository 介面
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

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

配置

Repository 基礎設施嘗試透過掃描發現 repository 的包下的類來自動檢測自定義實現片段。這些類需要遵循附加 Impl 字尾(預設值)的命名約定。

以下示例展示了一個使用預設字尾的 repository 和一個為字尾設定自定義值的 repository

示例 1. 配置示例
  • Java

  • XML

@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
<repositories base-package="com.acme.repository" />

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

前面示例中的第一個配置嘗試查詢名為 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
}
@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

如果你使用 @Component("specialCustom") 註解 UserRepository 介面,則 bean 名稱加上 Impl 將匹配在 com.acme.impl.two 中為 repository 實現定義的 bean 名稱,並會使用它而不是第一個實現。

手動裝配

如果你的自定義實現只使用基於註解的配置和自動裝配,前面所示的方法效果很好,因為它被視為任何其他 Spring bean。如果你的實現片段 bean 需要特殊的裝配,你可以宣告該 bean 並根據前面章節中描述的約定命名它。然後,基礎設施會按名稱引用手動定義的 bean 定義,而不是自己建立一個。以下示例展示瞭如何手動裝配自定義實現

示例 3. 手動裝配自定義實現
  • Java

  • XML

class MyClass {
  MyClass(@Qualifier("userRepositoryImpl") UserRepository userRepository) {
    …
  }
}
<repositories base-package="com.acme.repository" />

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

使用 spring.factories 註冊片段

配置 一節所述,基礎設施只會在 repository 基包內自動檢測片段。因此,位於其他位置或希望由外部歸檔檔案貢獻的片段如果它們不共享公共名稱空間,將不會被找到。在 spring.factories 中註冊片段可以讓你規避此限制,如下一節所述。

想象一下,你想利用文字搜尋索引為你的組織提供一些可在多個 repository 中使用的自定義搜尋功能。

首先,你需要片段介面。注意通用 <T> 引數,用於使片段與 repository 領域型別對齊。

片段介面
public interface SearchExtension<T> {

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

假設實際的全文搜尋可以透過一個註冊為上下文中的 BeanSearchService 來獲得,這樣你就可以在我們的 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());
    }
}

在上面的示例中,RepositoryMethodContext.getContext() 用於檢索實際方法呼叫的元資料。RepositoryMethodContext 公開附加到 repository 的資訊,例如領域型別。在這種情況下,我們使用 repository 領域型別來標識要搜尋的索引名稱。

公開呼叫元資料開銷較大,因此預設是停用的。要訪問 RepositoryMethodContext.getContext(),你需要通知負責建立實際 repository 的 repository 工廠,使其公開方法元資料。

公開 Repository 元資料
  • 標記介面

  • Bean 後置處理器

RepositoryMetadataAccess 標記介面新增到片段實現將觸發基礎設施,併為使用該片段的那些 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 {

    // ...
}

exposeMetadata 標誌可以直接透過 BeanPostProcessor 在 repository 工廠 bean 上設定。

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.lang.Nullable;

@Configuration
class MyConfiguration {

    @Bean
    static BeanPostProcessor exposeMethodMetadata() {

        return new BeanPostProcessor() {

            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) {

                if(bean instanceof RepositoryFactoryBeanSupport<?,?,?> factoryBean) {
                    factoryBean.setExposeMetadata(true);
                }
                return bean;
            }
        };
    }
}

請不要直接複製/貼上上述內容,而應考慮你的實際用例,因為上述做法只是簡單地在每個 repository 上啟用該標誌,這可能需要更細粒度的方法。

擁有了片段宣告和實現後,你可以在 META-INF/spring.factories 檔案中註冊擴充套件,並在需要時打包。

在 META-INF/spring.factories 中註冊片段
com.acme.search.SearchExtension=com.acme.search.DefaultSearchExtension

現在你可以開始使用你的擴充套件了;只需將介面新增到你的 repository 中。

使用方法
import com.acme.search.SearchExtension;
import org.springframework.data.repository.CrudRepository;

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

}

定製基礎 Repository

前一節中描述的方法需要定製每個 repository 介面,以便定製基礎 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
  }
}
該類需要有一個超類的建構函式,供特定於儲存的 repository 工廠實現使用。如果 repository 基礎類有多個建構函式,請覆蓋接受一個 EntityInformation 以及一個特定於儲存的基礎設施物件(例如 EntityManager 或模板類)的那個建構函式。

最後一步是讓 Spring Data 基礎設施知曉定製的 repository 基礎類。在配置中,你可以透過使用 repositoryBaseClass 來實現,如下例所示

示例 4. 配置自定義 repository 基礎類
  • Java

  • XML

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />