MongoDB 固有のクエリメソッド

通常、リポジトリに対してトリガーするデータアクセス操作のほとんどは、MongoDB データベースに対してクエリを実行します。このようなクエリを定義するには、次の例に示すように、リポジトリインターフェースでメソッドを宣言する必要があります。

PersonRepository とクエリメソッド
  • 命令的

  • リアクティブ

public interface PersonRepository extends PagingAndSortingRepository<Person, String> {

    List<Person> findByLastname(String lastname);                      (1)

    Page<Person> findByFirstname(String firstname, Pageable pageable); (2)

    Person findByShippingAddresses(Address address);                   (3)

    Person findFirstByLastname(String lastname);                       (4)

    Stream<Person> findAllBy();                                        (5)
}
1findByLastname メソッドは、指定された姓を持つすべての人々に対するクエリを表示します。クエリは、And および Or と連結できる制約のメソッド名を解析することによって導出されます。メソッド名は {"lastname" : lastname} というクエリ式になります。
2 クエリにページネーションを適用します。メソッドシグネチャーに Pageable パラメーターを指定すると、メソッドが Page インスタンスを返し、それに応じて Spring Data がクエリを自動的にページングするようになります。
3 プリミティブ型ではないプロパティに基づいてクエリできることを示します。複数の一致が見つかった場合は、IncorrectResultSizeDataAccessException をスローします。
4First キーワードを使用して、クエリを最初の結果のみに制限します。<3> とは異なり、このメソッドは複数の一致が見つかった場合でも例外をスローしません。
5 ストリームを反復しながら個々の要素を読み取り、変換する Java 8 Stream を使用します。
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {

    Flux<Person> findByFirstname(String firstname);                                   (1)

    Flux<Person> findByFirstname(Publisher<String> firstname);                        (2)

    Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); (3)

    Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);       (4)

    Mono<Person> findFirstByLastname(String lastname);                                (5)
}
1 このメソッドは、指定された lastname を持つすべての人々のクエリを示します。クエリは、And および Or と連結できる制約のメソッド名を解析することによって導出されます。メソッド名は {"lastname" : lastname} のクエリ式になります。
2 このメソッドは、指定された Publisher によって firstname が発行されると、指定された firstname を持つすべての人々のクエリを示します。
3Pageable を使用して、オフセットと並べ替えのパラメーターをデータベースに渡します。
4 指定された条件で単一のエンティティを検索します。一意でない結果の場合は、IncorrectResultSizeDataAccessException で完了します。
5<4> を除き、クエリがより多くの結果ドキュメントを生成した場合でも、最初のエンティティは常に発行されます。
Page 戻り値の型 ( Mono<Page> など) は、リアクティブリポジトリではサポートされていません。

派生ファインダーメソッドで Pageable を使用して、sortlimitoffset パラメーターをクエリに渡し、負荷とネットワークトラフィックを軽減することができます。返された Flux は、宣言された範囲内のデータのみを出力します。

Pageable page = PageRequest.of(1, 10, Sort.by("lastname"));
Flux<Person> persons = repository.findByFirstnameOrderByLastname("luke", page);
ドメインクラスで DBRef としてマップされているパラメーターの参照はサポートされていません。
クエリメソッドでサポートされているキーワード
キーワード サンプル 論理的な結果

After

findByBirthdateAfter(Date date)

{"birthdate" : {"$gt" : date}}

GreaterThan

findByAgeGreaterThan(int age)

{"age" : {"$gt" : age}}

GreaterThanEqual

findByAgeGreaterThanEqual(int age)

{"age" : {"$gte" : age}}

Before

findByBirthdateBefore(Date date)

{"birthdate" : {"$lt" : date}}

LessThan

findByAgeLessThan(int age)

{"age" : {"$lt" : age}}

LessThanEqual

findByAgeLessThanEqual(int age)

{"age" : {"$lte" : age}}

Between

findByAgeBetween(int from, int to)
findByAgeBetween(Range<Integer> range)

{"age" : {"$gt" : from, "$lt" : to}}
lower / upper bounds ($gt / $gte & $lt / $lte) according to Range

In

findByAgeIn(Collection ages)

{"age" : {"$in" : [ages…​]}}

NotIn

findByAgeNotIn(Collection ages)

{"age" : {"$nin" : [ages…​]}}

IsNotNull, NotNull

findByFirstnameNotNull()

{"firstname" : {"$ne" : null}}

IsNull, Null

findByFirstnameNull()

{"firstname" : null}

Like, StartingWith, EndingWith

findByFirstnameLike(String name)

{"firstname" : name} (name as regex)

NotLike, IsNotLike

findByFirstnameNotLike(String name)

{"firstname" : { "$not" : name }} (name as regex)

Containing on String

findByFirstnameContaining(String name)

{"firstname" : name} (name as regex)

NotContaining on String

findByFirstnameNotContaining(String name)

{"firstname" : { "$not" : name}} (name as regex)

Containing on Collection

findByAddressesContaining(Address address)

{"addresses" : { "$in" : address}}

NotContaining on Collection

