web-dev-qa-db-ja.com

C#で$ lookupを集計する

私は次のMongoDbクエリが機能しています:

db.Entity.aggregate(
    [
        {
            "$match":{"Id": "12345"}
        },
        {
            "$lookup": {
                "from": "OtherCollection",
                "localField": "otherCollectionId",
                "foreignField": "Id",
                "as": "ent"
            }
        },
        { 
            "$project": { 
                "Name": 1,
                "Date": 1,
                "OtherObject": { "$arrayElemAt": [ "$ent", 0 ] } 
            }
        },
        { 
            "$sort": { 
                "OtherObject.Profile.Name": 1
            } 
        }
    ]
)

これにより、別のコレクションから一致するオブジェクトと結合されたオブジェクトのリストが取得されます。

LINQまたはこの正確な文字列を使用してC#でこれを使用する方法を知っている人はいますか?

次のコードを使用してみましたが、QueryDocumentMongoCursorの型が見つからないようです-廃止されたと思いますか?

BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);
12
TomSelleck

JSONを解析する必要はありません。ここでのすべては、LINQまたはAggregate Fluentインターフェイスのいずれかで実際に直接実行できます。

質問は実際にはあまり多くのことを与えないため、いくつかのデモクラスを使用するだけです。

セットアップ

基本的に、ここには2つのコレクションがあります。

entities

{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }

およびothers

{
        "_id" : ObjectId("5b08cef10a8a7614c70a5712"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
        "name" : "Sub-A"
}
{
        "_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
        "entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
        "name" : "Sub-B"
}

そして、非常に基本的な例として、それらをバインドするいくつかのクラス:

public class Entity
{
  public ObjectId id;
  public string name { get; set; }
}

public class Other
{
  public ObjectId id;
  public ObjectId entity { get; set; }
  public string name { get; set; }
}

public class EntityWithOthers
{
  public ObjectId id;
  public string name { get; set; }
  public IEnumerable<Other> others;
}

 public class EntityWithOther
{
  public ObjectId id;
  public string name { get; set; }
  public Other others;
}

問い合わせ

流Interfaceなインターフェイス

var listNames = new[] { "A", "B" };

var query = entities.Aggregate()
    .Match(p => listNames.Contains(p.name))
    .Lookup(
      foreignCollection: others,
      localField: e => e.id,
      foreignField: f => f.entity,
      @as: (EntityWithOthers eo) => eo.others
    )
    .Project(p => new { p.id, p.name, other = p.others.First() } )
    .Sort(new BsonDocument("other.name",-1))
    .ToList();

サーバーに送信されたリクエスト:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : { 
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "others"
  } }, 
  { "$project" : { 
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$others", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

流fluentなインターフェイスは基本的なBSON構造と基本的に同じなので、おそらく最も理解しやすいでしょう。 $lookup ステージにはすべて同じ引数があり、 $arrayElemAtFirst()で表されます。 $sort の場合、BSONドキュメントまたは他の有効な式を指定するだけです。

別の方法は、MongoDB 3.6以降のサブパイプラインステートメントを使用した新しい表現形式の $lookup です。

BsonArray subpipeline = new BsonArray();

subpipeline.Add(
  new BsonDocument("$match",new BsonDocument(
    "$expr", new BsonDocument(
      "$eq", new BsonArray { "$$entity", "$entity" }  
    )
  ))
);

var lookup = new BsonDocument("$lookup",
  new BsonDocument("from", "others")
    .Add("let", new BsonDocument("entity", "$_id"))
    .Add("pipeline", subpipeline)
    .Add("as","others")
);

var query = entities.Aggregate()
  .Match(p => listNames.Contains(p.name))
  .AppendStage<EntityWithOthers>(lookup)
  .Unwind<EntityWithOthers, EntityWithOther>(p => p.others)
  .SortByDescending(p => p.others.name)
  .ToList();

サーバーに送信されたリクエスト:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "let" : { "entity" : "$_id" },
    "pipeline" : [
      { "$match" : { "$expr" : { "$eq" : [ "$$entity", "$entity" ] } } }
    ],
    "as" : "others"
  } },
  { "$unwind" : "$others" },
  { "$sort" : { "others.name" : -1 } }
]

Fluent "Builder"はまだ構文を直接サポートしていません。また、LINQ Expressionsは $expr 演算子をサポートしていませんが、BsonDocumentおよびBsonArrayを使用して構築できますまたは他の有効な式。ここでは、示されているようにBsonDocumentではなく式を使用して $unwind を適用するために $sort 結果を「入力」しますついさっき。

他の用途とは別に、「サブパイプライン」の主なタスクは、ターゲット配列 $lookup で返されるドキュメントを減らすことです。また、 $unwind は、実際には 「マージ」 をサーバー実行時の $lookup ステートメントに入れる目的を果たします。そのため、これは通常、結果の配列の最初の要素を取得するよりも効率的です。

クエリ可能なGroupJoin

var query = entities.AsQueryable()
    .Where(p => listNames.Contains(p.name))
    .GroupJoin(
      others.AsQueryable(),
      p => p.id,
      o => o.entity,
      (p, o) => new { p.id, p.name, other = o.First() }
    )
    .OrderByDescending(p => p.other.name);

サーバーに送信されたリクエスト:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "o"
  } },
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$o", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

これはほとんど同じですが、異なるインターフェイスを使用するだけで、わずかに異なるBSONステートメントを生成します。これは、機能ステートメントの命名が単純化されているためです。これは、単にSelectMany()から生成される $unwind を使用する他の可能性をもたらします。

