1. 概要

Spring Data MongoDBは、MongoDBネイティブクエリ言語にシンプルで高レベルの抽象化を提供します。 この記事では、プロジェクションとアグリゲーションフレームワークのサポートについて説明します。

このトピックを初めて使用する場合は、紹介記事 Spring DataMongoDBの概要を参照してください。

2. 投影

MongoDBでは、プロジェクションは、データベースからドキュメントの必須フィールドのみをフェッチする方法です。 これにより、データベースサーバーからクライアントに転送する必要のあるデータの量が減り、パフォーマンスが向上します。

Spring Data MongDBを使用すると、MongoTemplateMongoRepositoryの両方でプロジェクションを使用できます。

先に進む前に、使用するデータモデルを見てみましょう。

@Document
public class User {
    @Id
    private String id;
    private String name;
    private Integer age;
    
    // standard getters and setters
}

2.1. MongoTemplateを使用したプロジェクション

Fieldクラスのinclude()メソッドと exclude()メソッドは、それぞれフィールドの包含と除外に使用されます。

Query query = new Query();
query.fields().include("name").exclude("id");
List<User> john = mongoTemplate.find(query, User.class);

これらのメソッドをチェーン化して、複数のフィールドを含めたり除外したりできます。 @Id (データベースでは _id )とマークされたフィールドは、明示的に除外されていない限り、常にフェッチされます。

レコードがプロジェクションでフェッチされる場合、モデルクラスインスタンスで除外されるフィールドはnullです。 フィールドがプリミティブ型またはそのラッパークラスである場合、除外されたフィールドの値はプリミティブ型のデフォルト値です。

たとえば、Stringnull int / Integer0 boolean[ X118X] / Booleanfalseになります。

したがって、上記の例では、nameフィールドはJohnidnullageは次のようになります。 0になります。

2.2. MongoRepositoryを使用したプロジェクション

MongoRepositoriesを使用している場合、@QueryアノテーションのfieldsはJSON形式で定義できます。

@Query(value="{}", fields="{name : 1, _id : 0}")
List<User> findNameAndExcludeId();

結果は、MongoTemplateを使用した場合と同じになります。 value =” {}” はフィルターがないことを示しているため、すべてのドキュメントがフェッチされます。

3. 集約

MongoDBの集計は、データを処理して計算結果を返すために構築されました。 データは段階的に処理され、1つの段階の出力が次の段階への入力として提供されます。 変換を適用し、段階的にデータに対して計算を行うこの機能により、集計は分析のための非常に強力なツールになります。

Spring Data MongoDBは、集約クエリをラップする Aggregation 、個々のパイプラインステージをラップする AggregationOperation 、およびAggregationResultsの3つのクラスを使用してネイティブ集約クエリの抽象化を提供します。集計によって生成された結果のコンテナー。

実行して集計するには、まず Aggregation クラスの静的ビルダーメソッドを使用して集計パイプラインを作成し、次に newAggregation()メソッドを使用してAggregationのインスタンスを作成します。 Aggregation クラスを作成し、最後にMongoTemplateを使用して集計を実行します。

MatchOperation matchStage = Aggregation.match(new Criteria("foo").is("bar"));
ProjectionOperation projectStage = Aggregation.project("foo", "bar.baz");
        
Aggregation aggregation 
  = Aggregation.newAggregation(matchStage, projectStage);

AggregationResults<OutType> output 
  = mongoTemplate.aggregate(aggregation, "foobar", OutType.class);

MatchOperationProjectionOperationの両方がAggregationOperationを実装していることに注意してください。 他の集約パイプラインにも同様の実装があります。 OutType は、期待される出力のデータモデルです。

ここで、主要な集約パイプラインとオペレーターをカバーするために、いくつかの例とその説明を見ていきます。

この記事で使用するデータセットには、MongoDBリポジトリからダウンロードできる米国内のすべての郵便番号の詳細がリストされています。

testデータベースのzipsというコレクションにインポートした後、サンプルドキュメントを見てみましょう。

{
    "_id" : "01001",
    "city" : "AGAWAM",
    "loc" : [
        -72.622739,
        42.070206
    ],
    "pop" : 15338,
    "state" : "MA"
}

わかりやすくするため、またコードを簡潔にするために、次のコードスニペットでは、Aggregationクラスのすべてのstaticメソッドが静的にインポートされると想定します。

3.1. 人口の降順で人口が1,000万人を超えるすべての州を取得する

