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