Couchbase リポジトリ

Spring Data リポジトリの抽象化のゴールは、さまざまな永続ストアのデータアクセス層を実装するために必要なボイラープレートコードの量を大幅に削減することです。

デフォルトでは、操作が単一ドキュメント操作であり、ID がわかっている場合、操作はキー / 値によってサポートされます。他のすべての操作では、デフォルトで N1QL クエリが生成されるため、データアクセスのパフォーマンスを高めるために適切なインデックスを作成する必要があります。

クエリに必要な一貫性を調整したり ( 一貫性のあるクエリを参照)、異なるバケットでバックアップされた異なるリポジトリを使用したりできることに注意してください。([couchbase.repository.multibucket] を参照)

構成

リポジトリのサポートは常に存在しますが、一般的に、または特定の名前空間に対して有効にする必要があります。AbstractCouchbaseConfiguration を継承する場合は、@EnableCouchbaseRepositories アノテーションを使用するだけです。検索パスを絞り込んだりカスタマイズしたりするためのオプションが多数用意されており、最も一般的なオプションの 1 つは basePackages です。

また、spring boot 内で実行している場合は、autoconfig サポートによってすでにアノテーションが設定されているため、デフォルトをオーバーライドする場合にのみアノテーションを使用する必要があることに注意してください。

例 1: アノテーションベースのリポジトリのセットアップ
@Configuration
@EnableCouchbaseRepositories(basePackages = {"com.couchbase.example.repos"})
public class Config extends AbstractCouchbaseConfiguration {
    //...
}

高度な使用箇所については、[couchbase.repository.multibucket] で説明されています。

QueryDSL 構成

Spring Data Couchbase は、型安全なクエリを構築するための QueryDSL をサポートしています。コード生成を有効にするには、CouchbaseAnnotationProcessor をアノテーションプロセッサーとして設定する必要があります。さらに、ランタイムでは、リポジトリで QueryDSL を有効にするために querydsl-apt が必要です。

例 2: Maven 構成例
    . existing depdendencies including those required for spring-data-couchbase
    .
    .
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>${querydslVersion}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>annotation-processing</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <proc>only</proc>
                            <annotationProcessors>
                                <annotationProcessor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</annotationProcessor>
                            </annotationProcessors>
                            <generatedTestSourcesDirectory>target/generated-sources</generatedTestSourcesDirectory>
                            <compilerArgs>
                                <arg>-Aquerydsl.logInfo=true</arg>
                            </compilerArgs>
                        </configuration>
                    </execution>
                </executions>
        </plugin>
    </plugins>
</build>
例 3: Gradle 構成例
dependencies {
    annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
    annotationProcessor 'org.springframework.data:spring-data-couchbase'
    testAnnotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}'
    testAnnotationProcessor 'org.springframework.data:spring-data-couchbase'
}
tasks.withType(JavaCompile).configureEach {
    options.compilerArgs += [
            "-processor",
            "org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor"]
}

使用方法

最も単純なケースでは、リポジトリは CrudRepository<T, String> を継承します。ここで、T は公開するエンティティです。UserInfo のリポジトリを見てみましょう。

例 4: UserInfo リポジトリ
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<UserInfo, String> {
}

これは単なるインターフェースであり、実際のクラスではないことに注意してください。コンテキストが初期化されると、バックグラウンドでリポジトリ記述の実際の実装が作成され、通常の Bean を介してそれらにアクセスできるようになります。これは、完全な CRUD セマンティクスをサービス層とアプリケーションに公開しながら、多くの定型コードを節約できることを意味します。

ここで、@Autowire を UserRepository を使用するクラスに送信すると想像してみましょう。どのような方法があるのでしょうか ?

表 1: UserRepository で公開されたメソッド
メソッド 説明

UserInfo 保存 (UserInfo エンティティ)

指定されたエンティティを保存します。

Iterable<UserInfo> save(Iterable<UserInfo> エンティティ)

エンティティのリストを保存します。

UserInfo findOne(文字列 ID)

一意の ID でエンティティを検索します。

ブール値が存在します。(文字列 ID)

指定されたエンティティがその一意の ID によって存在するかどうかを確認します。

Iterable<UserInfo> findAll()

バケット内のこの型のすべてのエンティティを検索します。

Iterable<UserInfo> findAll(Iterable<String> ID)

この型と指定された ID リストによってすべてのエンティティを検索します。

ロング count()

バケット内のエンティティの数を数えます。

void delete(文字列 ID)

ID に基づいてエンティティを削除します。

void 削除 (UserInfo エンティティ)

エンティティを削除します。

void delete(Iterable<UserInfo> エンティティ)

指定されたすべてのエンティティを削除します。

ボイド deleteAll()

バケット内の型ごとにすべてのエンティティを削除します。

