MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Agregar $ lookup com C #


Não há necessidade de analisar o JSON. Tudo aqui pode ser feito diretamente com as interfaces LINQ ou Aggregate Fluent.

Apenas usando algumas aulas de demonstração porque a questão realmente não dá muito para continuar.

Configuração


Basicamente temos aqui duas coleções, sendo

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

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

E algumas classes para vinculá-los, apenas como exemplos muito 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

Interface Fluente

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

Solicitação enviada ao 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 } }
]

Provavelmente o mais fácil de entender, pois a interface fluente é basicamente a mesma que a estrutura geral do BSON. A $lookup stage tem todos os mesmos argumentos e o $arrayElemAt é representado com First() . Para o $sort você pode simplesmente fornecer um documento BSON ou outra expressão válida.

Uma alternativa é a forma expressiva mais recente de $lookup com uma instrução de sub-pipeline para MongoDB 3.6 e 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();

Solicitação enviada ao 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 } }
]

O Fluent "Builder" ainda não suporta a sintaxe diretamente, nem as expressões LINQ suportam o $expr operador, no entanto, você ainda pode construir usando BsonDocument e BsonArray ou outras expressões válidas. Aqui também "digitamos" o $unwind resultado para aplicar um $sort usando uma expressão em vez de um BsonDocument como mostrado anteriormente.

Além de outros usos, uma tarefa principal de um "sub-pipeline" é reduzir os documentos retornados no array de destino de $lookup . Também o $unwind aqui serve ao propósito de realmente ser "mesclado" no $lookup instrução na execução do servidor, portanto, isso geralmente é mais eficiente do que apenas pegar o primeiro elemento da matriz resultante.

Participação de grupo consultável

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

Solicitação enviada ao 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 } }
]

Isso é quase idêntico, mas apenas usando a interface diferente e produz uma instrução BSON ligeiramente diferente, e realmente apenas por causa da nomenclatura simplificada nas instruções funcionais. Isso traz a outra possibilidade de simplesmente usar um $unwind como produzido a partir de um 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);

Solicitação enviada ao 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 colocando um $unwind diretamente seguindo $lookup é na verdade um "padrão otimizado" para a estrutura de agregação. No entanto, o driver .NET atrapalha isso nessa combinação, forçando um $project no meio, em vez de usar a nomenclatura implícita no "as" . Se não fosse por isso, isso é realmente melhor que o $arrayElemAt quando você sabe que tem "um" resultado relacionado. Se você quiser o $unwind "coalescência", então é melhor usar a interface fluente ou uma forma diferente, conforme demonstrado posteriormente.

Querível 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;

Solicitação enviada ao 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 } }
]

Tudo bastante familiar e realmente apenas para nomenclatura funcional. Assim como com o uso do $unwind opção:
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;

Solicitação enviada ao 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 } }
]

O que na verdade está usando a forma de "coalescência otimizada". O tradutor ainda insiste em adicionar um $project já que precisamos do intermediário select para validar a afirmação.

Resumo


Portanto, existem algumas maneiras de chegar essencialmente ao que é basicamente a mesma instrução de consulta com exatamente os mesmos resultados. Enquanto você "poderia" analisar o JSON para BsonDocument form e alimente isso para o fluente Aggregate() comando, geralmente é melhor usar os construtores naturais ou as interfaces LINQ, pois eles mapeiam facilmente na mesma instrução.

As opções com $unwind são amplamente mostrados porque, mesmo com uma correspondência "singular", a forma de "coalescência" é realmente muito mais ideal do que usar $arrayElemAt para pegar o "primeiro" elemento do array. Isso se torna ainda mais importante com considerações como o Limite BSON, onde o $lookup matriz de destino pode fazer com que o documento pai exceda 16 MB sem filtragem adicional. Há outro post aqui em Aggregate $lookup O tamanho total dos documentos no pipeline de correspondência excede o tamanho máximo do documento, onde eu realmente discuto como evitar que esse limite seja atingido usando essas opções ou outros Lookup() sintaxe disponível para a interface fluente apenas neste momento.