Abfrage nach dem Auffüllen in Mongoose


82

Ich bin ziemlich neu in Mongoose und MongoDB im Allgemeinen, daher fällt es mir schwer herauszufinden, ob so etwas möglich ist:

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

Gibt es einen besseren Weg, dies zu tun?

Bearbeiten

Entschuldigung für die Verwirrung. Ich versuche, alle Elemente abzurufen, die entweder das lustige Tag oder das Politik-Tag enthalten.

Bearbeiten

Dokument ohne where-Klausel:

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

Mit der where-Klausel erhalte ich ein leeres Array.

Antworten:


61

Mit einer modernen MongoDB größer als 3,2 können Sie in den meisten Fällen $lookupeine Alternative zu .populate()verwenden. Dies hat auch den Vorteil , tatsächlich tun , die Join „auf dem Server“ im Gegensatz zu dem, was .populate()tut , was ist eigentlich „mehrere Anfragen“ zu „emulieren“ Join.

Es .populate()handelt sich also nicht wirklich um einen "Join" im Sinne einer relationalen Datenbank. Der $lookupBediener hingegen erledigt die Arbeit tatsächlich auf dem Server und ist mehr oder weniger analog zu einem "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"
  }
)

NB Die .collection.namehier wertet tatsächlich auf die „string“ , dass der tatsächliche Name der MongoDB Sammlung ist als dem Modell zugeordnet. Da Mungo standardmäßig Sammlungsnamen "pluralisiert" und $lookupden tatsächlichen MongoDB-Sammlungsnamen als Argument benötigt (da es sich um eine Serveroperation handelt), ist dies ein praktischer Trick, der im Mungo-Code verwendet werden kann, anstatt den Sammlungsnamen direkt "hart zu codieren" .

Während wir auch $filterArrays verwenden könnten , um unerwünschte Elemente zu entfernen, ist dies aufgrund der Aggregation Pipeline Optimization für die spezielle Bedingung, $lookupgefolgt von einer $unwindund einer $matchBedingung , tatsächlich die effizienteste Form .

Dies führt tatsächlich dazu, dass die drei Pipeline-Stufen zu einer zusammengefasst werden:

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

Dies ist sehr optimal, da die eigentliche Operation "die Sammlung filtert, um sie zuerst zu verbinden", dann die Ergebnisse zurückgibt und das Array "abwickelt". Beide Methoden werden verwendet, damit die Ergebnisse die BSON-Grenze von 16 MB nicht überschreiten. Dies ist eine Einschränkung, die der Client nicht hat.

Das einzige Problem ist, dass es in gewisser Weise "kontraintuitiv" erscheint, insbesondere wenn Sie die Ergebnisse in einem Array haben möchten, aber dafür ist das $grouphier gedacht, da es in die ursprüngliche Dokumentform rekonstruiert wird.

Es ist auch bedauerlich, dass wir zu diesem Zeitpunkt einfach nicht $lookupin der gleichen Syntax schreiben können, die der Server verwendet. IMHO, dies ist ein Versehen, das korrigiert werden muss. Im Moment funktioniert die einfache Verwendung der Sequenz und ist die praktikabelste Option mit der besten Leistung und Skalierbarkeit.

Nachtrag - MongoDB 3.6 und höher

Obwohl das hier gezeigte Muster aufgrund der Art und Weise, wie die anderen Stufen in die Rolle gerollt werden , ziemlich optimiert ist$lookup , hat es einen Fehler darin, dass der "LEFT JOIN", der normalerweise beiden inhärent ist, $lookupund die Aktionen von populate()durch die "optimale" Verwendung von negiert werden $unwindhier, wo leere Arrays nicht erhalten bleiben. Sie können die preserveNullAndEmptyArraysOption hinzufügen , dies negiert jedoch die oben beschriebene "optimierte" Sequenz und lässt im Wesentlichen alle drei Stufen intakt, die normalerweise bei der Optimierung kombiniert würden.

MongoDB 3.6 wird um eine "ausdrucksstärkere" Form erweitert, bei der $lookupein "Sub-Pipeline" -Ausdruck zugelassen wird. Dies erfüllt nicht nur das Ziel, den "LEFT JOIN" beizubehalten, sondern ermöglicht auch eine optimale Abfrage, um die zurückgegebenen Ergebnisse mit einer stark vereinfachten Syntax zu reduzieren:

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