findByAddressesNotContaining(Address address)

{"addresses" : { "$not" : { "$in" : address}}}

Regex

findByFirstnameRegex(String firstname)

{"firstname" : {"$regex" : firstname }}

(No keyword)

findByFirstname(String name)

{"firstname" : name}

Not

findByFirstnameNot(String name)

{"firstname" : {"$ne" : name}}

Near

findByLocationNear(Point point)

{"location" : {"$near" : [x,y]}}

Near

findByLocationNear(Point point, Distance max)

{"location" : {"$near" : [x,y], "$maxDistance" : max}}

Near

findByLocationNear(Point point, Distance min, Distance max)

{"location" : {"$near" : [x,y], "$minDistance" : min, "$maxDistance" : max}}

Within

findByLocationWithin(Circle circle)

{"location" : {"$geoWithin" : {"$center" : [ [x, y], distance]}}}

Within

findByLocationWithin(Box box)

{"location" : {"$geoWithin" : {"$box" : [ [x1, y1], x2, y2]}}}

IsTrue, True

findByActiveIsTrue()

{"active" : true}

IsFalse, False

findByActiveIsFalse()

{"active" : false}

Exists

findByLocationExists(boolean exists)

{"location" : {"$exists" : exists }}

IgnoreCase

findByUsernameIgnoreCase(String username)

{"username" : {"$regex" : "^username$", "$options" : "i" }}

プロパティ基準がドキュメントを比較する場合、ドキュメント内のフィールドの順序と正確な同等性が重要になります。

地理空間クエリ

前のキーワードの表で見たように、いくつかのキーワードにより、MongoDB クエリ内の地理空間操作がトリガーされます。次のいくつかの例に示すように、Near キーワードを使用すると、さらに変更を加えることができます。

次の例は、指定された点から指定された距離にあるすべての人物を検索する near クエリを定義する方法を示しています。

高度な Near クエリ
  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    // { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
    List<Person> findByLocationNear(Point location, Distance distance);
}
interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    // { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
    Flux<Person> findByLocationNear(Point location, Distance distance);
}

Distance パラメーターをクエリメソッドに追加すると、結果を指定された距離内の結果に制限できます。Metric を含む Distance が設定されている場合、次の例に示すように、$code の代わりに $nearSphere を透過的に使用します。

例 1: Distance と Metrics の使用
Point point = new Point(43.7, 48.8);
Distance distance = new Distance(200, Metrics.KILOMETERS);
… = repository.findByLocationNear(point, distance);
// {'location' : {'$nearSphere' : [43.7, 48.8], '$maxDistance' : 0.03135711885774796}}
リアクティブ地理空間リポジトリクエリは、ドメイン型とリアクティブラッパー型内の GeoResult<T> 結果をサポートします。GeoPage および GeoResults は、平均距離を事前計算する結果遅延アプローチと矛盾するため、サポートされていません。ただし、Pageable 引数をページ結果に自分で渡すこともできます。

Distance を Metric とともに使用すると、(単純な $near の代わりに) $nearSphere 句が追加されます。それを超えると、実際の距離は使用される Metrics に従って計算されます。

(Metric はメートル法の測定単位を指すものではないことに注意してください。キロメートルではなくマイルである可能性もあります。むしろ、metric は、使用するシステムに関係なく、測定システムの概念を指します)

ターゲットプロパティで @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) を使用すると、$nearSphere 演算子の使用が強制されます。
  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    // {'geoNear' : 'location', 'near' : [x, y] }
    GeoResults<Person> findByLocationNear(Point location);

    // No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance }
    // Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance,
    //          'distanceMultiplier' : metric.multiplier, 'spherical' : true }
    GeoResults<Person> findByLocationNear(Point location, Distance distance);

    // Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min,
    //          'maxDistance' : max, 'distanceMultiplier' : metric.multiplier,
    //          'spherical' : true }
    GeoResults<Person> findByLocationNear(Point location, Distance min, Distance max);

    // {'geoNear' : 'location', 'near' : [x, y] }
    GeoResults<Person> findByLocationNear(Point location);
}
interface PersonRepository extends ReactiveMongoRepository<Person, String>  {

    // {'geoNear' : 'location', 'near' : [x, y] }
    Flux<GeoResult<Person>> findByLocationNear(Point location);

    // No metric: {'geoNear' : 'person', 'near' : [x, y], maxDistance : distance }
    // Metric: {'geoNear' : 'person', 'near' : [x, y], 'maxDistance' : distance,
    //          'distanceMultiplier' : metric.multiplier, 'spherical' : true }
    Flux<GeoResult<Person>> findByLocationNear(Point location, Distance distance);

    // Metric: {'geoNear' : 'person', 'near' : [x, y], 'minDistance' : min,
    //          'maxDistance' : max, 'distanceMultiplier' : metric.multiplier,
    //          'spherical' : true }
    Flux<GeoResult<Person>> findByLocationNear(Point location, Distance min, Distance max);

    // {'geoNear' : 'location', 'near' : [x, y] }
    Flux<GeoResult<Person>> findByLocationNear(Point location);
}