ここでは、3つのパイプラインがあります。

  1. $groupステージですべての郵便番号の人口を合計
  2. $ match ステージで、人口が1,000万人を超える州を除外します
  3. $ sort ステージで、すべてのドキュメントを人口の降順で並べ替えます

期待される出力には、状態としてのフィールド _id と、状態の総人口を含むフィールドstatePopがあります。 このためのデータモデルを作成し、集計を実行してみましょう。

public class StatePoulation {
 
    @Id
    private String state;
    private Integer statePop;
 
    // standard getters and setters
}

@Id アノテーションは、_idフィールドを出力からモデルのstateにマップします。

GroupOperation groupByStateAndSumPop = group("state")
  .sum("pop").as("statePop");
MatchOperation filterStates = match(new Criteria("statePop").gt(10000000));
SortOperation sortByPopDesc = sort(Sort.by(Direction.DESC, "statePop"));

Aggregation aggregation = newAggregation(
  groupByStateAndSumPop, filterStates, sortByPopDesc);
AggregationResults<StatePopulation> result = mongoTemplate.aggregate(
  aggregation, "zips", StatePopulation.class);

AggregationResultsクラスはIterableを実装しているため、それを繰り返し処理して結果を出力できます。

出力データモデルが不明な場合は、標準のMongoDBクラスDocumentを使用できます。

3.2. 平均的な都市人口で最小の州を取得する

この問題には、次の4つの段階が必要です。

  1. $ group は、各都市の総人口を合計します
  2. $ group は、各州の平均人口を計算します
  3. $ sort ステージで、州の平均都市人口を昇順で並べ替えます
  4. $ limit は、平均都市人口が最も少ない最初の州を取得します

必ずしも必須ではありませんが、追加の $ project ステージを使用して、StatePopulationデータモデルに従ってドキュメントを再フォーマットします。

GroupOperation sumTotalCityPop = group("state", "city")
  .sum("pop").as("cityPop");
GroupOperation averageStatePop = group("_id.state")
  .avg("cityPop").as("avgCityPop");
SortOperation sortByAvgPopAsc = sort(Sort.by(Direction.ASC, "avgCityPop"));
LimitOperation limitToOnlyFirstDoc = limit(1);
ProjectionOperation projectToMatchModel = project()
  .andExpression("_id").as("state")
  .andExpression("avgCityPop").as("statePop");

Aggregation aggregation = newAggregation(
  sumTotalCityPop, averageStatePop, sortByAvgPopAsc,
  limitToOnlyFirstDoc, projectToMatchModel);

AggregationResults<StatePopulation> result = mongoTemplate
  .aggregate(aggregation, "zips", StatePopulation.class);
StatePopulation smallestState = result.getUniqueMappedResult();

この例では、最終段階で出力ドキュメントの数を1に制限しているため、結果に含まれるドキュメントは1つだけであることがすでにわかっています。 そのため、 getUniqueMappedResult()を呼び出して、必要なStatePopulationインスタンスを取得できます。

もう1つの注意点は、 @Idアノテーションを使用して_idを状態にマップするのではなく、投影段階で明示的に行ったことです。

3.3. 最大および最小の郵便番号で州を取得する

この例では、次の3つの段階が必要です。

  1. $ group は、各州の郵便番号の数をカウントします
  2. $ sort は、郵便番号の数で州を並べ替えます
  3. $ group は、 $firstおよび$last 演算子を使用して、最大および最小の郵便番号で州を検索します
GroupOperation sumZips = group("state").count().as("zipCount");
SortOperation sortByCount = sort(Direction.ASC, "zipCount");
GroupOperation groupFirstAndLast = group().first("_id").as("minZipState")
  .first("zipCount").as("minZipCount").last("_id").as("maxZipState")
  .last("zipCount").as("maxZipCount");

Aggregation aggregation = newAggregation(sumZips, sortByCount, groupFirstAndLast);

AggregationResults<Document> result = mongoTemplate
  .aggregate(aggregation, "zips", Document.class);
Document document= result.getUniqueMappedResult();

ここではモデルを使用していませんが、MongoDBドライバーですでに提供されているDocumentを使用しています。

4. 結論

この記事では、Spring Data MongoDBのプロジェクションを使用して、MongoDBでドキュメントの指定されたフィールドをフェッチする方法を学習しました。

また、SpringDataでのMongoDB集約フレームワークのサポートについても学びました。 グループ化、プロジェクト、並べ替え、制限、照合などの主要な集計フェーズについて説明し、その実用的なアプリケーションの例をいくつか見てきました。 完全なソースコードは、GitHubから入手できます。