sql >> Database >  >> NoSQL >> MongoDB

Query na bevolken in Mongoose

Met een moderne MongoDB groter dan 3.2 kunt u $lookup . gebruiken als alternatief voor .populate() in de meeste gevallen. Dit heeft ook het voordeel dat de join daadwerkelijk "op de server" wordt uitgevoerd in tegenstelling tot wat .populate() doet wat eigenlijk "meerdere zoekopdrachten" is om te "emuleren" een deelname.

Dus .populate() is niet echt een "join" in de zin van hoe een relationele database het doet. De $lookup operator daarentegen doet het werk op de server en is min of meer analoog aan een "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 De .collection.name hier evalueert feitelijk naar de "string" die de werkelijke naam is van de MongoDB-verzameling zoals toegewezen aan het model. Omdat mangoest standaard collectienamen "pluraliseert" en $lookup de daadwerkelijke MongoDB-verzamelingsnaam als argument nodig heeft (aangezien het een serverbewerking is), dan is dit een handige truc om te gebruiken in mangoestcode, in tegenstelling tot het "hard coderen" van de verzamelingsnaam rechtstreeks.

Hoewel we ook $filter . kunnen gebruiken op arrays om de ongewenste items te verwijderen, dit is eigenlijk de meest efficiënte vorm vanwege Aggregation Pipeline Optimization voor de speciale voorwaarde van als $lookup gevolgd door zowel een $unwind en een $match staat.

Dit resulteert er feitelijk in dat de drie pijplijnfasen in één worden samengevoegd:

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

Dit is zeer optimaal omdat de eigenlijke bewerking "de verzameling filtert om eerst samen te voegen", vervolgens de resultaten retourneert en de array "afwikkelt". Beide methoden worden gebruikt, zodat de resultaten de BSON-limiet van 16 MB niet overschrijden, wat een beperking is die de client niet heeft.

Het enige probleem is dat het in sommige opzichten "contra-intuïtief" lijkt, vooral als je de resultaten in een array wilt, maar dat is wat de $group is voor hier, omdat het wordt gereconstrueerd naar de originele documentvorm.

Het is ook jammer dat we op dit moment gewoon niet echt $lookup kunnen schrijven in dezelfde uiteindelijke syntaxis die de server gebruikt. IMHO, dit is een vergissing die moet worden gecorrigeerd. Maar voor nu werkt het eenvoudigweg gebruik van de reeks en is dit de meest haalbare optie met de beste prestaties en schaalbaarheid.

Aanvulling - MongoDB 3.6 en hoger

Hoewel het hier getoonde patroon redelijk geoptimaliseerd . is vanwege de manier waarop de andere fasen in de $lookup . worden gerold , het heeft één tekortkoming in die zin dat de "LEFT JOIN" die normaal inherent is aan zowel $lookup en de acties van populate() wordt teniet gedaan door de "optimale" gebruik van $unwind hier die geen lege arrays behoudt. U kunt de preserveNullAndEmptyArrays . toevoegen optie, maar dit negeert de "geoptimaliseerde" hierboven beschreven en laat in wezen alle drie de fasen intact die normaal gesproken zouden worden gecombineerd in de optimalisatie.

MongoDB 3.6 breidt uit met een "meer expressieve" vorm van $lookup het toestaan ​​van een "sub-pijplijn"-expressie. Wat niet alleen voldoet aan het doel om de "LEFT JOIN" te behouden, maar nog steeds een optimale zoekopdracht mogelijk maakt om de geretourneerde resultaten te verminderen en met een sterk vereenvoudigde syntaxis:

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

De $expr gebruikt om de gedeclareerde "lokale" waarde te matchen met de "buitenlandse" waarde is eigenlijk wat MongoDB nu "intern" doet met de originele $lookup syntaxis. Door in deze vorm uit te drukken, kunnen we de initiële $match . aanpassen uitdrukking binnen de "sub-pijplijn" zelf.

Als echte "aggregatiepijplijn" kun je in feite alles doen wat je kunt doen met een aggregatiepijplijn binnen deze "subpijplijn"-expressie, inclusief het "nesten" van de niveaus van $lookup naar andere gerelateerde collecties.

Verder gebruik valt een beetje buiten het bestek van wat de vraag hier stelt, maar zelfs met betrekking tot "geneste populatie" dan is het nieuwe gebruikspatroon van $lookup staat toe dat dit vrijwel hetzelfde is, en een "lot" krachtiger in zijn volledige gebruik.

Werkvoorbeeld

Het volgende geeft een voorbeeld met behulp van een statische methode op het model. Zodra die statische methode is geïmplementeerd, wordt de aanroep eenvoudig:

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

Of verbeteren om een ​​beetje moderner te zijn wordt zelfs:

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

Waardoor het erg lijkt op .populate() in structuur, maar het doet in plaats daarvan de join op de server. Voor de volledigheid:het gebruik hier werpt de geretourneerde gegevens terug naar de documentinstanties van mangoesten volgens zowel de bovenliggende als de onderliggende gevallen.

Het is vrij triviaal en gemakkelijk aan te passen of gewoon te gebruiken zoals in de meeste gevallen.

NB Het gebruik van asynchrone hier is alleen voor de beknoptheid van het uitvoeren van het bijgevoegde voorbeeld. De daadwerkelijke implementatie is vrij van deze afhankelijkheid.

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

Of een beetje moderner voor Node 8.x en hoger met async/await en geen extra afhankelijkheden:

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

En vanaf MongoDB 3.6 en hoger, zelfs zonder de $unwind en $group gebouw:

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

})()


  1. Een structuur voor aggregatie genereren

  2. Rails, Sidekiq - Redis NOAUTH

  3. pymongo.errors.CursorNotFound:cursor id '...' niet geldig op server

  4. golang + redis concurrency scheduler prestatieprobleem