MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Agregacja $ lookup z C#

Nie ma potrzeby analizowania JSON. Wszystko tutaj można faktycznie zrobić bezpośrednio za pomocą interfejsów LINQ lub Aggregate Fluent.

Po prostu używam kilku klas demonstracyjnych, ponieważ pytanie nie daje zbyt wiele do zrobienia.

Konfiguracja

Zasadniczo mamy tutaj dwie kolekcje, będące

podmioty

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

i inne

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

I kilka klas, z którymi można je powiązać, tak samo jak bardzo proste przykłady:

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;
}

Zapytania

Płynny interfejs

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();

Żądanie wysłane do serwera:

[
  { "$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 } }
]

Prawdopodobnie najłatwiejszy do zrozumienia, ponieważ płynny interfejs jest w zasadzie taki sam, jak ogólna struktura BSON. $lookup stage ma te same argumenty i $arrayElemAt jest reprezentowany przez First() . Dla $sort możesz po prostu dostarczyć dokument BSON lub inne prawidłowe wyrażenie.

Alternatywą jest nowsza ekspresyjna forma $lookup z instrukcją podpotoku dla MongoDB 3.6 i nowszych.

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();

Żądanie wysłane do serwera:

[ 
  { "$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 } }
]

"Konstruktor" Fluent nie obsługuje jeszcze składni bezpośrednio, ani wyrażenia LINQ nie obsługują $expr operatora, jednak nadal możesz konstruować za pomocą BsonDocument i BsonArray lub inne poprawne wyrażenia. Tutaj również „wpisujemy” $unwind wynik w celu zastosowania $sort używając wyrażenia zamiast BsonDocument jak pokazano wcześniej.

Oprócz innych zastosowań, głównym zadaniem „podpotoku” jest zmniejszenie liczby dokumentów zwracanych w docelowej tablicy $lookup . Również $unwind tutaj służy do faktycznego "scalania" z $lookup oświadczenie o wykonywaniu serwera, więc jest to zazwyczaj bardziej wydajne niż pobranie pierwszego elementu wynikowej tablicy.

Dołączanie do grupy z możliwością zapytań

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);

Żądanie wysłane do serwera:

[ 
  { "$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 } }
]

Jest to prawie identyczne, ale po prostu używa innego interfejsu i tworzy nieco inną instrukcję BSON, i tak naprawdę tylko z powodu uproszczonego nazewnictwa w instrukcjach funkcjonalnych. To wywołuje inną możliwość prostego użycia $unwind wytworzony z SelectMany() :

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);

Żądanie wysłane do serwera:

[
  { "$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 } }
]

Normalnie umieszczanie $unwind bezpośrednio po $lookup jest w rzeczywistości „zoptymalizowanym wzorcem” dla struktury agregacji. Jednak sterownik .NET psuje to w tej kombinacji, wymuszając $project pomiędzy, zamiast używać implikowanego nazewnictwa w "as" . Jeśli nie, to jest to faktycznie lepsze niż $arrayElemAt gdy wiesz, że masz „jeden” powiązany wynik. Jeśli chcesz $unwind "koalescencja", wtedy lepiej jest używać płynnego interfejsu lub innej formy, jak zademonstrowano później.

Dość naturalne

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;

Żądanie wysłane do serwera:

[
  { "$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 } }
]

Wszystko całkiem znajome i naprawdę sprowadza się do funkcjonalnego nazewnictwa. Podobnie jak przy użyciu $unwind opcja:

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;

Żądanie wysłane do serwera:

[ 
  { "$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 } }
]

Który faktycznie używa formy „zoptymalizowanej koalescencji”. Tłumacz nadal nalega na dodanie $project ponieważ potrzebujemy pośredniego select aby oświadczenie było ważne.

Podsumowanie

Tak więc istnieje wiele sposobów, aby w zasadzie dojść do tego, co jest zasadniczo tym samym zapytaniem z dokładnie tymi samymi wynikami. Podczas gdy „możesz” przeanalizować plik JSON do BsonDocument formularz i podaj to płynnie Aggregate() polecenia, ogólnie lepiej jest użyć naturalnych konstruktorów lub interfejsów LINQ, ponieważ łatwo mapują one to samo stwierdzenie.

Opcje z $unwind są w dużej mierze pokazane, ponieważ nawet przy "pojedynczym" dopasowaniu ta forma "koalescencyjna" jest w rzeczywistości znacznie bardziej optymalna niż przy użyciu $arrayElemAt wziąć „pierwszy” element tablicy. Staje się to jeszcze ważniejsze, biorąc pod uwagę takie rzeczy jak limit BSON, gdzie $lookup tablica docelowa może spowodować, że dokument nadrzędny przekroczy 16 MB bez dalszego filtrowania. Jest jeszcze jeden post na temat Aggregate $lookup Całkowity rozmiar dokumentów w pasującym potoku przekracza maksymalny rozmiar dokumentu, w którym faktycznie omawiam, jak uniknąć przekroczenia tego limitu za pomocą takich opcji lub innych funkcji Lookup() składnia dostępna tylko w płynnym interfejsie w tej chwili.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB:znajdź minimalny element w tablicy i usuń go

  2. Dlaczego mongoDB używa identyfikatora obiektu?

  3. mongodb TTL nie usuwa dokumentów

  4. Połącz laravel jenssegers z klastrem atlasu mongodb

  5. Jak zaktualizować/ulepszyć dokument w Mongoose?