JSON ベースのクエリメソッドとフィールド制限

org.springframework.data.mongodb.repository.Query アノテーションをリポジトリクエリメソッドに追加すると、次の例に示すように、クエリをメソッド名から派生させる代わりに、使用する MongoDB JSON クエリ文字列を指定できます。

  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    @Query("{ 'firstname' : ?0 }")
    List<Person> findByThePersonsFirstname(String firstname);

}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    @Query("{ 'firstname' : ?0 }")
    Flux<Person> findByThePersonsFirstname(String firstname);

}

?0 プレースホルダーを使用すると、メソッド引数の値を JSON クエリ文字列に置き換えることができます。

String パラメーター値はバインドプロセス中にエスケープされます。つまり、引数を介して MongoDB 固有の演算子を追加することはできません。

次の例に示すように、フィルタープロパティを使用して、Java オブジェクトにマップされるプロパティのセットを制限することもできます。

  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    @Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}")
    List<Person> findByThePersonsFirstname(String firstname);

}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    @Query(value="{ 'firstname' : ?0 }", fields="{ 'firstname' : 1, 'lastname' : 1}")
    Flux<Person> findByThePersonsFirstname(String firstname);

}

前の例のクエリは、Person オブジェクトの firstnamelastnameId プロパティのみを返します。age プロパティ ( java.lang.Integer) は設定されていないため、その値は null です。

SpEL 式を使用した JSON ベースのクエリ

クエリ文字列とフィールド定義を SpEL 式と一緒に使用して、実行時に動的クエリを作成できます。SpEL 式は述語値を提供でき、サブドキュメントで述語を継承するために使用できます。

式は、すべての引数を含む配列を通じてメソッド引数を公開します。次のクエリは、[0] を使用して、lastname の述語値を宣言します(これは、?0 パラメーターバインディングと同等です)。

  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    @Query("{'lastname': ?#{[0]} }")
    List<Person> findByQueryWithExpression(String param0);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    @Query("{'lastname': ?#{[0]} }")
    Flux<Person> findByQueryWithExpression(String param0);
}

式を使用して、関数の呼び出し、条件の評価、値の構築を行うことができます。次の例に示すように、SpEL 内の Map のような宣言は JSON のように読み取られるため、JSON と組み合わせて SpEL 式を使用すると副作用が明らかになります。

  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    @Query("{'id': ?#{ [0] ? {$exists :true} : [1] }}")
    List<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);
}
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    @Query("{'id': ?#{ [0] ? {$exists :true} : [1] }}")
    Flux<Person> findByQueryWithExpressionAndNestedObject(boolean param0, String param1);
}
クエリ文字列内の SpEL は、クエリを強化する強力な方法となります。ただし、さまざまな不要な議論を受け入れることもできます。脆弱性の作成やクエリへの不要な変更を避けるために、文字列をクエリに渡す前に必ずサニタイズしてください。

式のサポートは、Query SPI: EvaluationContextExtension および ReactiveEvaluationContextExtension を通じて拡張可能です。Query SPI は、プロパティと関数を提供し、ルートオブジェクトをカスタマイズできます。拡張機能は、クエリの構築時の SpEL 評価時にアプリケーションコンテキストから取得されます。次の例は、評価コンテキスト拡張機能の使用方法を示しています。

  • 命令的

  • リアクティブ

public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport {

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

    @Override
    public Map<String, Object> getProperties() {
        return Collections.singletonMap("principal", SecurityContextHolder.getCurrent().getPrincipal());
    }
}
public class SampleEvaluationContextExtension implements ReactiveEvaluationContextExtension {

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

    @Override
    public Mono<? extends EvaluationContextExtension> getExtension() {
        return Mono.just(new EvaluationContextExtensionSupport() { ... });
    }
}
MongoRepositoryFactory を自分でブートストラップする場合はアプリケーションのコンテキストを認識しないため、Query SPI 拡張機能を取得するにはさらに構成が必要です。
リアクティブクエリメソッドでは org.springframework.data.spel.spi.ReactiveEvaluationContextExtension を利用できます。

全文検索クエリ

MongoDB の全文検索機能はストア固有であるため、より一般的な CrudRepository ではなく MongoRepository で見つけることができます。フルテキストインデックスを持つドキュメントが必要です (フルテキストインデックスの作成方法については、"テキストインデックス" を参照してください)。

MongoRepository の追加メソッドは、TextCriteria を入力パラメーターとして受け取ります。これらの明示的なメソッドに加えて、TextCriteria -derived リポジトリメソッドを追加することもできます。この条件は、追加の AND 条件として追加されます。エンティティに @TextScore アノテーション付きプロパティが含まれると、ドキュメントの全文スコアを取得できます。さらに、次の例に示すように、アノテーション付き @TextScore を使用すると、ドキュメントのスコアによって並べ替えることもできます。

@Document
class FullTextDocument {

  @Id String id;
  @TextIndexed String title;
  @TextIndexed String content;
  @TextScore Float score;
}

interface FullTextRepository extends Repository<FullTextDocument, String> {

