97 votes

Interrogation après populage dans Mongoose

Je suis assez novice en matière de Mongoose et de MongoDB en général, et j'ai donc du mal à savoir si une telle chose est possible :

Item = new Schema({
    id: Schema.ObjectId,
    dateCreated: { type: Date, default: Date.now },
    title: { type: String, default: 'No Title' },
    description: { type: String, default: 'No Description' },
    tags: [ { type: Schema.ObjectId, ref: 'ItemTag' }]
});

ItemTag = new Schema({
    id: Schema.ObjectId,
    tagId: { type: Schema.ObjectId, ref: 'Tag' },
    tagName: { type: String }
});

var query = Models.Item.find({});

query
    .desc('dateCreated')
    .populate('tags')
    .where('tags.tagName').in(['funny', 'politics'])
    .run(function(err, docs){
       // docs is always empty
    });

Y a-t-il une meilleure façon de procéder ?

Modifier

Toutes nos excuses pour toute confusion. Ce que j'essaie de faire, c'est d'obtenir tous les éléments qui contiennent soit le tag drôle, soit le tag politique.

Modifier

Document sans clause de localisation :

[{ 
    _id: 4fe90264e5caa33f04000012,
    dislikes: 0,
    likes: 0,
    source: '/uploads/loldog.jpg',
    comments: [],
    tags: [{
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'movies',
        tagId: 4fe64219007e20e644000007,
        _id: 4fe90270e5caa33f04000015,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    },
    { 
        itemId: 4fe90264e5caa33f04000012,
        tagName: 'funny',
        tagId: 4fe64219007e20e644000002,
        _id: 4fe90270e5caa33f04000017,
        dateCreated: Tue, 26 Jun 2012 00:29:36 GMT,
        rating: 0,
        dislikes: 0,
        likes: 0 
    }],
    viewCount: 0,
    rating: 0,
    type: 'image',
    description: null,
    title: 'dogggg',
    dateCreated: Tue, 26 Jun 2012 00:29:24 GMT 
 }, ... ]

Avec la clause where, j'obtiens un tableau vide.

80voto

Neil Lunn Points 1

Avec un MongoDB moderne supérieur à 3.2, vous pouvez utiliser $lookup comme alternative à .populate() dans la plupart des cas. Cette méthode présente également l'avantage d'effectuer la jointure "sur le serveur", contrairement à ce qui se passe dans le cas de la jointure de données. .populate() fait qui est en fait "requêtes multiples" pour "émuler". un joint.

Alors .populate() es no Il ne s'agit pas vraiment d'une "jointure" au sens où l'entend une base de données relationnelle. Le site $lookup d'autre part, effectue réellement le travail sur le serveur, et est plus ou moins analogue à un opérateur de "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 site .collection.name évalue en fait la "chaîne" qui est le nom réel de la collection MongoDB tel qu'il est attribué au modèle. Puisque Mongoose "pluralise" les noms de collection par défaut et que $lookup a besoin du nom de la collection MongoDB en tant qu'argument (puisqu'il s'agit d'une opération serveur), c'est une astuce pratique à utiliser dans le code Mongoose, plutôt que de coder directement le nom de la collection.

Alors que nous pourrions également utiliser $filter sur les tableaux pour supprimer les éléments non désirés, c'est en fait la forme la plus efficace en raison des éléments suivants Optimisation du pipeline d'agrégation pour la condition spéciale de l'as $lookup suivi à la fois d'un $unwind et un $match condition.

En fait, les trois étapes du pipeline sont regroupées en une seule :

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

C'est très optimal car l'opération actuelle "filtre la collection à joindre en premier", puis renvoie les résultats et "déroule" le tableau. Les deux méthodes sont employées pour que les résultats ne dépassent pas la limite BSON de 16 Mo, une contrainte que le client n'a pas.

Le seul problème est que cela semble "contre-intuitif" à certains égards, en particulier lorsque l'on veut obtenir les résultats dans un tableau, mais c'est ce que fait la fonction $group est pour ici, car il reconstruit à la forme du document original.

Il est également regrettable que nous ne puissions tout simplement pas, à l'heure actuelle, écrire $lookup dans la même syntaxe éventuelle que celle utilisée par le serveur. IMHO, il s'agit d'un oubli à corriger. Mais pour l'instant, utiliser simplement la séquence fonctionnera et constitue l'option la plus viable avec les meilleures performances et la meilleure évolutivité.

Addendum - MongoDB 3.6 et versions ultérieures

