Pro rychlou poznámku je třeba změnit "value"
pole uvnitř "hodnoty"
být číselný, protože v současnosti je to řetězec. Ale k odpovědi:
Pokud máte přístup k $reduce
z MongoDB 3.4, pak můžete ve skutečnosti udělat něco takového:
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" }
}
}
}
}}
])
Pokud máte MongoDB 3.6, můžete to trochu vyčistit pomocí $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" } }
]
}
}
}
}}
])
Ale je to víceméně to samé, kromě toho, že ponecháme další data
Když se vrátíte o něco zpět, pak můžete vždy $unwind
"města"
hromadit:
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" }
}
}
}
}}
])
Všechny vrátí (téměř) to samé:
{
"_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
}
]
}
První dva formuláře jsou samozřejmě nejoptimálnější, protože prostě vždy pracují „v rámci“ stejného dokumentu.
Operátoři jako $reduce
povolit výrazy "akumulace" na polích, takže je zde můžeme použít k udržení "redukovaného" pole, které testujeme na jedinečné "_id"
hodnotu pomocí $indexOfArray
abyste zjistili, zda již existuje nahromaděná položka, která odpovídá. Výsledek -1
znamená, že tam není.
Abychom vytvořili "redukované pole", vezmeme "initialValue"
z []
jako prázdné pole a poté do něj přidat pomocí $concatArrays
. O celém tomto procesu rozhoduje "ternární" $cond
operátor, který bere v úvahu "if"
podmínkou a "pak"
buď „připojí“ výstup $filter
na aktuální $$value
pro vyloučení aktuálního indexu _id
vstup, samozřejmě s dalším "polem" představujícím singulární objekt.
Pro tento „objekt“ opět používáme $indexOfArray
skutečně získat odpovídající index, protože víme, že položka "je tam", a použít jej k extrahování aktuálního "navštíveného"
hodnotu z tohoto záznamu přes $arrayElemAt
a $add
za účelem zvýšení.
V "else"
v případě jednoduše přidáme "pole" jako "objekt", který má pouze výchozí "visited"
hodnotu 1
. Použití obou těchto případů efektivně shromažďuje jedinečné hodnoty v poli pro výstup.
Ve druhé verzi jsme pouze $unwind
pole a použijte postupné $group
fáze, aby bylo možné nejprve „počítat“ s jedinečnými vnitřními položkami a poté „znovu zkonstruovat pole“ do podobné podoby.
Pomocí $unwind
vypadá mnohem jednodušeji, ale protože to, co ve skutečnosti dělá, je vzít kopii dokumentu pro každou položku pole, pak to ve skutečnosti zvyšuje značnou režii na zpracování. V moderních verzích obecně existují operátory polí, což znamená, že je nemusíte používat, pokud není vaším záměrem „hromadit se napříč dokumenty“. Pokud tedy skutečně potřebujete $group
na hodnotě klíče z "uvnitř" pole, pak je to místo, kde jej skutečně potřebujete použít.
Pokud jde o "proměnné"
pak můžeme jednoduše použít $filter
znovu zde, abyste získali odpovídající "Rozpočet"
vstup. Děláme to jako vstup do $map
operátor, který umožňuje "přetvoření" obsahu pole. Chceme to hlavně proto, abyste si mohli vzít obsah "hodnot"
(jakmile to celé uděláte numericky) a použijte $avg
operátor, který dodává onen „zápis cesty k poli“ přímo do hodnot pole, protože ve skutečnosti může vrátit výsledek z takového vstupu.
To obecně umožňuje prohlídku v podstatě VŠECH hlavních „operátorů pole“ pro agregační kanál (kromě „setových“ operátorů), vše v rámci jedné fáze potrubí.
Nikdy také nezapomeňte, že téměř vždy chcete $match
s běžnými operátory dotazů
jako „úplně první fázi“ jakéhokoli agregačního kanálu, abyste si vybrali pouze dokumenty, které potřebujete. Ideálně pomocí indexu.
Alternativy
Náhradníci pracují s dokumenty v klientském kódu. Obecně by se to nedoporučovalo, protože všechny výše uvedené metody ukazují, že ve skutečnosti „redukují“ obsah vrácený ze serveru, jak je obecně smyslem „agregací serverů“.
Vzhledem k povaze „založené na dokumentu“ „může“ být možné, že větší sady výsledků mohou pomocí $unwind
trvat podstatně déle a zpracování klienta by mohlo být možností, ale považoval bych to za mnohem pravděpodobnější
Níže je uveden seznam, který ukazuje použití transformace na proud kurzoru, protože výsledky jsou vráceny stejným způsobem. Existují tři demonstrované verze transformace, které ukazují "přesně" stejnou logiku jako výše, implementace s lodash
metody pro akumulaci a "přirozenou" akumulaci na mapě
implementace:
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()
}
})()