それはすごいですね! インターフェースを定義するだけで、管理対象エンティティ上に完全な CRUD 機能が得られます。

公開されたメソッドは多種多様なアクセスパターンを提供しますが、多くの場合、カスタムパターンを定義する必要があります。これを行うには、インターフェースにメソッド宣言を追加します。次のセクションで説明するように、メソッド宣言はバックグラウンドでリクエストに対して自動的に解決されます。

リポジトリとクエリ

N1QL ベースのクエリ

前提条件は、エンティティが保存されるバケットに PRIMARY INDEX を作成していることです。

次に例を示します。

例 5: N1QL クエリを備えた拡張 UserInfo リポジトリ
public interface UserRepository extends CrudRepository<UserInfo, String> {

    @Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}")
    List<UserInfo> findAllAdmins();

    List<UserInfo> findByFirstname(String fname);
}

ここでは、N1QL に基づいた 2 つのクエリ方法を示します。

最初の方法では、Query アノテーションを使用して N1QL ステートメントをインラインで提供します。SpEL (Spring 式言語) は、#{ と } の間の周囲の SpEL 式ブロックによってサポートされます。いくつかの N1QL 固有の値が SpEL を通じて提供されます。

  • #n1ql.selectEntity を使用すると、完全なエンティティ (ドキュメント ID と CAS 値を含む) を構築するために必要なすべてのフィールドがステートメントで選択されることを簡単に確認できます。

  • WHERE 句の #n1ql.filter は、Spring Data が型情報を格納するために使用するフィールドとエンティティ型を一致させる条件を追加します。

  • #n1ql.bucket は、エンティティが保存されているバケットの名前に置き換えられ、バッククォートでエスケープされます。

  • #n1ql.scope は、エンティティが格納されているスコープの名前に置き換えられ、バッククォートでエスケープされます。

  • #n1ql.collection は、エンティティが格納されているコレクションの名前に置き換えられ、バッククォートでエスケープされます。

  • #n1ql.fields は、エンティティを再構築するために必要なフィールドのリスト (SELECT 句など) に置き換えられます。

  • #n1ql.delete は delete from ステートメントに置き換えられます。

  • #n1ql.returning は、エンティティの再構築に必要な return 句に置き換えられます。

常に selectEntity SpEL および filter SpEL とともに WHERE 句を使用することをお勧めします (そうしないと、クエリが他のリポジトリのエンティティの影響を受ける可能性があるため)。

文字列ベースのクエリはパラメーター化されたクエリをサポートします。"$1" のような位置プレースホルダーを使用することもできます。この場合、各メソッドパラメーターは順番に $1$2$3 にマップされます。あるいは、"$someString" 構文を使用して名前付きプレースホルダーを使用することもできます。メソッドのパラメーターは、パラメーターの名前を使用して対応するプレースホルダーと照合されます。このプレースホルダーは、各パラメーター ( Pageable または Sort を除く) に @Param (例: @Param("someString")) のアノテーションを付けることでオーバーライドできます。クエリ内で 2 つのアプローチを混合することはできず、混合すると IllegalArgumentException が発生します。

N1QL プレースホルダーと SpEL を混合できることに注意してください。N1QL プレースホルダーでは引き続きすべてのメソッドパラメーターが考慮されるため、次の例のように正しいインデックスを使用してください。

例 6: SpEL と N1QL プレースホルダーを混合したインラインクエリ
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND #{[0]} = $2")
public List<User> findUsersByDynamicCriteria(String criteriaField, Object criteriaValue)

これにより、たとえば以下と同様に機能するクエリを生成できます。AND name = "someName" または AND age = 3. メソッド宣言が 1 つあります。

N1QL クエリで単一の射影を実行することもできます (フィールドを 1 つだけ選択し、結果を 1 つだけ返します (通常は COUNTAVGMAX などの集計))。このような射影は、longbooleanString のような単純な戻り値の型を持ちます。これは、DTO への射影を目的としたものではありません。

もう一つの例:
 #{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND test = $1
と同等です
SELECT #{#n1ql.fields} FROM #{#n1ql.collection} WHERE #{#n1ql.filter} AND test = $1

Spring Security による SpEL の実践

SpEL は、Spring Security などの他の Spring コンポーネントによって挿入されたデータに応じてクエリを実行する場合に役立ちます。SpEL コンテキストを継承して、このような外部データにアクセスするために必要なことは次のとおりです。

まず、EvaluationContextExtension を実装する必要があります (以下のようにサポートクラスを使用します)。

class SecurityEvaluationContextExtension extends EvaluationContextExtensionSupport {

  @Override
  public String getExtensionId() {
    return "security";
  }

  @Override
  public SecurityExpressionRoot getRootObject() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return new SecurityExpressionRoot(authentication) {};
  }
}

