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

Consultando após preencher no Mongoose


Com um MongoDB moderno maior que 3.2, você pode usar $lookup como uma alternativa para .populate() na maioria dos casos. Isso também tem a vantagem de realmente fazer o join "no servidor" ao contrário do que .populate() faz que na verdade é "várias consultas" para "emular" uma junção.

Então .populate() é não realmente uma "junção" no sentido de como um banco de dados relacional faz isso. A $lookup por outro lado, realmente faz o trabalho no servidor e é mais ou menos análogo a um "LEFT JOIN" :
Item.aggregate(
  [
    { "$lookup": {
      "from": ItemTags.collection.name,
      "localField": "tags",
      "foreignField": "_id",
      "as": "tags"
    }},
    { "$unwind": "$tags" },
    { "$match": { "tags.tagName": { "$in": [ "funny", "politics" ] } } },
    { "$group": {
      "_id": "$_id",
      "dateCreated": { "$first": "$dateCreated" },
      "title": { "$first": "$title" },
      "description": { "$first": "$description" },
      "tags": { "$push": "$tags" }
    }}
  ],
  function(err, result) {
    // "tags" is now filtered by condition and "joined"
  }
)

N.B. O .collection.name aqui, na verdade, avalia a "string" que é o nome real da coleção do MongoDB conforme atribuído ao modelo. Como o mangusto "pluraliza" os nomes das coleções por padrão e $lookup precisa do nome real da coleção do MongoDB como um argumento (já que é uma operação do servidor), então esse é um truque útil para usar no código do mongoose, em vez de "codificar diretamente" o nome da coleção.

Embora também possamos usar $filter em arrays para remover os itens indesejados, esta é realmente a forma mais eficiente devido à Otimização de Pipeline de Agregação para a condição especial de como $lookup seguido por um $unwind e um $match doença.

Isso realmente resulta nos três estágios do pipeline sendo rolados em um:
   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

Isso é altamente ideal, pois a operação real "filtra a coleção para ingressar primeiro", depois retorna os resultados e "desenrola" a matriz. Ambos os métodos são empregados para que os resultados não ultrapassem o limite BSON de 16 MB, que é uma restrição que o cliente não possui.

O único problema é que parece "contra-intuitivo" em alguns aspectos, principalmente quando você deseja os resultados em uma matriz, mas é isso que o $group é para aqui, pois reconstrói a forma do documento original.

Também é lamentável que simplesmente não possamos neste momento realmente escrever $lookup na mesma sintaxe eventual que o servidor usa. IMHO, este é um descuido a ser corrigido. Mas, por enquanto, simplesmente usar a sequência funcionará e é a opção mais viável com o melhor desempenho e escalabilidade.

Adendo - MongoDB 3.6 e superior


Embora o padrão mostrado aqui seja bastante otimizado devido a como os outros estágios são inseridos no $lookup , ele tem uma falha em que o "LEFT JOIN" que normalmente é inerente a ambos $lookup e as ações de populate() é negado pelo "ótimo" uso de $unwind aqui que não preserva arrays vazios. Você pode adicionar os preserveNullAndEmptyArrays opção, mas isso nega a opção "otimizada" seqüência descrita acima e essencialmente deixa todos os três estágios intactos que normalmente seriam combinados na otimização.

MongoDB 3.6 expande com um "mais expressivo" forma de $lookup permitindo uma expressão "sub-pipeline". O que não só cumpre o objetivo de reter o "LEFT JOIN", mas ainda permite uma consulta otimizada para reduzir os resultados retornados e com uma sintaxe bem simplificada:
Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

O $expr usado para combinar o valor "local" declarado com o valor "estrangeiro" é na verdade o que o MongoDB faz "internamente" agora com o $lookup original sintaxe. Ao expressar neste formulário, podemos adaptar o $match inicial expressão dentro do "sub-pipeline" nós mesmos.

Na verdade, como um verdadeiro "pipeline de agregação", você pode fazer praticamente qualquer coisa com um pipeline de agregação dentro dessa expressão "sub-pipeline", incluindo "aninhar" os níveis de $lookup para outras coleções relacionadas.

O uso adicional está um pouco além do escopo do que a pergunta aqui faz, mas em relação à "população aninhada", o novo padrão de uso de $lookup permite que isso seja praticamente o mesmo, e um "lote" mais poderoso em seu uso total.

Exemplo de trabalho


