sql >> Database >  >> NoSQL >> MongoDB

Aggregatie Verzamel innerlijke objecten

Even een snelle opmerking:u moet uw "value" . wijzigen veld binnen de "values" om numeriek te zijn, aangezien het momenteel een string is. Maar op naar het antwoord:

Als je toegang hebt tot $reduce van MongoDB 3.4, dan kun je eigenlijk zoiets als dit doen:

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Als je MongoDB 3.6 hebt, kun je dat een beetje opschonen met $mergeObjects :

db.collection.aggregate([
  { "$addFields": {
     "cities": {
       "$reduce": {
         "input": "$cities",
         "initialValue": [],
         "in": {
           "$cond": {
             "if": { "$ne": [{ "$indexOfArray": ["$$value._id", "$$this._id"] }, -1] },
             "then": {
               "$concatArrays": [
                 { "$filter": {
                   "input": "$$value",
                   "as": "v",
                   "cond": { "$ne": [ "$$this._id", "$$v._id" ] }
                 }},
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": {
                     "$add": [
                       { "$arrayElemAt": [
                         "$$value.visited",
                         { "$indexOfArray": [ "$$value._id", "$$this._id" ] }
                       ]},
                       1
                     ]
                   }
                 }]
               ]
             },
             "else": {
               "$concatArrays": [
                 "$$value",
                 [{
                   "_id": "$$this._id",
                   "name": "$$this.name",
                   "visited": 1
                 }]
               ]
             }
           }
         }
       }
     },
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "$mergeObjects": [
             "$$this",
             { "values": { "$avg": "$$this.values.value" } }
           ]
         }
       }
     }
  }}
])

Maar het is min of meer hetzelfde, behalve dat we de additionalData . behouden

Even daarvoor terug, dan kun je altijd $unwind de "cities" verzamelen:

db.collection.aggregate([
  { "$unwind": "$cities" },
  { "$group": {
     "_id": { 
       "_id": "$_id",
       "cities": {
         "_id": "$cities._id",
         "name": "$cities.name"
       }
     },
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "variables": { "$first": "$variables" },
     "visited": { "$sum": 1 }
  }},
  { "$group": {
     "_id": "$_id._id",
     "_class": { "$first": "$class" },
     "name": { "$first": "$name" },
     "startTimestamp": { "$first": "$startTimestamp" },
     "endTimestamp" : { "$first": "$endTimestamp" },
     "source" : { "$first": "$source" },
     "cities": {
       "$push": {
         "_id": "$_id.cities._id",
         "name": "$_id.cities.name",
         "visited": "$visited"
       }
     },
     "variables": { "$first": "$variables" },
  }},
  { "$addFields": {
     "variables": {
       "$map": {
         "input": {
           "$filter": {
             "input": "$variables",
             "cond": { "$eq": ["$$this.name", "Budget"] } 
           }
         },
         "in": {
           "_id": "$$this._id",
           "name": "$$this.name",
           "defaultValue": "$$this.defaultValue",
           "lastValue": "$$this.lastValue",
           "value": { "$avg": "$$this.values.value" }
         }
       }
     }
  }}
])

Ze geven allemaal (bijna) hetzelfde terug:

{
        "_id" : ObjectId("5afc2f06e1da131c9802071e"),
        "_class" : "Traveler",
        "name" : "John Due",
        "startTimestamp" : 1526476550933,
        "endTimestamp" : 1526476554823,
        "source" : "istanbul",
        "cities" : [
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-1122",
                        "name" : "Cairo",
                        "visited" : 1
                },
                {
                        "_id" : "ef8f6b26328f-0663202f94faeaeb-3981",
                        "name" : "Moscow",
                        "visited" : 2
                }
        ],
        "variables" : [
                {
                        "_id" : "c8103687c1c8-97d749e349d785c8-9154",
                        "name" : "Budget",
                        "defaultValue" : "",
                        "lastValue" : "",
                        "value" : 3000
                }
        ]
}

De eerste twee formulieren zijn natuurlijk het meest optimale om te doen, omdat ze gewoon te allen tijde "binnen" hetzelfde document werken.

Operators zoals $reduce "accumulatie"-expressies op arrays toestaan, dus we kunnen het hier gebruiken om een ​​"gereduceerde" array te behouden die we testen op de unieke "_id" waarde met behulp van $indexOfArray om te zien of er al een verzameld item is dat overeenkomt. Een resultaat van -1 betekent dat het er niet is.

Om een ​​"gereduceerde array" te construeren nemen we de "initialValue" van [] als een lege array en voeg deze vervolgens toe via $concatArrays . Al dat proces wordt bepaald via de "ternaire" $cond operator die rekening houdt met de "if" voorwaarde en "then" ofwel "voegt" zich bij de uitvoer van de $filter op de huidige $$value om de huidige index _id uit te sluiten invoer, met natuurlijk een andere "array" die het enkelvoudige object vertegenwoordigt.

Voor dat "object" gebruiken we opnieuw de $indexOfArray om de overeenkomende index daadwerkelijk te krijgen, omdat we weten dat het item "er is", en dat te gebruiken om de huidige "visited" te extraheren waarde van dat item via $arrayElemAt en $add om het te verhogen.