Bien que le modèle présenté ici soit assez optimisé en raison de la manière dont les autres étapes sont intégrées dans la $lookup mais il présente un défaut : le "LEFT JOIN", qui est normalement inhérent aux deux types d'utilisateurs, ne peut être utilisé. $lookup et les actions de populate() est annulé par le "optimal" l'usage de $unwind ici qui ne préserve pas les tableaux vides. Vous pouvez ajouter le preserveNullAndEmptyArrays mais cela annule l'option "optimisé" décrit ci-dessus et laisse essentiellement intactes les trois étapes qui seraient normalement combinées dans l'optimisation.

MongoDB 3.6 s'étend avec une "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 aussi une requête optimale pour réduire les résultats retournés et avec une syntaxe beaucoup plus simple :

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

En $expr utilisé afin de faire correspondre la valeur "locale" déclarée avec la valeur "étrangère" est en fait ce que MongoDB fait "en interne" maintenant avec la valeur originale $lookup syntaxe. En l'exprimant sous cette forme, nous pouvons adapter l'initial $match à l'intérieur même de la "sous-pipeline".

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 plus poussée dépasse un peu le cadre de la question posée ici, mais en ce qui concerne même la "population imbriquée", le nouveau schéma d'utilisation de $lookup permet de le faire à peu près de la même façon, et une "lot" plus puissant dans sa pleine utilisation.


Exemple de travail

Voici un exemple d'utilisation d'une méthode statique sur le modèle. Une fois que cette méthode statique est implémentée, l'appel devient simplement :

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

Ou l'amélioration pour être un peu plus moderne devient même :

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

Ce qui le rend 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 reconvertit les données retournées en instances de documents de la mangouste selon les cas parent et enfant.

Il est assez trivial et facile à adapter ou à utiliser tel quel pour les cas les plus courants.

N.B. L'utilisation de asynchrone ici, c'est juste pour la brièveté de l'exécution de l'exemple ci-joint. L'implémentation réelle est libre 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 plus 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, même sans l'option $unwind y $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()
  }

})()

40voto

aaronheckmann Points 3875

Ce que vous demandez n'est pas directement pris en charge mais peut être réalisé en ajoutant une autre étape de filtrage après le retour de la requête.

d'abord, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) est certainement ce que vous devez faire pour filtrer les documents de balises. Ensuite, après le retour de la requête, vous devrez filtrer manuellement les documents qui n'ont pas de balises tags les documents qui correspondent aux critères de remplissage. quelque chose comme :

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags.length;
   })
   // do stuff with docs
});

24voto

Aafreen Sheikh Points 897

Essayez de remplacer

.populate('tags').where('tags.tagName').in(['funny', 'politics']) 

par

.populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )

15voto

Fabian Points 994

Mise à jour : Veuillez jeter un coup d'oeil aux commentaires - cette réponse ne correspond pas correctement à la question, mais peut-être qu'elle répond à d'autres questions d'utilisateurs qui sont tombées dessus (je le pense à cause des upvotes) donc je ne supprimerai pas cette "réponse" :

Premièrement : Je sais que cette question est vraiment dépassée, mais j'ai cherché exactement ce problème et ce post SO était l'entrée #1 de Google. J'ai donc implémenté le docs.filter (réponse acceptée), mais comme je l'ai lu dans la version documentation sur la mangouste v4.6.0 que nous pouvons maintenant utiliser simplement :

Item.find({}).populate({
    path: 'tags',
    match: { tagName: { $in: ['funny', 'politics'] }}
}).exec((err, items) => {
  console.log(items.tags) 
  // contains only tags where tagName is 'funny' or 'politics'
})

J'espère que cela aidera les futurs utilisateurs de moteurs de recherche.

3voto

OllyBarca Points 1088

Après avoir rencontré le même problème récemment, j'ai trouvé la solution suivante :

Tout d'abord, trouver tous les ItemTags où tagName est soit 'funny' soit 'politics' et retourner un tableau d'ItemTag _ids.

Ensuite, trouver les éléments qui contiennent tous les _identifiants ItemTag dans le tableau des tags.

ItemTag
  .find({ tagName : { $in : ['funny','politics'] } })
  .lean()
  .distinct('_id')
  .exec((err, itemTagIds) => {
     if (err) { console.error(err); }
     Item.find({ tag: { $all: itemTagIds} }, (err, items) => {
        console.log(items); // Items filtered by tagName
     });
  });

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X