  // Execute a full-text search and define sorting dynamically
  List<FullTextDocument> findAllBy(TextCriteria criteria, Sort sort);

  // Paginate over a full-text search result
  Page<FullTextDocument> findAllBy(TextCriteria criteria, Pageable pageable);

  // Combine a derived query with a full-text search
  List<FullTextDocument> findByTitleOrderByScoreDesc(String title, TextCriteria criteria);
}


Sort sort = Sort.by("score");
TextCriteria criteria = TextCriteria.forDefaultLanguage().matchingAny("spring", "data");
List<FullTextDocument> result = repository.findAllBy(criteria, sort);

criteria = TextCriteria.forDefaultLanguage().matching("film");
Page<FullTextDocument> page = repository.findAllBy(criteria, PageRequest.of(1, 1, sort));
List<FullTextDocument> result = repository.findByTitleOrderByScoreDesc("mongodb", criteria);

集計方法

リポジトリ層は、アノテーション付きのリポジトリクエリメソッドを介して集約フレームワークと対話する手段を提供します。JSON ベースのクエリと同様に、org.springframework.data.mongodb.repository.Aggregation アノテーションを使用してパイプラインを定義できます。定義には、?0 や SpEL 式  ?#{ … } などの単純なプレースホルダーが含まれる場合があります。

例 2: リポジトリメソッドの集約
public interface PersonRepository extends CrudRepository<Person, String> {

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  List<PersonAggregate> groupByLastnameAndFirstnames();                            (1)

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  List<PersonAggregate> groupByLastnameAndFirstnames(Sort sort);                   (2)

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : ?0 } } }")
  List<PersonAggregate> groupByLastnameAnd(String property);                       (3)

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : ?0 } } }")
  Slice<PersonAggregate> groupByLastnameAnd(String property, Pageable page);       (4)

  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  Stream<PersonAggregate> groupByLastnameAndFirstnamesAsStream();                  (5)

  @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
  SumValue sumAgeUsingValueWrapper();                                              (6)

  @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
  Long sumAge();                                                                   (7)

  @Aggregation("{ $group : { _id : null, total : { $sum : $age } } }")
  AggregationResults<SumValue> sumAgeRaw();                                        (8)

  @Aggregation("{ '$project': { '_id' : '$lastname' } }")
  List<String> findAllLastnames();                                                 (9)

  @Aggregation(pipeline = {
		  "{ $group : { _id : '$author', books: { $push: '$title' } } }",
		  "{ $out : 'authors' }"
  })
  void groupAndOutSkippingOutput();                                                (10)
}
public class PersonAggregate {

  private @Id String lastname;                                                     (2)
  private List<String> names;

  public PersonAggregate(String lastname, List<String> names) {
     // ...
  }

  // Getter / Setter omitted
}

public class SumValue {

  private final Long total;                                                        (6) (8)

  public SumValue(Long total) {
    // ...
  }

  // Getter omitted
}
1Person コレクション内の名を lastname ごとにグループ化し、これらを PersonAggregate として返す集計パイプライン。
2Sort 引数が存在する場合、$sort は宣言されたパイプラインステージの後に追加されるため、他のすべての集計ステージを通過した後の最終結果の順序にのみ影響します。PersonAggregate.lastname には @Id のアノテーションが付けられているため、Sort プロパティはメソッドの戻り値の型 PersonAggregate に対してマップされます。これにより、Sort.by("lastname") は { $sort : { '_id', 1 } } に変わります。
3 動的集約パイプラインの ?0 を property の指定された値に置き換えます。
4$skip$limit$sort は、Pageable 引数を介して渡すことができます。<2> と同様にパイプライン定義に演算子を追加します。Pageable を受け入れるメソッドは、ページネーションを容易にするために Slice を返すことができます。
5 集計メソッドは Stream を返し、基礎となるカーソルからの結果を直接消費できます。ストリームを消費した後は必ずストリームを閉じて、close() を呼び出すか、try-with-resources を介してサーバー側カーソルを解放してください。
6 単一の Document を返す集約の結果を、目的の SumValue ターゲット型のインスタンスにマップします。
7 集計の結果、蓄積結果のみを保持する単一のドキュメントが生成されます。$sum は、結果 Document から直接抽出できます。より詳細な制御を得るには、<7> に示すように、メソッドの戻り値の型として AggregationResult を検討することをお勧めします。
8 汎用ターゲットラッパー型 SumValue または org.bson.Document にマップされた生の AggregationResults を取得します。
9<6> と同様に、複数の結果 Document から 1 つの値を直接取得できます。
10 戻り値の型が void の場合、$out ステージの出力をスキップします。

一部のシナリオでは、集計には、最大実行時間、追加のログコメント、一時的にデータをディスクに書き込む権限などの追加オプションが必要になる場合があります。@Meta アノテーションを使用して、maxExecutionTimeMscomment または allowDiskUse 経由でこれらのオプションを設定します。

interface PersonRepository extends CrudRepository<Person, String> {

  @Meta(allowDiskUse = true)
  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  List<PersonAggregate> groupByLastnameAndFirstnames();
}