Spring Data Couchbase が関連する SpEL 値にアクセスできるようにするために必要なのは、構成内で対応する Bean を宣言することだけです。

@Bean
EvaluationContextExtension securityExtension() {
    return new SecurityEvaluationContextExtension();
}

これは、接続されているユーザーのロールに応じてクエリを作成する場合に役立ちます。たとえば、次のようになります。

@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND " +
"role = '?#{hasRole('ROLE_ADMIN') ? 'public_admin' : 'admin'}'")
List<UserInfo> findAllAdmins(); //only ROLE_ADMIN users will see hidden admins

削除クエリの例:

@Query("#{#n1ql.delete} WHERE #{#n1ql.filter} AND " +
"username = $1 #{#n1ql.returning}")
UserInfo removeUser(String username);

2 番目の方法では、Spring-Data のクエリ導出メカニズムを使用して、メソッド名とパラメーターから N1QL クエリを構築します。これにより、SELECT …​ FROM …​ WHERE firstName = "valueOfFnameAtRuntime" のようなクエリが生成されます。これらの条件を組み合わせて、countByFirstname のような名前でカウントを実行したり、findFirst3ByLastname のような名前で制限を実行したりすることもできます。

実際には、生成された N1QL クエリには、リポジトリのエンティティクラスに一致するドキュメントのみを選択するための追加の N1QL 条件も含まれています。

ほとんどの Spring-Data キーワードがサポートされています: @Query (N1QL) メソッド名内のサポートされるキーワード

キーワード サンプル N1QL WHERE 句のスニペット

And

findByLastnameAndFirstname

lastName = a AND firstName = b

Or

findByLastnameOrFirstname

lastName = a OR firstName = b

Is,Equals

findByField,findByFieldEquals

field = a

IsNot,Not

findByFieldIsNot

field != a

Between

findByFieldBetween

field BETWEEN a AND b

IsLessThan,LessThan,IsBefore,Before

findByFieldIsLessThan,findByFieldBefore

field < a

IsLessThanEqual,LessThanEqual

findByFieldIsLessThanEqual

field ⇐ a

IsGreaterThan,GreaterThan,IsAfter,After

findByFieldIsGreaterThan,findByFieldAfter

field > a

IsGreaterThanEqual,GreaterThanEqual

findByFieldGreaterThanEqual

field >= a

IsNull

findByFieldIsNull

field IS NULL

IsNotNull,NotNull

findByFieldIsNotNull

field IS NOT NULL

IsLike,Like

findByFieldLike

field LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)

IsNotLike,NotLike

findByFieldNotLike

field NOT LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)

IsStartingWith,StartingWith,StartsWith

findByFieldStartingWith

field LIKE "a%" - a should be a String prefix

IsEndingWith,EndingWith,EndsWith

findByFieldEndingWith

field LIKE "%a" - a should be a String suffix

IsContaining,Containing,Contains

findByFieldContains

field LIKE "%a%" - a should be a String

IsNotContaining,NotContaining,NotContains

findByFieldNotContaining

field NOT LIKE "%a%" - a should be a String

IsIn,In

findByFieldIn

field IN array - note that the next parameter value (or its children if a collection/array) should be compatible for storage in a JsonArray)

IsNotIn,NotIn

findByFieldNotIn

field NOT IN array - note that the next parameter value (or its children if a collection/array) should be compatible for storage in a JsonArray)

IsTrue,True

findByFieldIsTrue

field = TRUE

IsFalse,False

findByFieldFalse

field = FALSE

MatchesRegex,Matches,Regex

findByFieldMatches

REGEXP_LIKE(field, "a") - note that the ignoreCase is ignored here, a is a regular expression in String form

Exists

findByFieldExists

field IS NOT MISSING - used to verify that the JSON contains this attribute

OrderBy

findByFieldOrderByLastnameDesc

field = a ORDER BY lastname DESC

IgnoreCase

findByFieldIgnoreCase

LOWER(field) = LOWER("a") - 文字列である必要があります

このアプローチでは、カウントクエリと [ リポジトリ .limit- クエリ結果 ] 機能の両方を使用できます。

N1QL では、リポジトリに使用できるもう 1 つのインターフェースは PagingAndSortingRepository ( CrudRepository を継承したもの) です。2 つのメソッドが追加されます。

表 2: PagingAndSortingRepository で公開されたメソッド
メソッド 説明

Iterable<T> findAll(ソートソート);

いずれかの属性に基づいて並べ替えながら、関連するすべてのエンティティを取得できます。

Page<T> findAll(ページング可能、ページング可能);

ページ内のエンティティを取得できるようにします。返された Page により、次のページの Pageable と項目のリストを簡単に取得できます。最初の呼び出しでは、new PageRequest(0, pageSize) を Pageable として使用します。

