Búsqueda agregada de $ con C#
mongodb aggregation-framework (2)
No hay necesidad de analizar el JSON. Todo lo que aquí se puede hacer directamente con LINQ o las interfaces de Aggregate Fluent.
Solo uso algunas clases de demostración porque la pregunta realmente no da mucho para continuar.
Preparar
Básicamente tenemos dos colecciones aquí, siendo
entidades
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5710"), "name" : "A" }
{ "_id" : ObjectId("5b08ceb40a8a7614c70a5711"), "name" : "B" }
y otros
{
"_id" : ObjectId("5b08cef10a8a7614c70a5712"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5710"),
"name" : "Sub-A"
}
{
"_id" : ObjectId("5b08cefd0a8a7614c70a5713"),
"entity" : ObjectId("5b08ceb40a8a7614c70a5711"),
"name" : "Sub-B"
}
Y un par de clases para unirlas, como ejemplos muy básicos:
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;
}
Consultas
Interfaz fluida
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();
Solicitud enviada al servidor:
[
{ "$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 } }
]
Probablemente el más fácil de entender ya que la interfaz fluida es básicamente la misma que la estructura general de BSON.
La etapa
$lookup
tiene todos los mismos argumentos y
$arrayElemAt
se representa con
First()
.
Para el
$sort
puede simplemente proporcionar un documento BSON u otra expresión válida.
Una alternativa es la forma expresiva más nueva de
$lookup
con una declaración de sub-pipeline para MongoDB 3.6 y superior.
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();
Solicitud enviada al servidor:
[
{ "$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" aún no admite la sintaxis directamente, ni LINQ Expressions es compatible con el operador
$expr
, sin embargo, aún puede construir utilizando
BsonDocument
y
BsonArray
u otras expresiones válidas.
Aquí también "tipeamos" el resultado de
$sort
BsonDocument
para aplicar un
$sort
usando una expresión en lugar de un
BsonDocument
como se mostró anteriormente.
Aparte de otros usos, una tarea principal de una "sub-tubería" es reducir los documentos devueltos en la matriz de destino de
$lookup
.
Además,
$unwind
aquí tiene el propósito de
ser
realmente
"fusionado"
en la declaración de
$lookup
en la ejecución del servidor, por lo que esto suele ser más eficiente que simplemente capturar el primer elemento de la matriz resultante.
Grupo Queryable
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);
Solicitud enviada al servidor:
[
{ "$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 } }
]
Esto es casi idéntico, pero solo utiliza la interfaz diferente y produce una declaración BSON ligeramente diferente, y en realidad solo por el nombre simplificado en las declaraciones funcionales.
Esto hace surgir la otra posibilidad de simplemente usar un
$unwind
como se produce desde un
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);
Solicitud enviada al servidor:
[
{ "$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 } }
]
Normalmente, colocar
$unwind
directamente después de
$lookup
es en realidad un
"patrón optimizado"
para el marco de agregación.
Sin embargo, el controlador .NET desordena esto en esta combinación forzando un
$project
en el medio en lugar de usar el nombre implícito en
"as"
.
Si no es por eso, esto es realmente mejor que el
$arrayElemAt
cuando sabes que tienes un resultado relacionado "uno".
Si desea la "coalescencia" de
$unwind
, entonces es mejor que utilice la interfaz fluida o una forma diferente como se muestra más adelante.
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;
Solicitud enviada al servidor:
[
{ "$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 } }
]
Todo muy familiar y realmente solo para nombrar funcionalmente.
Al igual que con el uso de la opción
$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;
Solicitud enviada al servidor:
[
{ "$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 } }
]
Que en realidad está utilizando la forma de
"coalescencia optimizada"
.
El traductor todavía insiste en agregar un
$project
ya que necesitamos la
select
intermedia para que la declaración sea válida.
Resumen
Así que hay bastantes formas de llegar a lo que es básicamente la misma declaración de consulta con exactamente los mismos resultados.
Mientras que "podría" analizar el
BsonDocument
JSON a
BsonDocument
y
BsonDocument
al
BsonDocument
Aggregate()
, generalmente es mejor usar los constructores naturales o las interfaces LINQ, ya que se asignan fácilmente a la misma declaración.
Las opciones con
$unwind
se muestran en gran parte porque incluso con una coincidencia "singular" que la forma de "coalescencia" es en realidad mucho más óptima que usar
$arrayElemAt
para tomar el "primer" elemento de matriz.
Esto incluso se vuelve más importante con consideraciones de cosas como el límite BSON donde la matriz de destino
$lookup
podría hacer que el documento principal supere los 16 MB sin más filtros.
Hay otra publicación aquí en
Aggregate $ lookup. El tamaño total de los documentos en el canal de concordancia excede el tamaño máximo del documento,
donde realmente discuto cómo evitar que se alcance ese límite al usar tales opciones u otra sintaxis de
Lookup()
disponible para la interfaz fluida solo en este momento .
Tengo la siguiente consulta de MongoDb trabajando:
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
}
}
]
)
Esto recupera una lista de objetos unidos con un objeto coincidente de otra colección.
¿Alguien sabe cómo puedo usar esto en C # usando LINQ o usando esta cadena exacta?
Intenté usar el siguiente código, pero parece que no puedo encontrar los tipos para
QueryDocument
y
MongoCursor
. ¿Creo que están en desuso?
BsonDocument document = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ name : value }");
QueryDocument queryDoc = new QueryDocument(document);
MongoCursor toReturn = _connectionCollection.Find(queryDoc);
MongoDB.Entities aquí cómo hacerlo con MongoDB.Entities . En los casos en que dos entidades se encuentran en una relación de uno a muchos o de muchos a muchos, puede obtener acceso a relaciones inversas sin tener que hacer uniones manualmente como se muestra a continuación. [descargo de responsabilidad: soy el autor de la biblioteca]
using System;
using System.Linq;
using MongoDB.Entities;
using MongoDB.Driver.Linq;
namespace
{
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();
}
}
}