sql >> Databasteknik >  >> NoSQL >> MongoDB

Frågar efter befolkning i Mongoose

Med en modern MongoDB större än 3.2 kan du använda $lookup som ett alternativ till .populate() i de flesta fallen. Detta har också fördelen av att faktiskt göra joinen "på servern" i motsats till vad .populate() gör vilket faktiskt är "multiple queries" för att "emulera" en gå med.

.populate() är inte verkligen en "join" i betydelsen hur en relationsdatabas gör det. $lookup operatör å andra sidan, gör faktiskt jobbet på servern och är mer eller mindre analog med en "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"
  }
)

Obs. .collection.name här utvärderas faktiskt till "strängen" som är det faktiska namnet på MongoDB-samlingen som tilldelas modellen. Eftersom mongoose "pluraliserar" samlingsnamn som standard och $lookup behöver det faktiska MongoDB-samlingsnamnet som ett argument (eftersom det är en serveroperation), så är detta ett praktiskt knep att använda i mongoose-kod, i motsats till att "hårdkoda" samlingsnamnet direkt.

Även om vi också skulle kunna använda $filter på arrayer för att ta bort de oönskade föremålen, är detta faktiskt den mest effektiva formen på grund av aggregation Pipeline Optimization för det speciella tillståndet som $lookup följt av både en $unwind och en $match skick.

Detta resulterar faktiskt i att de tre pipeline-stegen rullas till ett:

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

Detta är mycket optimalt eftersom den faktiska operationen "filtrerar samlingen för att gå med först", sedan returnerar den resultaten och "lindar upp" arrayen. Båda metoderna används så att resultaten inte bryter mot BSON-gränsen på 16MB, vilket är en begränsning som klienten inte har.

Det enda problemet är att det verkar "kontraintuitivt" på vissa sätt, särskilt när du vill ha resultaten i en array, men det är vad $group är för här, eftersom den rekonstruerar till den ursprungliga dokumentformen.

Det är också olyckligt att vi i nuläget helt enkelt inte kan skriva $lookup i samma slutliga syntax som servern använder. IMHO, detta är ett förbiseende som ska korrigeras. Men för närvarande kommer det att fungera helt enkelt att använda sekvensen och är det mest lönsamma alternativet med bästa prestanda och skalbarhet.

Tillägg - MongoDB 3.6 och senare

Även om mönstret som visas här är ganska optimerat på grund av hur de andra stegen rullas in i $lookup , den har ett fel genom att "LEFT JOIN" som normalt är inneboende för både $lookup och åtgärderna för populate() negeras av "optimal" användning av $unwind här som inte bevarar tomma arrayer. Du kan lägga till preserveNullAndEmptyArrays alternativet, men detta förnekar "optimerad" sekvensen som beskrivs ovan och lämnar i huvudsak alla tre stegen intakta som normalt skulle kombineras i optimeringen.

MongoDB 3.6 expanderar med en "mer uttrycksfull" form av $lookup tillåter ett "sub-pipeline"-uttryck. Vilket inte bara uppfyller målet att behålla "LEFT JOIN" utan fortfarande tillåter en optimal fråga för att minska returnerade resultat och med en mycket förenklad syntax:

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

$expr som används för att matcha det deklarerade "lokala" värdet med det "utländska" värdet är faktiskt vad MongoDB gör "internt" nu med den ursprungliga $lookup syntax. Genom att uttrycka i detta formulär kan vi skräddarsy den initiala $match uttryck inom "sub-pipeline" själva.

Faktum är att som en riktig "aggregationspipeline" kan du göra nästan allt du kan göra med en aggregeringspipeline inom detta "sub-pipeline"-uttryck, inklusive "kapsla" nivåerna för $lookup till andra relaterade samlingar.

Ytterligare användning är lite utanför räckvidden för vad frågan här ställer, men i förhållande till även "kapslade populationer" är det nya användningsmönstret för $lookup låter detta vara ungefär detsamma, och en "mängd" kraftfullare i sin fulla användning.

Arbetsexempel

Följande ger ett exempel med en statisk metod på modellen. När den statiska metoden väl är implementerad blir anropet helt enkelt:

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

Eller att förbättra till att vara lite modernare blir till och med:

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

Gör det väldigt likt .populate() i struktur, men det gör faktiskt anslutningen på servern istället. För fullständighetens skull kastar användningen här den returnerade datan tillbaka till mongoose-dokumentinstanser i enlighet med både överordnade och underordnade fall.

Det är ganska trivialt och lätt att anpassa eller bara använda som i de flesta vanliga fall.

OBS Användningen av async här är bara för att köra det bifogade exemplet. Den faktiska implementeringen är fri från detta beroende.

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

Eller lite modernare för Node 8.x och högre med async/await och inga ytterligare beroenden:

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

Och från MongoDB 3.6 och uppåt, även utan $unwind och $group byggnad:

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. MongoDB:hitta och hitta One med kapslad array-filtrering

  2. Att använda sed på variabeln xargs fungerar inte inuti skalexpansion

  3. Hur uppdaterar man värden med pymongo?

  4. Använda Redis Replication på olika maskiner (multi master)