Hvordan kan jeg (i MongoDB) kombinere data fra flere samlinger til én samling?
Kan jeg bruke map-reduce, og i så fall hvordan?
Jeg vil sette stor pris på noen eksempler, siden jeg er nybegynner.
Selv om du ikke kan gjøre dette i sanntid, kan du kjøre map-reduce flere ganger for å slå sammen data ved å bruke alternativet "reduce" out i MongoDB 1.8+ map/reduce (se http://www.mongodb.org/display/DOCS/MapReduce#MapReduce-Outputoptions). Du må ha en nøkkel i begge samlingene som du kan bruke som _id.
La oss for eksempel si at du har en users
-samling og en comments
-samling, og at du vil ha en ny samling som inneholder demografisk brukerinformasjon for hver kommentar.
La oss si at samlingen users
har følgende felt:
Samlingen comments
har følgende felter:
Du kan gjøre dette map/reduce:
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
Nå har du en ny samling kalt users_comments
som inneholder de sammenslåtte dataene, og den kan du nå bruke. Disse reduserte samlingene har alle _id
, som er nøkkelen du sendte ut i map-funksjonene, og alle verdiene er underobjekter inne i nøkkelen value
- verdiene er ikke på øverste nivå i disse reduserte dokumentene.
Dette er et litt enkelt eksempel. Du kan gjenta dette med flere samlinger så mye du vil for å fortsette å bygge opp den reduserte samlingen. Du kan også gjøre oppsummeringer og aggregeringer av data i prosessen. Sannsynligvis vil du definere mer enn én reduce-funksjon etter hvert som logikken for aggregering og bevaring av eksisterende felt blir mer kompleks.
Legg også merke til at det nå finnes ett dokument for hver bruker med alle brukerens kommentarer i en matrise. Hvis vi skulle slå sammen data som har et én-til-én-forhold i stedet for én-til-mange, ville det vært flatt, og du kunne ganske enkelt brukt en reduce-funksjon som denne:
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;
};
Hvis du vil flate ut samlingen users_comments
slik at det er ett dokument per kommentar, kan du i tillegg kjøre dette:
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"});
Denne teknikken bør absolutt ikke utføres på sparket. Den egner seg for en cron-jobb eller lignende som oppdaterer de sammenslåtte dataene med jevne mellomrom. Du vil sannsynligvis ønske å kjøre ensureIndex
på den nye samlingen for å sikre at spørringer du utfører mot den, kjører raskt (husk at dataene dine fremdeles er inne i en value
-nøkkel, så hvis du skulle indeksere comments_with_demographics
på kommentarens created
-tidspunkt, ville det være db.comments_with_demographics.ensureIndex({"value.created": 1});
.
Kodeutdrag. Flere innlegg på stack overflow, inkludert dette.
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();
Det må du gjøre i applikasjonslaget. Hvis du bruker en ORM, kan den bruke annotasjoner (eller noe lignende) for å hente referanser som finnes i andre samlinger. Jeg har bare jobbet med Morphia, og @Reference
-annotasjonen henter den refererte entiteten ved forespørsel, så jeg slipper å gjøre det selv i koden.