MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Zapytanie po wypełnieniu w Mongoose

Z nowoczesną MongoDB większą niż 3.2 możesz użyć $lookup jako alternatywa dla .populate() w większości przypadków. Ma to również tę zaletę, że faktycznie wykonuje łączenie „na serwerze” w przeciwieństwie do tego, co .populate() robi, co w rzeczywistości jest "wieloma zapytaniami" do "emulowania" dołączyć.

Więc .populate() jest nie tak naprawdę „join” w sensie tego, jak robi to relacyjna baza danych. $lookup z drugiej strony operator faktycznie wykonuje pracę na serwerze i jest mniej więcej analogiczny do "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"
  }
)

Uwaga .collection.name tutaj w rzeczywistości zwraca się do „ciągu”, który jest rzeczywistą nazwą kolekcji MongoDB przypisaną do modelu. Ponieważ mangusta domyślnie „pluralizuje” nazwy kolekcji i $lookup potrzebuje rzeczywistej nazwy kolekcji MongoDB jako argumentu (ponieważ jest to operacja serwera), to jest to poręczna sztuczka do użycia w kodzie mongoose, w przeciwieństwie do "twardego kodowania" nazwy kolekcji bezpośrednio.

Chociaż moglibyśmy również użyć $filter na tablicach do usuwania niechcianych elementów, jest to w rzeczywistości najbardziej wydajna forma ze względu na Optymalizację potoku agregacji dla specjalnego warunku jako $lookup po którym następuje oba $unwind i $match stan.

W rzeczywistości powoduje to połączenie trzech etapów rurociągu w jeden:

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

Jest to wysoce optymalne, ponieważ właściwa operacja „najpierw filtruje kolekcję do dołączenia”, a następnie zwraca wyniki i „odwija” tablicę. Stosowane są obie metody, więc wyniki nie przekraczają limitu BSON wynoszącego 16 MB, co jest ograniczeniem, którego klient nie ma.

Jedynym problemem jest to, że pod pewnymi względami wydaje się to „nie intuicyjne”, szczególnie gdy chcesz uzyskać wyniki w postaci tablicy, ale właśnie to jest właśnie to, co $group jest tutaj, ponieważ rekonstruuje oryginalną formę dokumentu.

Szkoda również, że w tej chwili po prostu nie możemy napisać $lookup w tej samej ostatecznej składni, której używa serwer. IMHO, to przeoczenie, które należy poprawić. Ale na razie wystarczy użyć sekwencji i jest to najbardziej opłacalna opcja z najlepszą wydajnością i skalowalnością.

Uzupełnienie — MongoDB 3.6 i nowsze

Chociaż pokazany tutaj wzorzec jest dość zoptymalizowany ze względu na sposób, w jaki inne etapy trafiają do $lookup , ma jedną wadę, ponieważ "LEFT JOIN", która jest zwykle nieodłączna dla obu $lookup i działania populate() jest zanegowane przez "optymalne" użycie $unwind tutaj co nie zachowuje pustych tablic. Możesz dodać preserveNullAndEmptyArrays opcja, ale to neguje opcję „zoptymalizowaną” opisanej powyżej kolejności i zasadniczo pozostawia nienaruszone wszystkie trzy etapy, które normalnie byłyby połączone w optymalizacji.

MongoDB 3.6 rozwija się z "bardziej wyrazistym" forma $lookup zezwalając na wyrażenie „podpotok”. Który nie tylko spełnia cel, jakim jest zachowanie „LEFT JOIN”, ale nadal umożliwia optymalne zapytanie w celu zmniejszenia zwracanych wyników i przy znacznie uproszczonej składni:

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

$expr używane w celu dopasowania zadeklarowanej wartości „lokalnej” do wartości „obcej” jest w rzeczywistości tym, co MongoDB robi teraz „wewnętrznie” z oryginalnym $lookup składnia. Wyrażając w tej formie możemy dostosować początkowy $match wyrażenie wewnątrz „podpotoku” sami.

W rzeczywistości, jako prawdziwy „potok agregacji”, możesz zrobić prawie wszystko, co możesz zrobić z potoku agregacji w tym wyrażeniu „podpotoku”, w tym „zagnieżdżanie” poziomów $lookup do innych powiązanych kolekcji.

Dalsze użycie jest nieco poza zakresem tego, o co pyta to pytanie, ale w odniesieniu nawet do „zagnieżdżonej populacji” nowy wzorzec użycia $lookup pozwala na to, by było tak samo i "dużo" bardziej wydajny w pełnym wykorzystaniu.

Przykład pracy

Poniżej przedstawiono przykład użycia metody statycznej na modelu. Po zaimplementowaniu tej statycznej metody wywołanie staje się po prostu:

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

Lub ulepszenie, aby być nieco bardziej nowoczesnym, staje się nawet:

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

Uczynienie go bardzo podobnym do .populate() w strukturze, ale zamiast tego wykonuje łączenie na serwerze. Dla kompletności, użycie tutaj rzutuje zwrócone dane z powrotem na instancje dokumentu mangusty w zależności od przypadku nadrzędnego i podrzędnego.

Jest to dość trywialne i łatwe w adaptacji lub po prostu w użyciu, tak jak w większości typowych przypadków.

Uwaga Użycie async jest tutaj tylko dla zwięzłości uruchomienia załączonego przykładu. Rzeczywista implementacja jest wolna od tej zależności.

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

Lub trochę bardziej nowoczesny dla Node 8.x i nowszych z async/await i bez dodatkowych zależności:

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

I od MongoDB 3.6 i nowszych, nawet bez $unwind i $group budynek:

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. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. MongoDB:policz liczbę elementów w tablicy

  2. Czy w zapytaniu MongoDB można używać ścisłych dat JSON $dates?

  3. ScaleGrid ogłasza usługi hostingowe MongoDB w Kanadzie

  4. Automatycznie generowane pole dla MongoDB przy użyciu Spring Boot

  5. mongodb $w limicie