Das, was $exprverwendet wird, um den deklarierten "lokalen" Wert mit dem "fremden" Wert abzugleichen, ist tatsächlich das, was MongoDB jetzt "intern" mit der ursprünglichen $lookupSyntax macht. Indem wir in dieser Form ausdrücken, können wir den anfänglichen $matchAusdruck innerhalb der "Sub-Pipeline" selbst anpassen.

Tatsächlich können Sie als echte "Aggregationspipeline" fast alles tun, was Sie mit einer Aggregationspipeline innerhalb dieses Ausdrucks "Subpipeline" tun können, einschließlich des "Verschachtelns" der Ebenen $lookupin andere verwandte Sammlungen.

Die weitere Verwendung geht etwas über den Rahmen der hier gestellten Frage hinaus, aber in Bezug auf selbst "verschachtelte Grundgesamtheit" ermöglicht das neue Verwendungsmuster von, $lookupdass dies weitgehend gleich ist und "viel" leistungsfähiger in seiner vollen Verwendung ist.


Arbeitsbeispiel

Im Folgenden finden Sie ein Beispiel für die Verwendung einer statischen Methode für das Modell. Sobald diese statische Methode implementiert ist, wird der Aufruf einfach:

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

Oder die Verbesserung, um ein bisschen moderner zu werden, wird sogar:

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

Es ist der .populate()Struktur sehr ähnlich , führt aber stattdessen den Join auf dem Server durch. Der Vollständigkeit halber werden bei der Verwendung hier die zurückgegebenen Daten gemäß den übergeordneten und untergeordneten Fällen an mongoose Dokumentinstanzen zurückgegeben.

Es ist ziemlich trivial und einfach anzupassen oder einfach zu verwenden, wie es in den meisten Fällen der Fall ist.

Hinweis: Die Verwendung von Async dient hier nur der Kürze der Ausführung des beigefügten Beispiels. Die eigentliche Implementierung ist frei von dieser Abhängigkeit.

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

Oder etwas moderner für Node 8.x und höher async/awaitohne zusätzliche Abhängigkeiten:

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

Und ab MongoDB 3.6 und höher, auch ohne $unwindund $groupGebäude:

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

})()

3
Ich verwende Mongo / Mongoose nicht mehr, aber ich habe Ihre Antwort akzeptiert, da dies eine beliebte Frage ist und anscheinend für andere hilfreich war. Ich bin froh zu sehen, dass dieses Problem jetzt eine skalierbarere Lösung hat. Vielen Dank für die Aktualisierung der Antwort.
jschr

40

Was Sie verlangen, wird nicht direkt unterstützt, kann jedoch durch Hinzufügen eines weiteren Filterschritts nach der Rückkehr der Abfrage erreicht werden.

Erstens müssen .populate( 'tags', null, { tagName: { $in: ['funny', 'politics'] } } )Sie auf jeden Fall die Tags-Dokumente filtern. Nachdem die Abfrage zurückgegeben wurde, müssen Sie Dokumente manuell herausfiltern, die keine tagsDokumente enthalten, die den Auffüllkriterien entsprechen. etwas wie:

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

1
Hey Aaron, danke für die Antwort. Ich kann mich irren, aber wird das $ in on populate () nicht nur die übereinstimmenden Tags füllen? Alle zusätzlichen Tags auf dem Element werden herausgefiltert. Es hört sich so an, als müsste ich alle Elemente füllen und den zweiten Filterschritt dann basierend auf dem Tag-Namen reduzieren.
jschr

@aaronheckmann Ich habe Ihre vorgeschlagene Lösung implementiert. Sie werden gleich nach .exec filtern, da die Abfrage zum Auffüllen zwar nur die erforderlichen Objekte auffüllt, aber dennoch den gesamten Datensatz zurückgibt. Denken Sie, dass es in der neueren Version von Mongoose eine Option gibt, nur aufgefüllte Datensätze zurückzugeben, damit wir keine weitere Filterung vornehmen müssen?
Aqib Mumtaz

Ich bin auch neugierig auf die Leistung. Wenn die Abfrage am Ende den gesamten Datensatz zurückgibt, gibt es keinen Zweck für die Populationsfilterung? Was sagst du? Ich passe die Populationsabfrage für die Leistungsoptimierung an, aber auf diese Weise wird die Leistung für große Datenmengen nicht besser.
Aqib Mumtaz

mongoosejs.com/docs/api.html#query_Query-populate hat alle Details, wenn jemand anderes interessiert ist
Samazi

Wie passen sie in verschiedenen Feldern zusammen, wenn sie ausgefüllt sind?
Nicogaldo

20

Versuchen Sie es zu ersetzen

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

durch

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

1
Danke für die Antwort. Ich glaube, dies bevölkert nur jedes Element mit lustigen oder politischen Elementen, was die Elternliste nicht reduzieren würde. Was ich eigentlich gerne hätte, sind nur Artikel, deren Tag lustig oder politisch ist.
jschr

Können Sie zeigen, wie Ihr Dokument aussieht? Weil ein 'wo' innerhalb des Tags-Arrays für mich wie eine gültige Operation erscheint. Verstehen wir nur die Syntax falsch? Haben Sie versucht, diese 'wo'-Klausel vollständig zu entfernen und zu überprüfen, ob etwas zurückgegeben wird? Um zu testen, ob das Schreiben von 'tags.tagName' syntaktisch in Ordnung ist, können Sie alternativ die Referenz für eine Weile vergessen und Ihre Abfrage mit einem eingebetteten Array im Dokument 'Item' ausprobieren.
Aafreen Sheikh

Ich habe meinen ursprünglichen Beitrag mit dem Dokument bearbeitet. Ich konnte es mit dem Modell als eingebettetes Array in Item mit Erfolg testen, aber leider muss es ein DBRef sein, da ItemTag häufig aktualisiert wird. Nochmals vielen Dank für die Hilfe.
jschr

15

Update: Bitte werfen Sie einen Blick auf die Kommentare - diese Antwort stimmt nicht richtig mit der Frage überein, beantwortet aber möglicherweise andere Fragen von Benutzern, die auf sie gestoßen sind (ich denke, das liegt an den positiven Stimmen), sodass ich diese "Antwort" nicht löschen werde:

Erstens: Ich weiß, dass diese Frage wirklich veraltet ist, aber ich habe genau nach diesem Problem gesucht und dieser SO-Beitrag war der Google-Eintrag Nr. 1. Also habe ich die docs.filterVersion implementiert (akzeptierte Antwort), aber während ich in den Dokumenten zu Mungo v4.6.0 lese , können wir jetzt einfach Folgendes verwenden:

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

Hoffe, dies hilft zukünftigen Suchmaschinenbenutzern.


3
Aber dies wird nur das items.tags-Array sicher filtern? Artikel werden unabhängig vom TagName zurückgegeben ...
OllyBarca

1
Das ist richtig, @OllyBarca. Laut den Dokumenten wirkt sich die Übereinstimmung nur auf die Populationsabfrage aus.
andreimarinescu

1
Ich denke, das beantwortet die Frage nicht
Z.Alpha

1
@ Fabian das ist kein Fehler. Nur die Populationsabfrage (in diesem Fall fans) wird gefiltert. Das tatsächlich zurückgegebene Dokument (das als Eigenschaft Storyenthält fans) wird nicht beeinflusst oder gefiltert.
EnKrypt

2
Diese Antwort ist daher aus den in den Kommentaren genannten Gründen nicht korrekt. Wer dies in Zukunft betrachtet, sollte vorsichtig sein.
EnKrypt

3

Nachdem ich kürzlich selbst das gleiche Problem hatte, habe ich die folgende Lösung gefunden:

Suchen Sie zunächst alle ItemTags, bei denen tagName entweder "lustig" oder "politisch" ist, und geben Sie ein Array von ItemTag _ids zurück.

Suchen Sie dann im Tags-Array nach Elementen, die alle ItemTag _ids enthalten

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

Wie ich es gemacht habe const tagsIds = warte auf this.tagModel .find ({name: {$ in: tags}}) .lean () .distinct ('_ id'); return this.adviceModel.find ({tags: {$ all: tagsIds}});
Dragos Lupei

1

Die Antwort von @aaronheckmann hat bei mir funktioniert, aber ich musste sie ersetzen return doc.tags.length;, return doc.tags != null;da dieses Feld null enthält, wenn es nicht mit den in populate geschriebenen Bedingungen übereinstimmt. Also der endgültige Code:

query....
.exec(function(err, docs){
   docs = docs.filter(function(doc){
     return doc.tags != null;
   })
   // do stuff with docs
});
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.