Page および Slice をメソッドの戻り値の型として使用したり、N1QL でサポートされたリポジトリを使用したりすることもできます。
インラインクエリでページング可能パラメーターと並べ替えパラメーターを使用する場合、インラインクエリ自体に order by、limit、offset 句を含めるべきではありません。そうしないと、サーバーはクエリを不正な形式として拒否します。

自動インデックス管理

デフォルトでは、ユーザーはクエリに最適なインデックスを作成および管理することが期待されています。特に開発の初期段階では、迅速に作業を進めるためにインデックスを自動的に作成すると便利です。

N1QL の場合、エンティティ (クラスまたはフィールドのいずれか) に添付する必要がある次のアノテーションが提供されます。

  • @QueryIndexed: このフィールドがインデックスの一部であることを示すためにフィールドに配置されます

  • @CompositeQueryIndex: 複数のフィールド (複合) のインデックスを作成する必要があることを通知するためにクラスに配置されます。

  • @CompositeQueryIndexes: 複数の CompositeQueryIndex を作成する必要がある場合、このアノテーションはそれらのリストを取得します。

例: これは、エンティティに複合インデックスを定義する方法です。

例 7: 順序付けされた 2 つのフィールドの複合インデックス
@Document
@CompositeQueryIndex(fields = {"id", "name desc"})
public class Airline {
   @Id
   String id;

	@QueryIndexed
	String name;

	@PersistenceConstructor
	public Airline(String id, String name) {
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}

}

デフォルトでは、インデックスの作成は無効になっています。これを有効にしたい場合は、設定でそれをオーバーライドする必要があります。

例 8: 自動インデックス作成を有効にする
@Override
protected boolean autoIndexCreation() {
 return true;
}

一貫性のあるクエリ

デフォルトでは、N1QL を使用するリポジトリクエリは NOT_BOUNDED スキャン整合性を使用します。これは、結果はすぐに返されますが、インデックスのデータには、以前に書き込まれた操作のデータがまだ含まれていない可能性があることを意味します (結果整合性と呼ばれます)。クエリに「独自の書き込みを準備する」セマンティクスが必要な場合は、@ScanConsistency アノテーションを使用する必要があります。以下に例を示します。

例 9: 異なるスキャン一貫性の使用
@Repository
public interface AirportRepository extends PagingAndSortingRepository<Airport, String> {

	@Override
	@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
	Iterable<Airport> findAll();

}

DTO の射影

Spring Data リポジトリは通常、クエリメソッドを使用するときにドメインモデルを返します。ただし、さまざまな理由から、そのモデルのビューを変更する必要がある場合があります。このセクションでは、リソースの簡略化された縮小ビューを提供するための射影を定義する方法を学習します。

次のドメインモデルを参照してください。

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

@Entity
public class Address {

  @Id @GeneratedValue
  private Long id;
  private String street, state, country;

  …
}

この Person にはいくつかの属性があります:

  • id は主キーです

  • firstName と lastName はデータ属性です

  • address は別のドメインオブジェクトへのリンクです

ここで、次のように対応するリポジトリを作成するとします。

interface PersonRepository extends CrudRepository<Person, Long> {

  Person findPersonByFirstName(String firstName);
}

Spring Data は、すべての属性を含むドメインオブジェクトを返します。address 属性を取得するには、2 つのオプションがあります。1 つのオプションは、次のように Address オブジェクトのリポジトリを定義することです。

interface AddressRepository extends CrudRepository<Address, Long> {}

この状況では、PersonRepository を使用すると、Person オブジェクト全体が返されます。AddressRepository を使用すると、Address のみが返されます。

しかし、address の詳細をまったく公開したくない場合はどうすればよいでしょうか ? 1 つ以上の射影を定義することで、リポジトリサービスのコンシューマーに代替手段を提供できます。

例 10: 単純な射影
interface NoAddresses {  (1)

  String getFirstName(); (2)

  String getLastName();  (3)
}

この射影には次の詳細があります。

1 宣言型にする単純な Java インターフェース。
2firstName をエクスポートします。
3lastName をエクスポートします。

NoAddresses 射影には、firstName および lastName に対して getter のみがあるため、アドレス情報は提供されません。この場合、クエリメソッド定義は Person ではなく NoAdresses を返します。

interface PersonRepository extends CrudRepository<Person, Long> {

  NoAddresses findByFirstName(String firstName);
}

射影は、基礎となる型と公開されたプロパティに関連するメソッドシグネチャー間の契約を宣言します。基礎となる型のプロパティ名に従って getter メソッドに名前を付ける必要があります。基礎となるプロパティの名前が firstName の場合、getter メソッドの名前は getFirstName にする必要があります。そうしないと、Spring Data はソースプロパティを検索できません。