sql >> Database teknologi >  >> NoSQL >> MongoDB

Forespørger efter befolkning i Mongoose

Med en moderne MongoDB større end 3.2 kan du bruge $lookup som en alternativ til .populate() i de fleste tilfælde. Dette har også den fordel, at det faktisk udfører joinforbindelsen "på serveren" i modsætning til hvad .populate() gør, hvilket faktisk er "flere forespørgsler" for at "emulere" a join.

.populate() er ikke virkelig et "join" i betydningen af, hvordan en relationsdatabase gør det. $lookup På den anden side udfører operatøren faktisk arbejdet på serveren og er mere 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"
  }
)

NB .collection.name her evalueres faktisk til "strengen", der er det faktiske navn på MongoDB-samlingen som tildelt modellen. Siden mongoose "pluraliserer" samlingsnavne som standard og $lookup har brug for det faktiske MongoDB-samlingsnavn som et argument (da det er en serveroperation), så er dette et praktisk trick at bruge i mongoose-kode, i modsætning til at "hardkode" samlingsnavnet direkte.

Mens vi også kunne bruge $filter på arrays for at fjerne de uønskede elementer, er dette faktisk den mest effektive form på grund af Aggregation Pipeline Optimization for den særlige tilstand som $lookup efterfulgt af både en $unwind og en $match tilstand.

Dette resulterer faktisk i, at de tre pipeline-faser bliver rullet til én:

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

Dette er yderst optimalt, da den faktiske operation "filtrerer samlingen for at slutte sig til først", så returnerer den resultaterne og "afvikler" arrayet. Begge metoder anvendes, så resultaterne ikke bryder BSON-grænsen på 16 MB, hvilket er en begrænsning, som klienten ikke har.

Det eneste problem er, at det virker "modintuitivt" på nogle måder, især når du vil have resultaterne i et array, men det er hvad $group er til her, da den rekonstruerer til den originale dokumentform.

Det er også uheldigt, at vi på nuværende tidspunkt simpelthen ikke kan skrive $lookup i den samme eventuelle syntaks som serveren bruger. IMHO, dette er en forglemmelse, der skal rettes. Men indtil videre vil blot brugen af ​​sekvensen fungere og er den mest levedygtige mulighed med den bedste ydeevne og skalerbarhed.

Tilføjelse - MongoDB 3.6 og nyere

Selvom mønsteret vist her er temmelig optimeret på grund af hvordan de andre stadier bliver rullet ind i $lookup , den har en fejl, nemlig "LEFT JOIN", som normalt er iboende for både $lookup og handlingerne af populate() er negeret af "optimal" brug af $unwind her som ikke bevarer tomme arrays. Du kan tilføje preserveNullAndEmptyArrays mulighed, men dette negerer "optimeret" sekvensen beskrevet ovenfor og efterlader i det væsentlige alle tre stadier intakte, som normalt ville blive kombineret i optimeringen.

MongoDB 3.6 udvides med en "mere udtryksfuld" form for $lookup tillader et "sub-pipeline" udtryk. Hvilket ikke kun opfylder målet om at bevare "LEFT JOIN", men stadig tillader en optimal forespørgsel for at reducere returnerede resultater og med en meget forenklet syntaks:

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

$expr brugt for at matche den erklærede "lokale" værdi med den "fremmede" værdi er faktisk, hvad MongoDB gør "internt" nu med den originale $lookup syntaks. Ved at udtrykke i denne form kan vi skræddersy den indledende $match udtryk inden for "sub-pipeline" os selv.

Faktisk kan du som en ægte "aggregeringspipeline" gøre stort set alt, hvad du kan gøre med en aggregeringspipeline inden for dette "sub-pipeline"-udtryk, inklusive "nesting" af niveauerne af $lookup til andre relaterede samlinger.

Yderligere brug er lidt uden for rækkevidden af, hvad spørgsmålet her stiller, men i forhold til selv "indlejret befolkning" så er det nye brugsmønster for $lookup tillader dette at være meget det samme, og en "masse" mere kraftfuld i fuld brug.

Arbejdseksempel

Det følgende giver et eksempel med en statisk metode på modellen. Når den statiske metode er implementeret, bliver opkaldet simpelthen:

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

Eller at forbedre til at være en smule mere moderne bliver endda:

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

Gør det meget lig .populate() i struktur, men det gør faktisk joinforbindelsen på serveren i stedet for. For fuldstændighedens skyld kaster brugen her de returnerede data tilbage til mongoose-dokumentforekomster i henhold til både overordnede og underordnede sager.

Det er ret trivielt og nemt at tilpasse eller bare bruge, som det er i de fleste almindelige tilfælde.

NB Brugen af ​​async her er kun for kortheds skyld ved at køre det vedlagte eksempel. Den faktiske implementering er fri for denne afhængighed.

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 lidt mere moderne til Node 8.x og nyere med async/await og ingen yderligere afhængigheder:

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

Og fra MongoDB 3.6 og opefter, selv uden $unwind og $group bygning:

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. Hvad er den rigtige måde at lave en synkron MongoDB-forespørgsel i Node.js?

  2. Sådan samles efter år-måned-dag i en anden tidszone

  3. Hvorfor har Mongoose både skemaer og modeller?

  4. Fejlmeddelelse:MongoError:dårlig godkendelse mislykkedes via URI-streng