web-dev-qa-db-fra.com

Interrogation après le remplissage dans Mongoose

Je suis assez nouveau pour Mongoose et MongoDB en général, donc j'ai du mal à déterminer si quelque chose comme ça 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
    });

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

Modifier

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

Modifier

Document sans clause where:

[{ 
    _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.

63
jschr

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 fait .populate() qui est en fait "plusieurs requêtes" pour "émuler" une jointure.

Donc .populate() n'est pas pas vraiment une "jointure" dans le sens de la façon dont une base de données relationnelle le fait. L'opérateur $lookup , en revanche, fait 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 Correspond ici à la "chaîne" qui est le nom réel de la collection MongoDB tel qu'assigné au modèle. Étant donné que mongoose "pluralise" les noms de collection par défaut et $lookup a besoin du nom de collection MongoDB réel comme argument (car il s'agit d'une opération de serveur), alors c'est une astuce pratique à utiliser dans mongoose code, par opposition au "codage en dur" du nom de la collection directement.

Bien que nous puissions également utiliser $filter sur les tableaux pour supprimer les éléments indésirables, c'est en fait la forme la plus efficace en raison de Aggregation Pipeline Optimization pour la condition spéciale de as $lookup suivi à la fois d'une condition $unwind et d'une condition $match .

Cela se traduit en fait par les trois étapes du pipeline en un seul:

   { "$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 pour la joindre en premier", puis elle retourne 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, ce qui n'est pas une contrainte pour le client.

Le seul problème est qu'il semble "contre-intuitif" à certains égards, en particulier lorsque vous voulez les résultats dans un tableau, mais c'est à cela que sert $group ici, car il reconstruit à la forme originale du document.

Il est également regrettable que nous ne puissions tout simplement pas écrire à l'heure actuelle $lookup dans la même syntaxe que le serveur utilise. À mon humble avis, il s'agit d'un oubli à corriger. Mais pour l'instant, la simple utilisation de la séquence fonctionnera et est l'option la plus viable avec les meilleures performances et évolutivité.

Addendum - MongoDB 3.6 et supérieur

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

MongoDB 3.6 se développe avec une forme "plus expressive" de $lookup permettant une expression "sous-pipeline". Ce qui non seulement répond à l'objectif de conserver le "LEFT JOIN" mais permet toujours une requête optimale pour réduire les résultats renvoyés et avec une syntaxe beaucoup plus 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 l'original $lookup syntaxe. En exprimant sous cette forme, nous pouvons adapter l'expression $match initiale 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 vers d'autres collections associées.

Une utilisation ultérieure dépasse un peu la portée de ce que la question pose ici, mais en ce qui concerne même la "population imbriquée", le nouveau modèle d'utilisation de $lookup permet que cela soit sensiblement le même. , et un "lot" plus puissant dans sa pleine utilisation.


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. Par souci d'exhaustivité, l'utilisation ici renvoie les données renvoyées aux instances de document mangouste selon les cas parent et enfant.

C'est assez banal et facile à adapter ou à utiliser comme dans la plupart des cas.

N.B L'utilisation de 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 sans dépendances supplémentaires:

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 versions ultérieures, même sans la construction $unwind et $group :

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

})()
39
Neil Lunn

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

tout d'abord, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) est définitivement ce que vous devez faire pour filtrer les documents de balises. puis, après le retour de la requête, vous devrez filtrer manuellement les documents qui n'ont pas de documents tags correspondant 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
});
38
aaronheckmann

Essayez de remplacer

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

par

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

Mise à jour: s'il vous plaît jetez un oeil aux commentaires - cette réponse ne correspond pas correctement à la question, mais peut-être qu'elle répond à d'autres questions des utilisateurs rencontrés (je pense qu'en raison des votes positifs) donc je ne supprimerai pas cette "réponse":

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

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 des moteurs de recherche.

14
Fabian

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

Tout d'abord, recherchez tous les ItemTags où tagName est soit "drôle" soit "politique" et retournez un tableau d'ItidTag _ids.

Ensuite, recherchez les éléments qui contiennent tous les _ID de ItemTag dans le tableau des balises

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
     });
  });
2
OllyBarca

réponse de @ aaronheckmann a fonctionné pour moi mais j'ai dû remplacer return doc.tags.length; à return doc.tags != null; car ce champ contient null s'il ne correspond pas aux conditions écrites à l'intérieur de populate. Donc, le code final:

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