または、以下のサンプルに示すように、@Meta を使用して独自のアノテーションを作成します。

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@Meta(allowDiskUse = true)
@interface AllowDiskUse { }

interface PersonRepository extends CrudRepository<Person, String> {

  @AllowDiskUse
  @Aggregation("{ $group: { _id : $lastname, names : { $addToSet : $firstname } } }")
  List<PersonAggregate> groupByLastnameAndFirstnames();
}

単純型の単一結果は、返された Document をインスペクションし、以下をチェックします。

  1. ドキュメント内のエントリは 1 つだけのため、返却してください。

  2. 2 つのエントリ、1 つは _id 値です。もう一方を返します。

  3. 戻り値の型に割り当て可能な最初の値を返します。

  4. 上記のいずれにも当てはまらない場合は、例外をスローします。

Page 戻り値の型は、@Aggregation を使用するリポジトリメソッドではサポートされていません。ただし、Pageable 引数を使用して $skip$limit$sort をパイプラインに追加し、メソッドが Slice を返すようにすることができます。

例示による問い合わせ

導入

この章では、Query by Example の概要とその使用方法について説明します。

Query by Example(QBE)は、シンプルなインターフェースを備えた使いやすいクエリ手法です。動的なクエリの作成が可能になり、フィールド名を含むクエリを作成する必要がなくなります。実際、Query by Example では、ストア固有のクエリ言語を使用してクエリを記述する必要はまったくありません。

この章では、Query by Example の中心的な概念について説明します。情報は Spring Data Commons モジュールから取得されます。データベースによっては、文字列一致のサポートが制限される場合があります。

使用方法

例示による問い合わせ API は、次の 4 つの部分で構成されています。

  • プローブ: フィールドが設定されたドメインオブジェクトの実際の例。

  • ExampleMatcherExampleMatcher には、特定のフィールドの照合方法に関する詳細が記載されています。複数の例で再利用できます。

  • ExampleExample は、プローブと ExampleMatcher で構成されています。クエリの作成に使用されます。

  • FetchableFluentQueryFetchableFluentQuery は、Example から派生したクエリをさらにカスタマイズできる流れるような API を提供します。Fluent API を使用すると、クエリの順序付け射影と結果処理を指定できます。

例示による問い合わせは、いくつかのユースケースに適しています。

  • 静的または動的な制約のセットを使用してデータストアをクエリします。

  • 既存のクエリを壊すことを心配せずにドメインオブジェクトを頻繁にリファクタリングします。

  • 基盤となるデータストア API から独立して動作します。

例示による問い合わせには、いくつかの制限もあります。

  • firstname = ?0 or (firstname = ?1 and lastname = ?2) など、ネストまたはグループ化されたプロパティ制約はサポートされていません。

  • 文字列マッチングにおけるストア固有のサポート。データベースによっては、文字列一致では文字列の開始 / 含む / 終了 / 正規表現をサポートできます。

  • 他のプロパティ型と完全に一致します。

Query by Example を開始する前に、ドメインオブジェクトが必要です。開始するには、次の例に示すように、リポジトリのインターフェースを作成します。

サンプル Person オブジェクト
public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}

前の例は、単純なドメインオブジェクトを示しています。これを使用して Example を作成できます。デフォルトでは、null 値を持つフィールドは無視され、文字列はストア固有のデフォルトを使用して照合されます。

例示による問い合わせ条件へのプロパティの包含は、null 可能性に基づいています。プリミティブ型(intdouble、…)を使用するプロパティは、ExampleMatcher はプロパティパスを無視しますでない限り、常に含まれます。

例は、of ファクトリメソッドを使用するか、ExampleMatcher を使用して作成できます。Example は不変です。次のリストは、簡単な例を示しています。

例 3: 簡単な例
Person person = new Person();                         (1)
person.setFirstname("Dave");                          (2)

Example<Person> example = Example.of(person);         (3)
1 ドメインオブジェクトの新しいインスタンスを作成します。
2 クエリにプロパティを設定します。
3Example を作成します。

リポジトリを使用して、サンプルクエリを実行できます。これを行うには、リポジトリインターフェースに QueryByExampleExecutor<T> を継承させます。次のリストは、QueryByExampleExecutor インターフェースからの抜粋を示しています。

QueryByExampleExecutor
public interface QueryByExampleExecutor<T> {

  <S extends T> S findOne(Example<S> example);

  <S extends T> Iterable<S> findAll(Example<S> example);

  // … more functionality omitted.
}

マッチャーの例

例はデフォルト設定に限定されません。次の例に示すように、ExampleMatcher を使用して、文字列照合、null 処理、プロパティ固有の設定に独自のデフォルトを指定できます。

例 4: カスタマイズされたマッチングを使用したマッチャーの例
Person person = new Person();                          (1)
person.setFirstname("Dave");                           (2)

ExampleMatcher matcher = ExampleMatcher.matching()     (3)
  .withIgnorePaths("lastname")                         (4)
  .withIncludeNullValues()                             (5)
  .withStringMatcher(StringMatcher.ENDING);            (6)

