Wie kann ich (in MongoDB) Daten aus mehreren Sammlungen in einer Sammlung kombinieren?
Kann ich Map-Reduce verwenden und wenn ja, wie?
Ich würde einige Beispiele sehr zu schätzen wissen, da ich ein Anfänger bin.
Obwohl dies nicht in Echtzeit möglich ist, können Sie map-reduce mehrfach ausführen, um die Daten zusammenzuführen, indem Sie die Option "reduce" out in MongoDB 1.8+ map/reduce verwenden (siehe http://www.mongodb.org/display/DOCS/MapReduce#MapReduce-Outputoptions). Sie müssen einen Schlüssel in beiden Sammlungen haben, den Sie als _id verwenden können.
Nehmen wir zum Beispiel an, Sie haben eine "Users"-Sammlung und eine "Comments"-Sammlung und möchten eine neue Sammlung mit demografischen Informationen für jeden Kommentar erstellen.
Sagen wir, die Sammlung "users" hat die folgenden Felder:
Die Sammlung "Kommentare" enthält die folgenden Felder:
Sie würden diese Abbildung/Reduzierung durchführen:
var mapUsers, mapComments, reduce;
db.users_comments.remove();
// setup sample data - wouldn't actually use this in production
db.users.remove();
db.comments.remove();
db.users.save({firstName:"Rich",lastName:"S",gender:"M",country:"CA",age:"18"});
db.users.save({firstName:"Rob",lastName:"M",gender:"M",country:"US",age:"25"});
db.users.save({firstName:"Sarah",lastName:"T",gender:"F",country:"US",age:"13"});
var users = db.users.find();
db.comments.save({userId: users[0]._id, "comment": "Hey, what's up?", created: new ISODate()});
db.comments.save({userId: users[1]._id, "comment": "Not much", created: new ISODate()});
db.comments.save({userId: users[0]._id, "comment": "Cool", created: new ISODate()});
// end sample data setup
mapUsers = function() {
var values = {
country: this.country,
gender: this.gender,
age: this.age
};
emit(this._id, values);
};
mapComments = function() {
var values = {
commentId: this._id,
comment: this.comment,
created: this.created
};
emit(this.userId, values);
};
reduce = function(k, values) {
var result = {}, commentFields = {
"commentId": '',
"comment": '',
"created": ''
};
values.forEach(function(value) {
var field;
if ("comment" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.push(value);
} else if ("comments" in value) {
if (!("comments" in result)) {
result.comments = [];
}
result.comments.push.apply(result.comments, value.comments);
}
for (field in value) {
if (value.hasOwnProperty(field) && !(field in commentFields)) {
result[field] = value[field];
}
}
});
return result;
};
db.users.mapReduce(mapUsers, reduce, {"out": {"reduce": "users_comments"}});
db.comments.mapReduce(mapComments, reduce, {"out": {"reduce": "users_comments"}});
db.users_comments.find().pretty(); // see the resulting collection
An diesem Punkt haben Sie eine neue Sammlung namens "users_comments", die die zusammengeführten Daten enthält und die Sie nun verwenden können. Diese reduzierten Sammlungen haben alle eine _id
, die der Schlüssel ist, den Sie in Ihren Map-Funktionen ausgegeben haben, und alle Werte sind ein Unterobjekt innerhalb des value
-Schlüssels - die Werte befinden sich nicht auf der obersten Ebene dieser reduzierten Dokumente.
Dies ist ein recht einfaches Beispiel. Sie können dies mit weiteren Sammlungen beliebig oft wiederholen, um die reduzierte Sammlung weiter auszubauen. Sie könnten dabei auch Zusammenfassungen und Aggregationen von Daten vornehmen. Wahrscheinlich würden Sie mehr als eine Reduzierungsfunktion definieren, da die Logik für die Aggregation und die Beibehaltung vorhandener Felder immer komplexer wird.
Sie werden auch feststellen, dass es jetzt ein Dokument für jeden Benutzer mit allen Kommentaren dieses Benutzers in einem Array gibt. Würden wir Daten zusammenführen, die eine eins-zu-eins-Beziehung und nicht eine-zu-viele-Beziehung haben, wären sie flach und Sie könnten einfach eine Reduktionsfunktion wie diese verwenden:
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
Wenn Sie die Sammlung "users_comments" so reduzieren wollen, dass sie ein Dokument pro Kommentar enthält, führen Sie zusätzlich Folgendes aus:
var map, reduce;
map = function() {
var debug = function(value) {
var field;
for (field in value) {
print(field + ": " + value[field]);
}
};
debug(this);
var that = this;
if ("comments" in this.value) {
this.value.comments.forEach(function(value) {
emit(value.commentId, {
userId: that._id,
country: that.value.country,
age: that.value.age,
comment: value.comment,
created: value.created,
});
});
}
};
reduce = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
db.users_comments.mapReduce(map, reduce, {"out": "comments_with_demographics"});
Diese Technik sollte auf keinen Fall im laufenden Betrieb ausgeführt werden. Sie eignet sich für einen Cron-Job oder etwas ähnliches, das die zusammengeführten Daten regelmäßig aktualisiert. Sie werden wahrscheinlich ensureIndex
auf die neue Sammlung anwenden wollen, um sicherzustellen, dass Abfragen schnell ausgeführt werden (denken Sie daran, dass Ihre Daten immer noch in einem Wert
-Schlüssel enthalten sind. Wenn Sie also comments_with_demographics
auf den Zeitpunkt der Erstellung des Kommentars indizieren würden, wäre es db.comments_with_demographics.ensureIndex({"value.created": 1});
Codeschnipsel. Courtesy-Multiple Beiträge auf Stack Overflow einschließlich dieser ein.
db.cust.drop();
db.zip.drop();
db.cust.insert({cust_id:1, zip_id: 101});
db.cust.insert({cust_id:2, zip_id: 101});
db.cust.insert({cust_id:3, zip_id: 101});
db.cust.insert({cust_id:4, zip_id: 102});
db.cust.insert({cust_id:5, zip_id: 102});
db.zip.insert({zip_id:101, zip_cd:'AAA'});
db.zip.insert({zip_id:102, zip_cd:'BBB'});
db.zip.insert({zip_id:103, zip_cd:'CCC'});
mapCust = function() {
var values = {
cust_id: this.cust_id
};
emit(this.zip_id, values);
};
mapZip = function() {
var values = {
zip_cd: this.zip_cd
};
emit(this.zip_id, values);
};
reduceCustZip = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
if ("cust_id" in value) {
if (!("cust_ids" in result)) {
result.cust_ids = [];
}
result.cust_ids.push(value);
} else {
for (field in value) {
if (value.hasOwnProperty(field) ) {
result[field] = value[field];
}
};
}
});
return result;
};
db.cust_zip.drop();
db.cust.mapReduce(mapCust, reduceCustZip, {"out": {"reduce": "cust_zip"}});
db.zip.mapReduce(mapZip, reduceCustZip, {"out": {"reduce": "cust_zip"}});
db.cust_zip.find();
mapCZ = function() {
var that = this;
if ("cust_ids" in this.value) {
this.value.cust_ids.forEach(function(value) {
emit(value.cust_id, {
zip_id: that._id,
zip_cd: that.value.zip_cd
});
});
}
};
reduceCZ = function(k, values) {
var result = {};
values.forEach(function(value) {
var field;
for (field in value) {
if (value.hasOwnProperty(field)) {
result[field] = value[field];
}
}
});
return result;
};
db.cust_zip_joined.drop();
db.cust_zip.mapReduce(mapCZ, reduceCZ, {"out": "cust_zip_joined"});
db.cust_zip_joined.find().pretty();
var flattenMRCollection=function(dbName,collectionName) {
var collection=db.getSiblingDB(dbName)[collectionName];
var i=0;
var bulk=collection.initializeUnorderedBulkOp();
collection.find({ value: { $exists: true } }).addOption(16).forEach(function(result) {
print((++i));
//collection.update({_id: result._id},result.value);
bulk.find({_id: result._id}).replaceOne(result.value);
if(i%1000==0)
{
print("Executing bulk...");
bulk.execute();
bulk=collection.initializeUnorderedBulkOp();
}
});
bulk.execute();
};
flattenMRCollection("mydb","cust_zip_joined");
db.cust_zip_joined.find().pretty();
Das müssen Sie in Ihrer Anwendungsschicht tun. Wenn Sie einen ORM verwenden, könnte dieser Annotationen (oder etwas Ähnliches) verwenden, um Referenzen aus anderen Sammlungen zu holen. Ich habe nur mit Morphia gearbeitet, und die @Reference
Annotation holt die referenzierte Entität, wenn sie abgefragt wird, so dass ich es vermeiden kann, es selbst im Code zu tun.