Нет необходимости анализировать JSON.Здесь все можно сделать напрямую с помощью интерфейсов LINQ или Aggregate Fluent.
Просто с помощью некоторых демонстрационных классов, потому что вопрос на самом деле мало что дает для продолжения.
Настройка
В основном у нас есть две коллекции:
сущностей
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
и другие
{
"_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;
}
Запросы
Свободный интерфейс
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 } }
]
Вероятно, самый простой для понимания, поскольку свободный интерфейс в основном такой же, как и общая структура BSON.Стадия $lookup
имеет все те же аргументы, а $arrayElemAt
обозначается First()
.Для $sort
вы можете просто предоставить документ BSON или другое допустимое выражение.
Альтернативой является более новая выразительная форма $lookup
с подпунктомоператор конвейера для MongoDB 3.6 и выше.
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 не поддерживаютОператор $expr
, однако вы все еще можете создавать с использованием BsonDocument
и BsonArray
или других допустимых выражений.Здесь мы также "набираем" результат $unwind
, чтобы применить $sort
, используя выражение, а не BsonDocument
, как показано ранее.
Помимо других применений, основная задача «субпроводника» - уменьшить количество документов, возвращаемых в целевом массиве, до $lookup
.Также $unwind
здесь служит для "* объединения" *1072* в оператор $lookup
при выполнении на сервере, так что это обычно более эффективно, чемпросто захватывает первый элемент полученного массива.
Queryable 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, и на самом деле только из-за упрощенного именования в функциональных операторах.Это вызывает другую возможность простого использования $unwind
, полученного из 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);
Запрос, отправленный на сервер:
[
{ "$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
на самом деле "оптимизированный шаблон" для структуры агрегации.Тем не менее, драйвер .NET все портит в этой комбинации, заставляя $project
между, а не используя подразумеваемое именование "as"
.Если бы не это, это на самом деле лучше, чем $arrayElemAt
, когда вы знаете, что у вас есть «один» связанный результат.Если вы хотите $unwind
«коалесценция», то вам лучше использовать плавный интерфейс или другую форму, как показано ниже.
Querable Natural
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 } }
]
, который на самом деле использует оптимизированную коалесценцию "" форма.Переводчик по-прежнему настаивает на добавлении $project
, поскольку нам нужен промежуточный select
, чтобы сделать утверждение действительным.
Резюме
Так чтонесколько способов по существу получить то же, что и в основном, тот же оператор запроса с точно такими же результатами.Хотя вы «можете» проанализировать JSON-форму BsonDocument
и передать ее в свободную команду Aggregate()
, обычно лучше использовать естественные компоновщики или интерфейсы LINQ, поскольку они легко отображаются на один и тот же оператор.
Варианты с $unwind
в значительной степени показаны, потому что даже при «единственном» совпадении эта форма «коалесценции» на самом деле гораздо более оптимальна, чем использование $arrayElemAt
для получения«первый» элемент массива.Это даже становится более важным с учетом таких вещей, как предел BSON, где целевой массив $lookup
может привести к тому, что родительский документ превысит 16 МБ без дальнейшей фильтрации.Здесь есть еще одно сообщение о Aggregate $ lookup. Общий размер документов в соответствующем конвейере превышает максимальный размер документа , где я на самом деле обсуждаю, как избежать этого ограничения, используя такие параметры или другой синтаксис Lookup()
, доступный длясвободный интерфейс только в это время.