Example<Person> example = Example.of(person, matcher); (7)
1 ドメインオブジェクトの新しいインスタンスを作成します。
2 セットのプロパティ。
3ExampleMatcher を作成して、すべての値が一致することを期待します。この段階では、さらに構成しなくても使用できます。
4lastname プロパティパスを無視する新しい ExampleMatcher を構築します。
5 新しい ExampleMatcher を作成して、lastname プロパティパスを無視し、null 値を含めます。
6 新しい ExampleMatcher を作成して、lastname プロパティパスを無視し、null 値を含め、サフィックス文字列の照合を実行します。
7 ドメインオブジェクトと設定された ExampleMatcher に基づいて新しい Example を作成します。

デフォルトでは、ExampleMatcher はプローブに設定されたすべての値が一致することを期待しています。暗黙的に定義された述語のいずれかに一致する結果を取得する場合は、ExampleMatcher.matchingAny() を使用します。

個々のプロパティ(「名」や「姓」、ネストされたプロパティの場合は "address.city" など)の動作を指定できます。次の例に示すように、一致するオプションと大文字と小文字の区別を使用して調整できます。

マッチャーオプションの構成
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

マッチャーオプションを構成する別の方法は、ラムダ(Java 8 で導入)を使用することです。このアプローチは、実装者にマッチャーの変更を要求するコールバックを作成します。設定オプションはマッチャーインスタンス内に保持されているため、マッチャーを返す必要はありません。次の例は、ラムダを使用するマッチャーを示しています。

ラムダを使用したマッチャーオプションの構成
ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", match -> match.endsWith())
  .withMatcher("firstname", match -> match.startsWith());
}

Example によって作成されたクエリは、構成の統合ビューを使用します。デフォルトのマッチング設定は ExampleMatcher レベルで設定できますが、個々の設定は特定のプロパティパスに適用できます。ExampleMatcher で設定された設定は、明示的に定義されていない限り、プロパティパス設定に継承されます。プロパティパッチの設定は、デフォルト設定よりも優先されます。次の表は、さまざまな ExampleMatcher 設定の範囲を説明しています。

表 1: ExampleMatcher 設定の範囲
設定 スコープ

null ハンドリング

ExampleMatcher

文字列マッチング

ExampleMatcher とプロパティパス

プロパティを無視する

プロパティパス

大文字と小文字の区別

ExampleMatcher とプロパティパス

値変換

プロパティパス

Fluent API

QueryByExampleExecutor は、これまでメンションしなかったもう 1 つの方法、<S extends T, R> R findBy(Example<S> example, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction) を提供します。他のメソッドと同様に、Example から派生したクエリを実行します。ただし、2 番目の引数を使用すると、他の方法では動的に制御できない実行の側面を制御できます。これを行うには、2 番目の引数で FetchableFluentQuery のさまざまなメソッドを呼び出します。sortBy を使用すると、結果の順序を指定できます。as では、結果を変換する型を指定できます。project は、照会される属性を制限します。firstfirstValueoneoneValueallpagestreamcountexists は、取得する結果の種類と、予想よりも多くの結果が利用可能な場合のクエリの動作を定義します。

流れるような API を使用して、潜在的に多くの結果の最後を姓順に取得します。
Optional<Person> match = repository.findBy(example,
    q -> q
        .sortBy(Sort.by("lastname").descending())
        .first()
);

例の実行

次の例は、リポジトリ (この場合は Person オブジェクト) を使用する場合にクエリを実行する方法を示しています。

例 5: リポジトリを使用した例示による問い合わせ
public interface PersonRepository extends QueryByExampleExecutor<Person> {

}

public class PersonService {

  @Autowired PersonRepository personRepository;

  public List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}

スクロール

スクロールは、より大きな結果セットのチャンクを反復処理するための、よりきめ細かいアプローチです。スクロールは、安定ソート、スクロール型 (オフセットまたはキーセットベースのスクロール)、および結果の制限で構成されます。プロパティ名を使用して単純な並べ替え式を定義し、クエリの派生を通じて Top または First キーワードを使用して静的な結果制限を定義できます。式を連結して、複数の条件を 1 つの式にまとめることができます。

スクロールクエリは、アプリケーションがクエリ結果全体を処理するまで、要素のスクロール位置を取得して次の Window<T> を取得できる Window<T> を返します。次の結果バッチを取得して Java Iterator<List< …>> を処理するのと同様に、クエリ結果のスクロールにより、ScrollPosition から Window.positionAt(…​) にアクセスできます。

Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());

ScrollPosition は、クエリ結果全体を使用して要素の正確な位置を識別します。クエリ実行では位置パラメーターが排他的に扱われ、結果は指定された位置以降に開始されます。ScrollPosition#offset() と ScrollPosition#keyset() は、スクロール操作の開始を示す ScrollPosition の特別なバージョンです。

WindowIterator は、次の Window の存在をチェックする必要をなくし、ScrollPosition を適用することにより、Window 間のスクロールを簡素化するユーティリティを提供します。

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.offset());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

オフセットを使用したスクロール

