カスタムリポジトリの実装

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
}

リポジトリは、宣言の順序でインポートされる複数のカスタム実装で構成されます。カスタム実装は、基本実装およびリポジトリの側面よりも優先度が高くなります。この順序付けにより、ベースリポジトリおよびアスペクトメソッドをオーバーライドし、2 つのフラグメントが同じメソッドシグネチャーを提供する場合のあいまいさを解決できます。リポジトリフラグメントは、単一のリポジトリインターフェースでの使用に限定されません。複数のリポジトリがフラグメントインターフェースを使用し、異なるリポジトリでカスタマイズを再利用できる場合があります。

次の例は、リポジトリフラグメントとその実装を示しています。

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

@EnableLdapRepositories(repositoryImplementationPostfix = "MyPostfix")
class Configuration { … }
<repositories base-package="com.acme.repository" />

<repositories base-package="com.acme.repository" repository-impl-postfix="MyPostfix" />

前の例の最初の構成は、カスタムリポジトリ実装として機能する com.acme.repository.CustomizedUserRepositoryImpl というクラスを検索しようとします。2 番目の例では、com.acme.repository.CustomizedUserRepositoryMyPostfix を検索しようとします。

あいまいさの解決

一致するクラス名を持つ複数の実装が異なるパッケージで見つかった場合、Spring Data は Bean 名を使用して、使用する実装を識別します。

前に示した CustomizedUserRepository の次の 2 つのカスタム実装を考えると、最初の実装が使用されます。その 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
}

UserRepository インターフェースに @Component("specialCustom") でアノテーションを付けると、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);
}

実際のフルテキスト検索は、コンテキスト内で Bean として登録されている SearchService を介して利用できるものとし、これを 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 ポストプロセッサー

フラグメント実装に 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
@EnableLdapRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }
<repositories base-package="com.acme.repository"
     base-class="….MyRepositoryImpl" />