定製 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),參與切面等等。

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

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

  // Declare query methods here
}

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

Spring Data repositories 是透過構成 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
}

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

以下示例顯示了一個 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 實現定義的名稱,並將使用它而不是第一個實現。

手動裝配

如果您的定製實現僅使用基於註解的配置和自動裝配,前面所示的方法將很好地工作,因為它被視為任何其他 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 中註冊片段允許您規避此限制,如下一節所述。

想象一下,您希望為您的組織提供一些可跨多個 repositories 使用的定製搜尋功能,並利用文字搜尋索引。

首先,您需要的是片段介面。請注意泛型引數 <T>,以便將片段與 repository 領域型別對齊。

片段介面
public interface SearchExtension<T> {

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

假設實際的全文搜尋透過一個在上下文中註冊為 Bean 的 SearchService 可用,因此您可以在 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 標記介面新增到片段實現中,將觸發基礎設施併為使用該片段的 repositories 啟用元資料暴露。

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 {

    // ...
}

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

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 行為以便所有 repositories 都受影響的話。為了改為改變所有 repositories 的行為,您可以建立一個實現類,該類擴充套件特定持久化技術的基礎 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" />