sql >> データベース >  >> NoSQL >> MongoDB

アレイから最新のサブドキュメントを取得する

    これには、いくつかの異なる方法で取り組むことができます。もちろん、アプローチやパフォーマンスによって異なりますが、設計にはもっと大きな考慮事項があると思います。最も注目すべき点は、実際のアプリケーションの使用パターンにおける「改訂」データの「必要性」です。

    集計によるクエリ

    「内部配列から最後の要素」を取得するための最も重要なポイントについては、実際には .aggregate() これを行うための操作:

    function getProject(req,projectId) {
    
      return new Promise((resolve,reject) => {
        Project.aggregate([
          { "$match": { "project_id": projectId } },
          { "$addFields": {
            "uploaded_files": {
              "$map": {
                "input": "$uploaded_files",
                "as": "f",
                "in": {
                  "latest": {
                    "$arrayElemAt": [
                      "$$f.history",
                      -1
                    ]
                  },
                  "_id": "$$f._id",
                  "display_name": "$$f.display_name"
                }
              }
            }
          }},
          { "$lookup": {
            "from": "owner_collection",
            "localField": "owner",
            "foreignField": "_id",
            "as": "owner"
          }},
          { "$unwind": "$uploaded_files" },
          { "$lookup": {
             "from": "files_collection",
             "localField": "uploaded_files.latest.file",
             "foreignField": "_id",
             "as": "uploaded_files.latest.file"
          }},
          { "$group": {
            "_id": "$_id",
            "project_id": { "$first": "$project_id" },
            "updated_at": { "$first": "$updated_at" },
            "created_at": { "$first": "$created_at" },
            "owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } },
            "name":  { "$first": "$name" },
            "uploaded_files": {
              "$push": {
                "latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] },
                "_id": "$$uploaded_files._id",
                "display_name": "$$uploaded_files.display_name"
              }
            }
          }}
        ])
        .then(result => {
          if (result.length === 0)
            reject(new createError.NotFound(req.path));
          resolve(result[0])
        })
        .catch(reject)
      })
    }
    

    これは、追加のリクエスト( .populate())を行うのではなく、「サーバー」で「結合」を実行できる集約ステートメントであるためです。 実際にここで行います) $ lookup 、スキーマが質問に含まれていないため、実際のコレクション名を自由に使用しています。実際にこの方法で実行できることに気付いていなかったので、それで問題ありません。

    もちろん、「実際の」コレクション名はサーバーに必要です。サーバーには、「アプリケーション側」で定義されたスキーマの概念がありません。ここでは便利な方法がありますが、それについては後で詳しく説明します。

    projectIdの場所によっては注意が必要です。 .find()などの通常のマングースメソッドとは異なり、実際には $ match 実際にはObjectIdに「キャスト」する必要があります 入力値が実際に「文字列」である場合。 Mongooseは集約パイプラインに「スキーマタイプ」を適用できないため、特に projectId の場合は、これを自分で行う必要がある場合があります。 リクエストパラメータから来ました:

      { "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
    

    ここでの基本的な部分は、 $ map> すべての"uploaded_files"を反復処理します エントリを入力し、 "history"から「最新」を抽出します。 $ arrayElemAtの配列 -1である「最後の」インデックスを使用する 。

    「最新のリビジョン」が実際には「最後の」配列エントリである可能性が最も高いため、これは合理的です。 $ max $ filterの条件として 。そのため、パイプラインステージは次のようになります。

         { "$addFields": {
            "uploaded_files": {
              "$map": {
                "input": "$uploaded_files",
                "as": "f",
                "in": {
                  "latest": {
                    "$arrayElemAt": [
                       { "$filter": {
                         "input": "$$f.history.revision",
                         "as": "h",
                         "cond": {
                           "$eq": [
                             "$$h",
                             { "$max": "$$f.history.revision" }
                           ]
                         }
                       }},
                       0
                     ]
                  },
                  "_id": "$$f._id",
                  "display_name": "$$f.display_name"
                }
              }
            }
          }},
    

    と比較することを除いて、これはほぼ同じです。 $ max 値を返し、 "one"のみを返します 「フィルタリングされた」配列から「最初の」位置、つまり 0を返すインデックスを作成する配列からのエントリ インデックス。

    $ lookupの使用に関するその他の一般的なテクニックについて .populate()の代わりに 、「マングースにデータを入力した後のクエリ」 のエントリを参照してください これは、このアプローチを取るときに最適化できることについてもう少し話します。

    入力によるクエリ

    もちろん、 .populate()を使用して、同じ種類の操作を実行できます(効率的ではありませんが)。 呼び出しと結果の配列の操作:

    Project.findOne({ "project_id": projectId })
      .populate(populateQuery)
      .lean()
      .then(project => {
        if (project === null) 
          reject(new createError.NotFound(req.path));
    
          project.uploaded_files = project.uploaded_files.map( f => ({
            latest: f.history.slice(-1)[0],
            _id: f._id,
            display_name: f.display_name
          }));
    
         resolve(project);
      })
      .catch(reject)
    

    もちろん、実際には "history"からアイテムの「すべて」を返します。 、ただし、.mapを適用するだけです。 () .slice()<を呼び出す/ code> これらの要素に対して、それぞれの最後の配列要素を再度取得します。

    すべての履歴が返され、 .populate()が返されるため、オーバーヘッドが少し増えます。 呼び出しは追加のリクエストですが、同じ最終結果が得られます。

    設計のポイント

    ここで私が目にする主な問題は、コンテンツ内に「履歴」配列さえあることです。必要な関連アイテムのみを返品するには、上記のようなことを行う必要があるため、これはあまり良いアイデアではありません。

    ですから、「デザインのポイント」として、私はこれをしません。しかし、代わりに、すべての場合にアイテムから履歴を「分離」します。 「埋め込まれた」ドキュメントを維持しながら、「履歴」を別の配列に保持し、「最新の」リビジョンのみを実際のコンテンツとともに保持します。

    {
        "_id" : ObjectId("5935a41f12f3fac949a5f925"),
        "project_id" : 13,
        "updated_at" : ISODate("2017-07-02T22:11:43.426Z"),
        "created_at" : ISODate("2017-06-05T18:34:07.150Z"),
        "owner" : ObjectId("591eea4439e1ce33b47e73c3"),
        "name" : "Demo project",
        "uploaded_files" : [ 
            {
                "latest" : { 
                    {
                        "file" : ObjectId("59596f9fb6c89a031019bcae"),
                        "revision" : 1
                    }
                },
                "_id" : ObjectId("59596f9fb6c89a031019bcaf"),
                "display_name" : "Example filename.txt"
            }
        ]
        "file_history": [
          { 
            "_id": ObjectId("59596f9fb6c89a031019bcaf"),
            "file": ObjectId("59596f9fb6c89a031019bcae"),
            "revision": 0
        },
        { 
            "_id": ObjectId("59596f9fb6c89a031019bcaf"),
            "file": ObjectId("59596f9fb6c89a031019bcae"),
            "revision": 1
        }
    
    }
    

    これは、 $ setを設定するだけで維持できます。 関連するエントリと $ pushの使用 1回の操作での「履歴」について:

    .update(
      { "project_id": projectId, "uploaded_files._id": fileId }
      { 
        "$set": {
          "uploaded_files.$.latest": { 
            "file": revisionId,
            "revision": revisionNum
          }
        },
        "$push": {
          "file_history": {
            "_id": fileId,
            "file": revisionId,
            "revision": revisionNum
          }
        }
      }
    )
    

    配列を分離すると、クエリを実行して常に最新のものを取得し、実際にそのリクエストを行いたいときまで「履歴」を破棄できます。

    Project.findOne({ "project_id": projectId })
      .select('-file_history')      // The '-' here removes the field from results
      .populate(populateQuery)
    

    一般的なケースとして、私は単に「改訂」番号をまったく気にしないでしょう。 「最新」は常に「最後」であるため、配列に「追加」するときに、同じ構造の多くを維持することは実際には必要ありません。これは、構造を変更する場合にも当てはまります。ここでも、「最新」が常に、指定されたアップロードファイルの最後のエントリになります。

    このような「人工」インデックスを維持しようとすると問題が発生し、 .update()に示すように「アトミック」操作の変更がほとんど台無しになります。 ここでの例は、最新のリビジョン番号を提供するために「カウンター」値を知る必要があるため、どこかからそれを「読み取る」必要があるためです。




    1. jedis forJavaを使用してDockerRedisクラスターインスタンスに接続するにはどうすればよいですか?

    2. 配列フィールドがクエリ配列のサブセットであるMongoDBでドキュメントを検索する

    3. C#MongoDB複合クラスのシリアル化

    4. マングースの__vフィールドとは何ですか