オフセットスクロールは、ページネーションと同様に、オフセットカウンターを使用して多数の結果をスキップし、データソースが特定のオフセットで始まる結果のみを返すようにします。この単純なメカニズムにより、大量の結果がクライアントアプリケーションに送信されるのを回避できます。ただし、ほとんどのデータベースでは、サーバーが結果を返す前に完全なクエリ結果を具体化する必要があります。

例 6: リポジトリクエリメソッドでの OffsetScrollPosition の使用
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial()); (1)
1 位置 0 の要素を含めるには、オフセットなしで開始します。

ScollPosition.offset() と ScollPosition.offset(0L) には違いがあります。前者はスクロール操作の開始を示し、特定のオフセットを指しませんが、後者は結果の最初の要素 (位置 0) を識別します。スクロールの排他性を考慮すると、ScollPosition.offset(0) を使用すると最初の要素がスキップされ、1 のオフセットに変換されます。

Keyset-Filtering を使用したスクロール

オフセットベースの要件では、サーバーが結果を返す前に、ほとんどのデータベースで結果全体をマテリアライズする必要があります。そのため、クライアントはリクエストされた結果の一部しか表示しませんが、サーバーは完全な結果を構築する必要があり、追加の負荷が発生します。

Keyset-Filtering は、個々のクエリの計算と I/O 要件を削減することを目的として、データベースの組み込み機能を活用することにより、結果サブセットの取得にアプローチします。このアプローチでは、キーをクエリに渡すことでスクロールを再開するキーのセットを維持し、フィルター条件を効果的に修正します。

Keyset-Filtering の核となる考え方は、安定した並べ替え順序を使用して結果の取得を開始することです。次のチャンクにスクロールしたい場合は、並べ替えられた結果内の位置を再構築するために使用される ScrollPosition を取得します。ScrollPosition は、現在の Window 内の最後のエンティティのキーセットをキャプチャーします。クエリを実行するために、再構築によって条件句が書き直され、すべての並べ替えフィールドと主キーが含まれるようになります。これにより、データベースは潜在的なインデックスを利用してクエリを実行できるようになります。データベースは、指定されたキーセット位置からはるかに小さな結果を構築するだけでよく、大きな結果を完全にマテリアライズして特定のオフセットに到達するまで結果をスキップする必要はありません。

Keyset-Filtering では、キーセットプロパティ (並べ替えに使用されるもの) が null 非許容である必要があります。この制限は、比較演算子のストア固有の null 値の処理と、インデックス付きソースに対してクエリを実行する必要があるために適用されます。null 許容プロパティの Keyset-Filtering は、予期しない結果につながります。

リポジトリクエリメソッドでの KeysetScrollPosition の使用
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.keyset()); (1)
1 最初から開始し、追加のフィルタリングを適用しないでください。

Keyset-Filtering は、データベースに並べ替えフィールドに一致するインデックスが含まれている場合に最適に機能するため、静的並べ替えが適切に機能します。Keyset-Filtering を適用するスクロールクエリでは、並べ替え順序で使用されるプロパティがクエリによって返される必要があり、これらは返されるエンティティにマップされる必要があります。

インターフェースと DTO 射影を使用できますが、キーセット抽出の失敗を避けるために、並べ替えたすべてのプロパティを含めるようにしてください。

Sort 順序を指定するときは、クエリに関連する並べ替えプロパティを含めるだけで十分です。一意のクエリ結果を保証したくない場合は、その必要はありません。キーセットクエリメカニズムは、各クエリ結果が一意であることを保証するために、主キー (または複合主キーの残りの部分) を含めることによって並べ替え順序を修正します。

結果の並べ替え

MongoDB リポジトリでは、さまざまな方法で並べ替え順序を定義できます。次の例を見てみましょう。

クエリ結果の並べ替え
  • 命令的

  • リアクティブ

public interface PersonRepository extends MongoRepository<Person, String> {

    List<Person> findByFirstnameSortByAgeDesc(String firstname); (1)

    List<Person> findByFirstname(String firstname, Sort sort);   (2)

    @Query(sort = "{ age : -1 }")
    List<Person> findByFirstname(String firstname);              (3)

    @Query(sort = "{ age : -1 }")
    List<Person> findByLastname(String lastname, Sort sort);     (4)
}
1 メソッド名から派生した静的ソート。SortByAgeDesc の場合、sort パラメーターは { age : -1 } になります。
2 メソッド引数を使用した動的ソート。Sort.by(DESC, "age") は、sort パラメーターの { age : -1 } を作成します。
3Query アノテーションによる静的ソート。sort 属性に記載されているように、並べ替えパラメーターが適用されます。
4Query アノテーションによるデフォルトの並べ替えと、メソッド引数による動的並べ替えを組み合わせました。Sort.unsorted() の結果は { age : -1 } になります。Sort.by(ASC, "age") を使用すると、デフォルトがオーバーライドされ、{ age : 1 } が作成されます。Sort.by (ASC, "firstname") はデフォルトを変更し、結果は { age : -1, firstname : 1 } になります。
public interface PersonRepository extends ReactiveMongoRepository<Person, String> {

