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.