node.js - multiple - Consultando después de poblar en Mangosta
relation mongoose (6)
Soy bastante nuevo para Mongoose y MongoDB en general, así que estoy teniendo dificultades para descubrir si algo así 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 mejor manera de hacer esto?
Editar
Disculpas por cualquier confusión. Lo que intento 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.
Intenta reemplazar
.populate(''tags'').where(''tags.tagName'').in([''funny'', ''politics''])
por
.populate( ''tags'', null, { tagName: { $in: [''funny'', ''politics''] } } )
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 etiquetas. 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
});
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.
Después de tener el mismo problema recientemente, he encontrado la siguiente solución:
En primer lugar, encuentre todas las etiquetas 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 etiquetas
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
});
});
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 quieren 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 sintaxis 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 con el mejor rendimiento y escalabilidad.
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
)
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 mangostas 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();
}
)
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
});