var query = entities.AsQueryable()
  .Where(p => listNames.Contains(p.name))
  .GroupJoin(
    others.AsQueryable(),
    p => p.id,
    o => o.entity,
    (p, o) => new { p.id, p.name, other = o }
  )
  .SelectMany(p => p.other, (p, other) => new { p.id, p.name, other })
  .OrderByDescending(p => p.other.name);

サーバーに送信されたリクエスト:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "o"
  }},
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : "$o",
    "_id" : 0
  } },
  { "$unwind" : "$other" },
  { "$project" : {
    "id" : "$id",
    "name" : "$name",
    "other" : "$other",
    "_id" : 0
  }},
  { "$sort" : { "other.name" : -1 } }
]

通常、 $unwind$lookup の直後に配置することは、実際には集約フレームワークの "optimized pattern" です。ただし、.NETドライバーは、$projectで暗黙の命名を使用するのではなく、その間に"as"を強制することにより、この組み合わせでこれを台無しにします。そうでない場合、これは実際には $arrayElemAt よりも優れています。これは、「1」関連の結果があることがわかっている場合です。 $unwind "coalescence"が必要な場合は、Fluentインターフェイスを使用するか、後で説明する別のフォームを使用することをお勧めします。

キュラブルナチュラル

var query = from p in entities.AsQueryable()
            where listNames.Contains(p.name) 
            join o in others.AsQueryable() on p.id equals o.entity into joined
            select new { p.id, p.name, other = joined.First() }
            into p
            orderby p.other.name descending
            select p;

サーバーに送信されたリクエスト:

[
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "joined"
  } },
  { "$project" : {
    "id" : "$_id",
    "name" : "$name",
    "other" : { "$arrayElemAt" : [ "$joined", 0 ] },
    "_id" : 0
  } },
  { "$sort" : { "other.name" : -1 } }
]

すべてはかなり馴染みがあり、実際には機能的な命名法にまで及んでいます。 $unwind オプションを使用する場合と同様に:

var query = from p in entities.AsQueryable()
            where listNames.Contains(p.name) 
            join o in others.AsQueryable() on p.id equals o.entity into joined
            from sub_o in joined.DefaultIfEmpty()
            select new { p.id, p.name, other = sub_o }
            into p
            orderby p.other.name descending
            select p;

サーバーに送信されたリクエスト:

[ 
  { "$match" : { "name" : { "$in" : [ "A", "B" ] } } },
  { "$lookup" : {
    "from" : "others",
    "localField" : "_id",
    "foreignField" : "entity",
    "as" : "joined"
  } },
  { "$unwind" : { 
    "path" : "$joined", "preserveNullAndEmptyArrays" : true
  } }, 
  { "$project" : { 
    "id" : "$_id",
    "name" : "$name",
    "other" : "$joined",
    "_id" : 0
  } }, 
  { "$sort" : { "other.name" : -1 } }
]

実際には "最適化された合体" 形式を使用しています。ステートメントを有効にするには中間のselectが必要なので、翻訳者は $project を追加することを引き続き主張します。

概要

そのため、基本的に同じクエリステートメントで、まったく同じ結果が得られるものに到達するには、いくつかの方法があります。 JSONをBsonDocument形式に「解析」し、これを流__なAggregate()コマンドにフィードすることはできますが、通常は同じステートメントに簡単にマッピングされるため、ナチュラルビルダーまたはLINQインターフェイスを使用することをお勧めします。

$unwind のオプションは、 "特異"マッチであっても $arrayElemAt を使用するよりも実際に「合体」フォームがはるかに最適であるため、主に表示されます。 「最初の」配列要素を取得します。これは、 $lookup ターゲット配列が親ドキュメントをさらにフィルタリングせずに16MBを超える可能性があるBSON Limitなどのことを考慮するとさらに重要になります。ここに別の投稿があります Aggregate $ lookup一致するパイプラインのドキュメントの合計サイズが最大ドキュメントサイズを超えています ここで、流suchに利用可能なそのようなオプションまたは他のLookup()構文を使用して、その制限に達するのを回避する方法を実際に議論します現時点ではインターフェースのみ。

29
Neil Lunn

MongoDB.Entities でこれを行う方法を次に示します。 2つのエンティティが1対多または多対多の関係にある場合、以下に示すように、手動で結合する必要なく、逆の関係にアクセスできます。 [免責事項:私は図書館の著者です]

using System;
using System.Linq;
using MongoDB.Entities;
using MongoDB.Driver.Linq;

namespace StackOverflow
{
    public class Program
    {
        public class Author : Entity
        {
            public string Name { get; set; }
            public Many<Book> Books { get; set; }

            public Author() => this.InitOneToMany(() => Books);
        }

        public class Book : Entity
        {
            public string Title { get; set; }
        }

        static void Main(string[] args)
        {
            new DB("test");

            var book = new Book { Title = "The Power Of Now" };
            book.Save();

            var author = new Author { Name = "Eckhart Tolle" };
            author.Save();

            author.Books.Add(book);

            //build a query for finding all books that has Power in the title.
            var bookQuery = DB.Queryable<Book>()
                              .Where(b => b.Title.Contains("Power"));

            //find all the authors of books that has a title with Power in them
            var authors = author.Books
                                .ParentsQueryable<Author>(bookQuery); //also can pass in an ID or array of IDs

            //get the result
            var result = authors.ToArray();

            //output the aggregation pipeline
            Console.WriteLine(authors.ToString());


            Console.ReadKey();
        }
    }
}
0
Ryan Gunner