MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Interrogation après remplissage dans Mongoose

Avec un MongoDB moderne supérieur à 3.2, vous pouvez utiliser $lookup comme alternative à .populate() dans la plupart des cas. Cela a également l'avantage de faire la jointure "sur le serveur" par opposition à ce que .populate() fait ce qui est en fait "plusieurs requêtes" à "émuler" une jointure.

Donc .populate() n'est pas vraiment une "jointure" dans le sens de la façon dont une base de données relationnelle le fait. La $lookup d'autre part, fait réellement le travail sur le serveur, et est plus ou moins analogue à un "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. Le .collection.name ici correspond en fait à la "chaîne" qui est le nom réel de la collection MongoDB telle qu'assignée au modèle. Puisque la mangouste "pluralise" les noms de collection par défaut et $lookup a besoin du nom réel de la collection MongoDB comme argument (puisqu'il s'agit d'une opération de serveur), alors c'est une astuce pratique à utiliser dans le code mongoose, par opposition au "codage en dur" directement du nom de la collection.

Bien que nous puissions également utiliser $filter sur les tableaux pour supprimer les éléments indésirables, il s'agit en fait de la forme la plus efficace en raison de l'optimisation du pipeline d'agrégation pour la condition spéciale de $lookup suivi à la fois d'un $unwind et un $match état.

Cela se traduit en fait par le regroupement des trois étapes du pipeline en une seule :

   { "$lookup" : {
     "from" : "itemtags",
     "as" : "tags",
     "localField" : "tags",
     "foreignField" : "_id",
     "unwinding" : {
       "preserveNullAndEmptyArrays" : false
     },
     "matching" : {
       "tagName" : {
         "$in" : [
           "funny",
           "politics"
         ]
       }
     }
   }}

Ceci est hautement optimal car l'opération réelle "filtre la collection à joindre en premier", puis elle renvoie les résultats et "déroule" le tableau. Les deux méthodes sont utilisées afin que les résultats ne dépassent pas la limite BSON de 16 Mo, qui est une contrainte que le client n'a pas.

Le seul problème est que cela semble "contre-intuitif" à certains égards, en particulier lorsque vous voulez les résultats dans un tableau, mais c'est ce que le $group est pour ici, car il reconstruit le formulaire de document original.

Il est également regrettable que nous ne puissions tout simplement pas pour le moment écrire $lookup dans la même syntaxe éventuelle que le serveur utilise. A mon humble avis, c'est un oubli à corriger. Mais pour l'instant, le simple fait d'utiliser la séquence fonctionnera et constitue l'option la plus viable avec les meilleures performances et évolutivité.

Addendum - MongoDB 3.6 et versions ultérieures

Bien que le modèle présenté ici soit assez optimisé en raison de la façon dont les autres étapes sont intégrées dans le $lookup , il a un défaut en ce que le "LEFT JOIN" qui est normalement inhérent à la fois à $lookup et les actions de populate() est nié par le "optimal" utilisation de $unwind ici qui ne conserve pas les tableaux vides. Vous pouvez ajouter le preserveNullAndEmptyArrays option, mais cela annule l'option "optimisé" séquence décrite ci-dessus et laisse essentiellement intactes les trois étapes qui seraient normalement combinées dans l'optimisation.

MongoDB 3.6 s'agrandit avec un "plus expressif" forme de $lookup permettant une expression "sous-pipeline". Ce qui non seulement répond à l'objectif de conserver le "LEFT JOIN" mais permet tout de même une requête optimale pour réduire les résultats renvoyés et avec une syntaxe très simplifiée :

Item.aggregate([
  { "$lookup": {
    "from": ItemTags.collection.name,
    "let": { "tags": "$tags" },
    "pipeline": [
      { "$match": {
        "tags": { "$in": [ "politics", "funny" ] },
        "$expr": { "$in": [ "$_id", "$$tags" ] }
      }}
    ]
  }}
])

Le $expr utilisé pour faire correspondre la valeur "locale" déclarée avec la valeur "étrangère" est en fait ce que MongoDB fait "en interne" maintenant avec le $lookup d'origine syntaxe. En exprimant sous cette forme, nous pouvons personnaliser le $match initial expression dans le "sous-pipeline" nous-mêmes.

En fait, en tant que véritable "pipeline d'agrégation", vous pouvez faire à peu près tout ce que vous pouvez faire avec un pipeline d'agrégation dans cette expression de "sous-pipeline", y compris "imbriquer" les niveaux de $lookup à d'autres collections connexes.

Une utilisation ultérieure dépasse un peu la portée de ce que la question demande ici, mais en ce qui concerne même la "population imbriquée", le nouveau modèle d'utilisation de $lookup permet que cela soit à peu près le même, et un "beaucoup" plus puissant dans son utilisation complète.

Exemple de travail

Ce qui suit donne un exemple utilisant une méthode statique sur le modèle. Une fois cette méthode statique implémentée, l'appel devient simplement :

  Item.lookup(
    {
      path: 'tags',
      query: { 'tags.tagName' : { '$in': [ 'funny', 'politics' ] } }
    },
    callback
  )

Ou améliorer pour être un peu plus moderne devient même :

  let results = await Item.lookup({
    path: 'tags',
    query: { 'tagName' : { '$in': [ 'funny', 'politics' ] } }
  })

Le rendant très similaire à .populate() dans la structure, mais il fait en fait la jointure sur le serveur à la place. Pour être complet, l'utilisation ici renvoie les données renvoyées vers des instances de document mongoose en fonction des cas parent et enfant.

C'est assez trivial et facile à adapter ou à utiliser tel quel dans la plupart des cas courants.

N.B L'utilisation d'async ici est juste pour la brièveté de l'exécution de l'exemple ci-joint. L'implémentation réelle est exempte de cette dépendance.

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 un peu plus moderne pour Node 8.x et supérieur avec async/await et aucune dépendance supplémentaire :

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

Et à partir de MongoDB 3.6 et supérieur, même sans le $unwind et $group bâtiment :

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

})()