自定義 Repository 實現

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

定製單個 Repository

要透過自定義功能豐富一個 repository,您必須首先定義一個片段介面(fragment interface)和該自定義功能的實現,如下所示

自定義 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 repositories 是透過使用片段(fragments)來實現的,這些片段構成了 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

@EnableMongoRepositories(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. 歧義實現的解決
package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}
package com.acme.impl.two;

@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 域型別對齊。

片段介面
package com.acme.search;

public interface SearchExtension<T> {

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

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

片段實現
package com.acme.search;

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 啟用元資料暴露。

package com.acme.search;

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 中即可。

使用它
package io.my.movies;

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

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

}

定製基礎 Repository

當您想要定製基礎 repository 行為以影響所有 repositories 時,上一節中描述的方法要求定製每個 repository 介面。為了改變所有 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
@EnableMongoRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />