MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Abfragen nach dem Auffüllen in Mongoose

Mit einer modernen MongoDB größer als 3.2 können Sie $lookup verwenden als Alternative zu .populate() in den meisten Fällen. Dies hat auch den Vorteil, dass der Join tatsächlich "auf dem Server" durchgeführt wird, im Gegensatz zu .populate() tut, was eigentlich "mehrere Abfragen" zu "emulieren" sind ein Beitritt.

Also .populate() ist nicht wirklich ein "Join" im Sinne einer relationalen Datenbank. Die $lookup Operator hingegen erledigt eigentlich die Arbeit 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. Der .collection.name Hier wird tatsächlich die "Zeichenfolge" ausgewertet, die der tatsächliche Name der MongoDB-Sammlung ist, wie sie dem Modell zugewiesen ist. Da Mongoose Sammlungsnamen standardmäßig "pluralisiert" und $lookup den tatsächlichen Namen der MongoDB-Sammlung als Argument benötigt ( da es sich um eine Serveroperation handelt ), dann ist dies ein praktischer Trick, der im Mongoose-Code verwendet werden kann, im Gegensatz zur direkten "Festcodierung" des Sammlungsnamens.

Wir könnten aber auch $filter verwenden auf Arrays, um die unerwünschten Elemente zu entfernen, ist dies tatsächlich die effizienteste Form aufgrund der Aggregation Pipeline-Optimierung für die spezielle Bedingung von als $lookup gefolgt von einem $unwind und ein $match Zustand.

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 zu verbindende Sammlung zuerst filtert", dann die Ergebnisse zurückgibt und das Array "entwindet". Beide Methoden werden verwendet, damit die Ergebnisse die BSON-Grenze von 16 MB nicht überschreiten, was eine Einschränkung ist, 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 das ist es, was die $group ist für hier, da es die ursprüngliche Dokumentform rekonstruiert.

Es ist auch bedauerlich, dass wir zu diesem Zeitpunkt einfach nicht $lookup schreiben können in der gleichen eventuellen Syntax, die der Server verwendet. IMHO, das ist ein Versehen, das korrigiert werden muss. Aber 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 ziemlich optimiert ist aufgrund dessen, wie die anderen Stufen in $lookup gerollt werden , es hat einen Fehler, nämlich das "LEFT JOIN", das normalerweise beiden $lookup innewohnt und die Aktionen von populate() wird durch das "optimal" negiert Verwendung von $unwind Hier werden keine leeren Arrays beibehalten. Sie können preserveNullAndEmptyArrays hinzufügen Option, aber dies negiert das "optimiert" oben beschriebene Reihenfolge und lässt im Wesentlichen alle drei Stufen intakt, die normalerweise bei der Optimierung kombiniert würden.

MongoDB 3.6 wird um ein "ausdrucksstärkeres" erweitert Form von $lookup Zulassen eines "Sub-Pipeline"-Ausdrucks. Das erfüllt nicht nur das Ziel, den "LEFT JOIN" beizubehalten, sondern ermöglicht dennoch eine optimale Abfrage, um die zurückgegebenen Ergebnisse zu reduzieren, und das mit einer stark vereinfachten Syntax:

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

Der $expr verwendet wird, um den deklarierten „lokalen“ Wert mit dem „fremden“ Wert abzugleichen, ist eigentlich das, was MongoDB jetzt „intern“ mit dem ursprünglichen $lookup macht Syntax. Indem wir diese Form ausdrücken, können wir das anfängliche $match anpassen Ausdruck innerhalb der "Sub-Pipeline" selbst.

Tatsächlich können Sie als echte „Aggregationspipeline“ fast alles tun, was Sie mit einer Aggregationspipeline innerhalb dieses „Sub-Pipeline“-Ausdrucks tun können, einschließlich des „Verschachtelns“ der Ebenen von $lookup zu anderen verwandten Sammlungen.

Die weitere Verwendung geht etwas über den Rahmen dessen hinaus, was die Frage hier stellt, aber in Bezug auf sogar "verschachtelte Population" dann das neue Verwendungsmuster von $lookup ermöglicht, dass dies ziemlich gleich ist, und ein "viel" leistungsstärker bei voller Nutzung.

Arbeitsbeispiel

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

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

Oder etwas moderner zu machen wird sogar:

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

Dadurch ist es .populate() sehr ähnlich in der Struktur, aber es führt stattdessen den Join auf dem Server durch. Der Vollständigkeit halber wirft die Verwendung hier die zurückgegebenen Daten zurück in die Mongoose-Dokumentinstanzen, und zwar entsprechend sowohl den übergeordneten als auch den untergeordneten Fällen.

Es ist ziemlich trivial und einfach anzupassen oder einfach so zu verwenden, wie es für die meisten gängigen Fälle ist.

NB Die Verwendung von async hier dient 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 mit async/await und keine zusätzlichen 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 auch ohne $unwind und $group Gebä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()
  }

})()