自定義 Repository 實現

Spring Data 提供了多種選項,只需少量編碼即可建立查詢方法。但當這些選項無法滿足您的需求時,您也可以為 repository 方法提供自己的自定義實現。本節將介紹如何做到這一點。

自定義單個 Repository

要為 repository 豐富自定義功能,您必須首先定義一個 fragment 介面以及該自定義功能的實現,如下所示

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

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

與 fragment 介面對應的類名中最重要的部分是 Impl 字尾。您可以透過設定 @Enable<StoreModule>Repositories(repositoryImplementationPostfix = …) 來自定義特定 store 的字尾。

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

位於與 repository 介面同一包中、名稱匹配 repository 介面名 後面跟 實現字尾 的型別,被視為自定義實現,並將被當作自定義實現處理。遵循該名稱的類可能導致非預期行為。

我們認為單自定義實現命名模式已被棄用,不推薦使用此模式。請遷移到基於 fragment 的程式設計模型。

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

然後您可以讓您的 repository 介面繼承 fragment 介面,如下所示

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

  // Declare query methods here
}

透過讓您的 repository 介面繼承 fragment 介面,可以將 CRUD 功能和自定義功能結合起來,並使其可供客戶端使用。

Spring Data repository 透過使用構成 repository 組合的 fragment 來實現。Fragment 包括基礎 repository、功能切面(例如 Querydsl)以及自定義介面及其實現。每當您向 repository 介面新增一個介面時,您就透過新增一個 fragment 來增強組合。基礎 repository 和 repository 切面的實現由各個 Spring Data 模組提供。

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

帶有實現的 Fragment
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 和切面方法,並在兩個 fragment 提供相同方法簽名時解決歧義。Repository fragment 不僅限於在單個 repository 介面中使用。多個 repository 可以使用同一個 fragment 介面,從而允許您在不同的 repository 中重用自定義功能。

以下示例顯示了一個 repository fragment 及其實現

覆蓋 save(…) 的 Fragment
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 fragment 的 repository

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

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

配置

repository 基礎設施會嘗試透過掃描找到 repository 的包下的類來自動檢測自定義實現 fragment。這些類需要遵循追加字尾(預設為 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,這與 fragment 介面(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。如果您的實現 fragment 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 註冊 Fragment

如同在配置一節中提到的,基礎設施僅在 repository 基礎包中自動檢測 fragment。因此,位於其他位置或希望由外部歸檔貢獻的 fragment,如果它們不共享同一個名稱空間,將不會被找到。在 spring.factories 中註冊 fragment 可以讓您規避此限制,具體說明見下文。

假設您想為您的組織提供一些可跨多個 repository 使用的自定義搜尋功能,利用文字搜尋索引。

首先,您需要的是 fragment 介面。注意泛型引數 <T>,它用於使 fragment 與 repository 領域型別對齊。

Fragment 介面
public interface SearchExtension<T> {

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

假設實際的全文搜尋透過一個在上下文中註冊為 BeanSearchService 提供,因此您可以在我們的 SearchExtension 實現中使用它。您執行搜尋所需的一切是集合(或索引)名稱以及一個將搜尋結果轉換為實際領域物件的物件對映器,如下所示。

Fragment 實現
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 標記介面新增到 fragment 實現中將觸發基礎設施,併為使用該 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 {

    // ...
}

可以透過 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 上啟用該標誌。

完成 fragment 宣告和實現後,您可以將擴充套件註冊到 META-INF/spring.factories 檔案中,並在需要時打包。

META-INF/spring.factories 中註冊 fragment
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
  }
}
該類需要有一個超類的建構函式,特定 store 的 repository 工廠實現會使用它。如果 repository 基礎類有多個建構函式,請覆蓋接受一個 EntityInformation 以及一個特定 store 的基礎設施物件(例如 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" />