In de "else" in het geval dat we gewoon een "array" toevoegen als een "object" dat alleen een standaard "visited" heeft waarde van 1 . Door beide gevallen te gebruiken, worden op effectieve wijze unieke waarden binnen de array geaccumuleerd om uit te voeren.

In de laatste versie gebruiken we gewoon $unwind de array en gebruik achtereenvolgens $group stadia om eerst te "rekenen" op de unieke interne invoer en vervolgens "de array opnieuw te bouwen" in dezelfde vorm.

$unwind gebruiken ziet er veel eenvoudiger uit, maar aangezien het eigenlijk een kopie van het document maakt voor elke array-invoer, voegt dit in feite een aanzienlijke overhead toe aan de verwerking. In moderne versies zijn er over het algemeen array-operators, wat betekent dat u dit niet hoeft te gebruiken, tenzij het uw bedoeling is om "alle documenten te verzamelen". Dus als het echt nodig is $group op een waarde van een sleutel van "binnen" een array, dan is dat waar je hem echt moet gebruiken.

Wat betreft de "variables" dan kunnen we gewoon de $filter gebruiken nogmaals hier om de overeenkomende "Budget" te krijgen binnenkomst. We doen dit als invoer voor de $map operator die het "hervormen" van de array-inhoud mogelijk maakt. We willen dat vooral zodat je de inhoud van de "values" . kunt nemen ( als je het allemaal numeriek hebt gemaakt ) en gebruik de $avg operator, die die "veldpadnotatie"-vorm rechtstreeks aan de arraywaarden levert, omdat het in feite een resultaat van een dergelijke invoer kan retourneren.

Dat maakt over het algemeen de rondleiding langs vrijwel ALLE belangrijke "array-operators" voor de aggregatiepijplijn (exclusief de "set"-operators) allemaal binnen één pijplijnfase.

Vergeet ook nooit dat je bijna altijd wilt $match met gewone Query-operators als de "allereerste fase" van elke aggregatiepijplijn om alleen de documenten te selecteren die u nodig hebt. Idealiter met behulp van een index.

Alternatieven

Alternatieven werken door de documenten in klantcode. Het zou over het algemeen niet worden aanbevolen, aangezien alle bovenstaande methoden laten zien dat ze de inhoud die door de server wordt geretourneerd, feitelijk "verminderen", zoals over het algemeen het punt is van "serveraggregaties".

Het "kan" mogelijk zijn vanwege de "documentgebaseerde" aard dat grotere resultatensets aanzienlijk meer tijd in beslag kunnen nemen met behulp van $unwind en klantverwerking zou een optie kunnen zijn, maar ik zou het veel waarschijnlijker achten

Hieronder vindt u een lijst die laat zien hoe u een transformatie op de cursorstroom toepast, aangezien de resultaten hetzelfde doen. Er zijn drie gedemonstreerde versies van de transformatie, die "exact" dezelfde logica laten zien als hierboven, een implementatie met lodash methoden voor accumulatie en een "natuurlijke" accumulatie op de Map implementatie:

const { MongoClient } = require('mongodb');
const { chain } = require('lodash');

const uri = 'mongodb://localhost:27017';
const opts = { useNewUrlParser: true };

const log = data => console.log(JSON.stringify(data, undefined, 2));

const transform = ({ cities, variables, ...d }) => ({
  ...d,
  cities: cities.reduce((o,{ _id, name }) =>
    (o.map(i => i._id).indexOf(_id) != -1)
      ? [
          ...o.filter(i => i._id != _id),
          { _id, name, visited: o.find(e => e._id === _id).visited + 1 }
        ]
      : [ ...o, { _id, name, visited: 1 } ]
  , []).sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))
});

const alternate = ({ cities, variables, ...d }) => ({
  ...d,
  cities: chain(cities)
    .groupBy("_id")
    .toPairs()
    .map(([k,v]) =>
      ({
        ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
        visited: v.length
      })
    )
    .sort((a,b) => b.visited - a.visited)
    .value(),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

const natural = ({ cities, variables, ...d }) => ({
  ...d,
  cities: [
    ...cities
      .reduce((o,{ _id, name }) => o.set(_id,
        [ ...(o.has(_id) ? o.get(_id) : []), { _id, name } ]), new Map())
      .entries()
  ]
  .map(([k,v]) =>
    ({
      ...v.reduce((o,{ _id, name }) => ({ ...o, _id, name }),{}),
      visited: v.length
    })
  )
  .sort((a,b) => b.visited - a.visited),
  variables: variables.filter(v => v.name === "Budget")
    .map(({ values, additionalData, ...v }) => ({
      ...v,
      values: (values != undefined)
        ? values.reduce((o,e) => o + e.value, 0) / values.length
        : 0
    }))

});

(async function() {

  try {

    const client = await MongoClient.connect(uri, opts);

    let db = client.db('test');
    let coll = db.collection('junk');

    let cursor = coll.find().map(natural);

    while (await cursor.hasNext()) {
      let doc = await cursor.next();
      log(doc);
    }

    client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()



  1. Meteor JS:Client krijgt geen gegevens van Mongo DB

  2. Hoe een matrixveld in document samen te voegen in Mongo-aggregatie

  3. Waarom vertelt express me dat mijn standaardweergave-engine niet is gedefinieerd?

  4. Opnieuw verbinden ECONNREFUSED 127.0.0.1:6379