自訂儲存庫實作

Spring Data 提供了多種選項,可用於建立程式碼極少的查詢方法。但是,當這些選項不符合您的需求時,您也可以為儲存庫方法提供自己的自訂實作。本節說明如何執行此操作。

自訂個別儲存庫

若要使用自訂功能來擴充儲存庫,您必須先定義片段介面和自訂功能的實作,如下所示

自訂儲存庫功能介面
interface CustomizedUserRepository {
  void someCustomMethod(User user);
}
自訂儲存庫功能實作
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

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

對應於片段介面的類別名稱中,最重要的部分是 Impl 後綴。您可以透過設定 @Enable<StoreModule>Repositories(repositoryImplementationPostfix = …) 來客製化特定於儲存區的後綴。

從歷史上看,Spring Data 自訂儲存庫實作探索遵循一個命名模式,該模式從儲存庫衍生出自訂實作類別名稱,從而有效地允許單一自訂實作。

位於與儲存庫介面相同套件中,符合儲存庫介面名稱後接實作後綴的類型,會被視為自訂實作,並將被視為自訂實作。遵循該名稱的類別可能會導致非預期的行為。

我們認為單一自訂實作命名已過時,建議不要使用此模式。請改為遷移至基於片段的程式設計模型。

實作本身不依賴 Spring Data,可以是常規的 Spring bean。因此,您可以使用標準的依賴注入行為來注入對其他 bean (例如 JdbcTemplate) 的參考,參與面向切面程式設計等等。

然後,您可以讓您的儲存庫介面擴充片段介面,如下所示

對儲存庫介面的變更
interface UserRepository extends CrudRepository<User, Long>, CustomizedUserRepository {

  // Declare query methods here
}

使用您的儲存庫介面擴充片段介面,結合了 CRUD 和自訂功能,並使其可供客戶端使用。

Spring Data 儲存庫是透過使用形成儲存庫組合的片段來實作的。片段是基礎儲存庫、功能方面 (例如 QueryDsl),以及自訂介面及其實作。每次您將介面新增至儲存庫介面時,您都會透過新增片段來增強組合。基礎儲存庫和儲存庫方面實作由每個 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 的自訂儲存庫的介面

對儲存庫介面的變更
interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

儲存庫可以由多個自訂實作組成,這些實作按照其宣告的順序匯入。自訂實作的優先順序高於基礎實作和儲存庫方面。此排序可讓您覆寫基礎儲存庫和方面方法,並在兩個片段貢獻相同的方法簽名時解決歧義。儲存庫片段不限於在單一儲存庫介面中使用。多個儲存庫可以使用片段介面,讓您跨不同的儲存庫重複使用自訂項目。

以下範例顯示儲存庫片段及其實作

覆寫 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
  }
}

以下範例顯示使用上述儲存庫片段的儲存庫

自訂儲存庫介面
interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

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

組態

儲存庫基礎結構嘗試透過掃描在其找到儲存庫的套件下方的類別,來自動偵測自訂實作片段。這些類別需要遵循附加後綴 (預設為 Impl) 的命名慣例。

以下範例顯示使用預設後綴的儲存庫,以及為後綴設定自訂值的儲存庫

範例 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 的類別,以作為自訂儲存庫實作。第二個範例嘗試尋找 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 中為儲存庫實作定義的名稱相符,並且會使用它來取代第一個實作。

手動裝配

如果您的自訂實作僅使用基於註解的組態和自動裝配,則先前顯示的方法運作良好,因為它被視為任何其他 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 註冊片段

組態章節中已提及,基礎結構僅自動偵測儲存庫基礎套件內的片段。因此,如果片段不共用通用命名空間,則位於另一個位置或想要由外部封存檔貢獻的片段將不會被找到。在 spring.factories 中註冊片段可讓您規避此限制,如下節所述。

假設您想要為您的組織提供一些自訂搜尋功能,以供多個儲存庫使用,並利用文字搜尋索引。

首先,您只需要片段介面。請注意泛型 <T> 參數,以使片段與儲存庫網域類型對齊。

片段介面
package com.acme.search;

public interface SearchExtension<T> {

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

假設實際的全文檢索搜尋可透過 SearchService 取得,該服務在內容中註冊為 Bean,因此您可以在我們的 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 公開附加到儲存庫的資訊,例如網域類型。在本例中,我們使用儲存庫網域類型來識別要搜尋的索引名稱。

公開調用中繼資料的成本很高,因此預設情況下會停用。若要存取 RepositoryMethodContext.getContext(),您需要建議負責建立實際儲存庫的儲存庫 factory 公開方法中繼資料。

公開儲存庫中繼資料
  • 標記介面

  • Bean 後處理器

RepositoryMetadataAccess 標記介面新增至片段實作,將會觸發基礎結構,並為使用該片段的那些儲存庫啟用中繼資料公開。

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 {

    // ...
}

exposeMetadata 標記可以直接透過 BeanPostProcessor 在儲存庫 factory 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;
            }
        };
    }
}

請不要只是複製/貼上上述內容,而是要考慮您的實際使用案例,這可能需要更精細的方法,因為上述內容只會在每個儲存庫上啟用標記。

在同時擁有片段宣告和實作的情況下,如果需要,您可以在 META-INF/spring.factories 檔案中註冊擴充功能並封裝內容。

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

現在您已準備好使用您的擴充功能;只需將介面新增至您的儲存庫即可。

使用它
package io.my.movies;

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

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

}

自訂基礎儲存庫

前節中描述的方法需要在您想要自訂基礎儲存庫行為時自訂每個儲存庫介面,以便影響所有儲存庫。若要改為變更所有儲存庫的行為,您可以建立一個擴充特定於持久性技術的儲存庫基礎類別的實作。然後,此類別會充當儲存庫代理的自訂基礎類別,如下列範例所示

自訂儲存庫基礎類別
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
  }
}
該類別需要具有超級類別的建構子,特定於儲存區的儲存庫 factory 實作會使用該建構子。如果儲存庫基礎類別有多個建構子,請覆寫採用 EntityInformation 加上特定於儲存區的基礎結構物件 (例如 EntityManager 或範本類別) 的建構子。

最後一個步驟是讓 Spring Data 基礎結構知道自訂的儲存庫基礎類別。在組態中,您可以透過使用 repositoryBaseClass 來執行此操作,如下列範例所示

範例 4. 組態自訂儲存庫基礎類別
  • Java

  • XML

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

在自訂實作中使用 JpaContext

當使用多個 EntityManager 執行個體和自訂儲存庫實作時,您需要將正確的 EntityManager 裝配到儲存庫實作類別中。您可以透過在 @PersistenceContext 註解中明確命名 EntityManager 來執行此操作,或者,如果 EntityManager@Autowired,則可以使用 @Qualifier

從 Spring Data JPA 1.9 開始,Spring Data JPA 包含一個名為 JpaContext 的類別,可讓您依受管理網域類別取得 EntityManager,前提是它僅由應用程式中的其中一個 EntityManager 執行個體管理。以下範例顯示如何在自訂儲存庫中使用 JpaContext

範例 5. 在自訂儲存庫實作中使用 JpaContext
class UserRepositoryImpl implements UserRepositoryCustom {

  private final EntityManager em;

  @Autowired
  public UserRepositoryImpl(JpaContext context) {
    this.em = context.getEntityManagerByManagedType(User.class);
  }

  …
}

此方法的優點是,如果網域類型被指派給不同的持久性單元,則不必觸碰儲存庫來變更對持久性單元的參考。