Consultando después de poblar en Mangosta

Soy bastante nuevo para Mongoose y MongoDB en general, así que estoy teniendo dificultades para descubrir si algo como esto es posible:

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

¿Hay una manera mejor de hacer esto?

Editar

Disculpas por cualquier confusión. Lo que bash hacer es obtener todos los artículos que contengan la etiqueta divertida o la etiqueta política.

Editar

Documento sin cláusula 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 }, ... ] 

Con la cláusula where, obtengo una matriz vacía.

Con un MongoDB moderno mayor que 3.2, puede usar $lookup como alternativa para .populate() en la mayoría de los casos. Esto también tiene la ventaja de hacer realmente la unión “en el servidor” en oposición a lo que hace .populate() que es en realidad “múltiples consultas” para “emular” una unión.

Entonces .populate() no es realmente un “join” en el sentido de cómo lo hace una base de datos relacional. El operador de $lookup por otro lado, en realidad hace el trabajo en el servidor, y es más o menos análogo a un “UNIÓN IZQUIERDA” :

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

NB El .collection.name aquí en realidad evalúa la “cadena” que es el nombre real de la colección MongoDB según lo asignado al modelo. Como mongoose “pluraliza” nombres de colecciones por defecto y $lookup necesita el nombre real de la colección MongoDB como argumento (ya que es una operación del servidor), este es un truco útil para usar en código mongoose, en lugar de “codificar” la colección nombre directamente.

Si bien también pudimos usar $filter en matrices para eliminar los elementos no deseados, esta es la forma más eficiente debido a la optimización de canal agregado para la condición especial de $lookup seguida de una condición de $unwind y $match .

Esto realmente da como resultado que las tres etapas de la tubería se unan en una sola:

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

Esto es altamente óptimo ya que la operación real “filtra la colección para unirse primero”, luego devuelve los resultados y “desenrolla” la matriz. Ambos métodos se emplean para que los resultados no rompan el límite de BSON de 16 MB, que es una restricción que el cliente no tiene.

El único problema es que parece “contraintuitivo” en algunos aspectos, especialmente cuando se desean los resultados en una matriz, pero para eso es el $group , ya que se reconstruye a la forma del documento original.

También es desafortunado que, en este momento, no podamos escribir $lookup en la misma syntax eventual que usa el servidor. En mi humilde opinión, este es un descuido que debe corregirse. Pero por ahora, simplemente usar la secuencia funcionará y es la opción más viable con el mejor rendimiento y escalabilidad.

Adición – MongoDB 3.6 y más

Aunque el patrón que se muestra aquí está bastante optimizado debido a la forma en que las otras etapas se incluyen en $lookup , tiene una falla en que la “LEFT JOIN” que normalmente es inherente a $lookup y a las acciones de populate() es negada por el uso “óptimo” de $unwind aquí que no conserva las matrices vacías. Puede agregar la opción preserveNullAndEmptyArrays , pero esto anula la secuencia “optimizada” descrita anteriormente y esencialmente deja intactas las tres etapas que normalmente se combinarían en la optimización.

MongoDB 3.6 se expande con una forma de $lookup “más expresiva” que permite una expresión de “sub-canalización”. Lo cual no solo cumple el objective de conservar el “LEFT JOIN”, sino que también permite una consulta óptima para reducir los resultados devueltos y con una syntax muy simplificada:

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

El $expr utilizado para hacer coincidir el valor “local” declarado con el valor “extranjero” es en realidad lo que MongoDB hace “internamente” ahora con la syntax de $lookup original. Al express de esta forma, podemos adaptar la expresión inicial de $match dentro de la “sub-tubería”.

De hecho, como un verdadero “canal de agregación”, puede hacer casi todo lo que puede hacer con una canalización de agregación dentro de esta expresión de “sub-canalización”, que incluye “anidar” los niveles de $lookup en otras colecciones relacionadas.

El uso adicional está un poco más allá del scope de lo que aquí se pregunta, pero en relación con incluso la “población anidada”, el nuevo patrón de uso de $lookup permite que esto sea muy similar, y un “lote” más poderoso en su totalidad uso.


Ejemplo de trabajo

Lo siguiente da un ejemplo usando un método estático en el modelo. Una vez que se implementa ese método estático, la llamada simplemente se convierte en:

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

O mejorar para ser un poco más moderno, incluso se convierte en:

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

Haciéndolo muy similar a .populate() en la estructura, pero en realidad está haciendo la unión en el servidor. Para completar, el uso aquí arroja los datos devueltos a las instancias de documentos de mongooses según los casos padre e hijo.

Es bastante trivial y fácil de adaptar o simplemente usar como lo es para la mayoría de los casos comunes.

NB El uso de asincrónica aquí es solo por brevedad al ejecutar el ejemplo adjunto. La implementación real está libre de esta dependencia.

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

O un poco más moderno para Node 8.xy superior con async/await y sin dependencias adicionales:

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

Y desde MongoDB 3.6 y versiones posteriores, incluso sin el $unwind y $group building:

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

lo que está solicitando no se admite directamente, pero se puede lograr agregando otro paso de filtro después de que la consulta regrese.

primero, .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } ) es definitivamente lo que necesita hacer para filtrar los documentos de las tags. luego, después de que la consulta regrese, deberá filtrar manualmente los documentos que no tengan documentos de tags que coincidan con los criterios de llenado. algo como:

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

Intenta reemplazar

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

por

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

Actualización: por favor, eche un vistazo a los comentarios: esta respuesta no coincide correctamente con la pregunta, pero tal vez responda a otras preguntas de los usuarios que aparecieron (creo que debido a las votaciones ascendentes) por lo que no eliminaré esta “respuesta”:

Primero: sé que esta pregunta está desactualizada, pero busqué exactamente este problema y esta publicación de SO fue la entrada de Google n. ° 1. Así que implementé la versión docs.filter (respuesta aceptada), pero cuando leí en mongoose v4.6.0 docs ahora podemos simplemente usar:

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

Espero que esto ayude a los futuros usuarios de máquinas de búsqueda.

La respuesta de @aaronheckmann funcionó para mí, pero tuve que reemplazar return doc.tags.length; para return doc.tags != null; porque ese campo contiene nulo si no coincide con las condiciones escritas dentro de llenar. Entonces el código final:

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

Después de tener el mismo problema recientemente, he encontrado la siguiente solución:

En primer lugar, encuentre todas las tags de elemento donde tagName sea ‘chistoso’ o ‘político’ y devuelva una matriz de ItemTag _ids.

A continuación, busque los elementos que contienen todos los ítems _Art en el conjunto de 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 }); });