Veja a seguir um exemplo usando um método estático no modelo. Uma vez que esse método estático é implementado, a chamada simplesmente se torna:
  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

Ou melhorar para ser um pouco mais moderno ainda se torna:
  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

Tornando-o muito semelhante a .populate() na estrutura, mas na verdade está fazendo a junção no servidor. Para completar, o uso aqui lança os dados retornados de volta para as instâncias do documento mangusto de acordo com os casos pai e filho.

É bastante trivial e fácil de adaptar ou apenas usar como é para os casos mais comuns.

N.B O uso de assíncrono aqui é apenas para brevidade de execução do exemplo incluído. A implementação real está livre dessa dependência.
const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost/looktest');

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt,callback) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  this.aggregate(pipeline,(err,result) => {
    if (err) callback(err);
    result = result.map(m => {
      m[opt.path] = m[opt.path].map(r => rel(r));
      return this(m);
    });
    callback(err,result);
  });
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

function log(body) {
  console.log(JSON.stringify(body, undefined, 2))
}
async.series(
  [
    // Clean data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create tags and items
    (callback) =>
      async.waterfall(
        [
          (callback) =>
            ItemTag.create([{ "tagName": "movies" }, { "tagName": "funny" }],
              callback),

          (tags, callback) =>
            Item.create({ "title": "Something","description": "An item",
              "tags": tags },callback)
        ],
        callback
      ),

    // Query with our static
    (callback) =>
      Item.lookup(
        {
          path: 'tags',
          query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
        },
        callback
      )
  ],
  (err,results) => {
    if (err) throw err;
    let result = results.pop();
    log(result);
    mongoose.disconnect();
  }
)

Ou um pouco mais moderno para o Node 8.xe acima com async/await e sem dependências adicionais:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  dateCreated: { type: Date, default: Date.now },
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
});

itemSchema.statics.lookup = function(opt) {
  let rel =
    mongoose.model(this.schema.path(opt.path).caster.options.ref);

  let group = { "$group": { } };
  this.schema.eachPath(p =>
    group.$group[p] = (p === "_id") ? "$_id" :
      (p === opt.path) ? { "$push": `$${p}` } : { "$first": `$${p}` });

  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": opt.path,
      "localField": opt.path,
      "foreignField": "_id"
    }},
    { "$unwind": `$${opt.path}` },
    { "$match": opt.query },
    group
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m => 
    this({ ...m, [opt.path]: m[opt.path].map(r => rel(r)) })
  ));
}

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {
  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.create(
      ["movies", "funny"].map(tagName =>({ tagName }))
    );
    const item = await Item.create({ 
      "title": "Something",
      "description": "An item",
      tags 
    });

    // Query with our static
    const result = (await Item.lookup({
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);

    mongoose.disconnect();

  } catch (e) {
    console.error(e);
  } finally {
    process.exit()
  }
})()

E do MongoDB 3.6 e superior, mesmo sem o $unwind e $group prédio:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/looktest';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const itemTagSchema = new Schema({
  tagName: String
});

const itemSchema = new Schema({
  title: String,
  description: String,
  tags: [{ type: Schema.Types.ObjectId, ref: 'ItemTag' }]
},{ timestamps: true });

itemSchema.statics.lookup = function({ path, query }) {
  let rel =
    mongoose.model(this.schema.path(path).caster.options.ref);

  // MongoDB 3.6 and up $lookup with sub-pipeline
  let pipeline = [
    { "$lookup": {
      "from": rel.collection.name,
      "as": path,
      "let": { [path]: `$${path}` },
      "pipeline": [
        { "$match": {
          ...query,
          "$expr": { "$in": [ "$_id", `$$${path}` ] }
        }}
      ]
    }}
  ];

  return this.aggregate(pipeline).exec().then(r => r.map(m =>
    this({ ...m, [path]: m[path].map(r => rel(r)) })
  ));
};

const Item = mongoose.model('Item', itemSchema);
const ItemTag = mongoose.model('ItemTag', itemTagSchema);

const log = body => console.log(JSON.stringify(body, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // Clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // Create tags and items
    const tags = await ItemTag.insertMany(
      ["movies", "funny"].map(tagName => ({ tagName }))
    );

    const item = await Item.create({
      "title": "Something",
      "description": "An item",
      tags
    });

    // Query with our static
    let result = (await Item.lookup({
      path: 'tags',
      query: { 'tagName': { '$in': [ 'funny', 'politics' ] } }
    })).pop();
    log(result);


    await mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()