自定義倉庫實現

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

@EnableRedisRepositories(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 提供,該 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(),你需要建議負責建立實際倉庫的倉庫工廠暴露方法元資料。

暴露倉庫元資料
  • 標記介面

  • Bean Post Processor

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 直接在倉庫工廠 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
  }
}
該類需要有一個超類的建構函式,該建構函式供特定儲存的倉庫工廠實現使用。如果倉庫基類有多個建構函式,請覆蓋接受 EntityInformation 加上一個特定儲存的基礎設施物件(例如 EntityManager 或模板類)的那個建構函式。

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

示例 4. 配置自定義倉庫基類
  • Java

  • XML

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