    Flux<Person> findByFirstnameSortByAgeDesc(String firstname);

    Flux<Person> findByFirstname(String firstname, Sort sort);

    @Query(sort = "{ age : -1 }")
    Flux<Person> findByFirstname(String firstname);

    @Query(sort = "{ age : -1 }")
    Flux<Person> findByLastname(String lastname, Sort sort);
}

インデックスのヒント

@Hint アノテーションを使用すると、MongoDB のデフォルトのインデックス選択をオーバーライドし、代わりに指定されたインデックスをデータベースに強制的に使用させることができます。

例 7: インデックスヒントの例
@Hint("lastname-idx")                                          (1)
List<Person> findByLastname(String lastname);

@Query(value = "{ 'firstname' : ?0 }", hint = "firstname-idx") (2)
List<Person> findByFirstname(String firstname);
1lastname-idx という名前のインデックスを使用します。
2@Query アノテーションは、@Hint アノテーションを追加するのと同じ hint エイリアスを定義します。

インデックス作成の詳細については、"コレクション管理" セクションを参照してください。

照合のサポート

一般的な照合順序サポートリポジトリの隣では、さまざまな操作の照合順序を定義できます。

public interface PersonRepository extends MongoRepository<Person, String> {

  @Query(collation = "en_US")  (1)
  List<Person> findByFirstname(String firstname);

  @Query(collation = "{ 'locale' : 'en_US' }") (2)
  List<Person> findPersonByFirstname(String firstname);

  @Query(collation = "?1") (3)
  List<Person> findByFirstname(String firstname, Object collation);

  @Query(collation = "{ 'locale' : '?1' }") (4)
  List<Person> findByFirstname(String firstname, String collation);

  List<Person> findByFirstname(String firstname, Collation collation); (5)

  @Query(collation = "{ 'locale' : 'en_US' }")
  List<Person> findByFirstname(String firstname, @Nullable Collation collation); (6)
}
1 静的照合定義により { 'locale' : 'en_US' } が生成されます。
2 静的照合定義により { 'locale' : 'en_US' } が生成されます。
3 メソッドの 2 番目の引数に応じた動的照合。許可される型には、String (例: 'en_US' )、Locacle (例: Locacle.US)、および Document (例: new Document("locale" , "en_US" )) が含まれます。
4 メソッドの 2 番目の引数に応じた動的照合。
5Collation メソッドパラメーターをクエリに適用します。
6Collation メソッドパラメーターは、null でない場合、@Query のデフォルトの collation をオーバーライドします。
リポジトリファインダーメソッドの自動インデックス作成を有効にした場合、(1) および (2) に示すように、潜在的な静的照合順序定義がインデックスの作成時に組み込まれます。
最も具体的な Collation は、潜在的に定義されている他のものよりも優先されます。これは、ドメイン型 アノテーションよりもクエリメソッドアノテーションよりもメソッド引数を意味します。

コードベース全体で照合属性の使用を効率化するために、上記のアノテーションのメタアノテーションとして機能する @Collation アノテーションを使用することもできます。同じルールと場所が適用されるだけでなく、@Collation の直接使用は、@Query およびその他のアノテーションで定義された照合値よりも優先されます。つまり、照合順序が @Query を介して宣言され、さらに @Collation を介して宣言された場合、@Collation からの照合順序が選択されます。

例 8: @Collation を使用する
@Collation("en_US") (1)
class Game {
  // ...
}

interface GameRepository extends Repository<Game, String> {

  @Collation("en_GB")  (2)
  List<Game> findByTitle(String title);

  @Collation("de_AT")  (3)
  @Query(collation="en_GB")
  List<Game> findByDescriptionContaining(String keyword);
}
1@Document(collation=…​) の代わり。
2@Query(collation=…​) の代わり。
3 メタ使用よりも @Collation を優先します。

設定の読み取り

@ReadPreference アノテーションを使用すると、MongoDB の ReadPreferences を構成できます。

例 9: 読み取り設定の例
@ReadPreference("primaryPreferred") (1)
public interface PersonRepository extends CrudRepository<Person, String> {

    @ReadPreference("secondaryPreferred") (2)
    List<Person> findWithReadPreferenceAnnotationByLastname(String lastname);

    @Query(readPreference = "nearest") (3)
    List<Person> findWithReadPreferenceAtTagByFirstname(String firstname);

    List<Person> findWithReadPreferenceAtTagByFirstname(String firstname); (4)
1 クエリレベルの定義を持たないすべてのリポジトリ操作 (継承された非カスタム実装操作を含む) の読み取り設定を構成します。この場合、読み取り優先モードは primaryPreferred になります。
2 アノテーション ReadPreference で定義された読み取り優先モードを使用します(この場合は secondaryPreferred)
3@Query アノテーションは、@ReadPreference アノテーションを追加するのと同じ read preference mode エイリアスを定義します。
4 このクエリは、リポジトリで定義された読み取り優先モードを使用します。

MongoOperations および Query API は、ReadPreference に対してより詳細な制御を提供します。