¿Cómo puedo (en MongoDB) combinar datos de varias colecciones en una sola?
¿Puedo utilizar map-reduce y, si es así, cómo?
Agradecería mucho algún ejemplo ya que soy novato.
Aunque no se puede hacer esto en tiempo real, se puede ejecutar map-reduce varias veces para fusionar los datos utilizando la opción "reduce" out en map/reduce de MongoDB 1.8+ (ver http://www.mongodb.org/display/DOCS/MapReduce#MapReduce-Outputoptions). Necesita tener alguna clave en ambas colecciones que pueda utilizar como _id.
Por ejemplo, digamos que tienes una colección users
y una colección comments
y quieres tener una nueva colección que tenga información demográfica de los usuarios para cada comentario.
Digamos que la colección users
tiene los siguientes campos:
Y la colección comentarios
tiene los siguientes campos:
Se haría este mapa/reducción:
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
En este punto, tendrás una nueva colección llamada users_comments
que contiene los datos fusionados y que ahora puedes utilizar. Todas estas colecciones reducidas tienen _id
que es la clave que estabas emitiendo en tus funciones de mapa y luego todos los valores son un subobjeto dentro de la clave value
- los valores no están en el nivel superior de estos documentos reducidos.
Este es un ejemplo algo simple. Se puede repetir esto con más colecciones tanto como se quiera para seguir construyendo la colección reducida. También podría hacer resúmenes y agregaciones de datos en el proceso. Es probable que defina más de una función de reducción a medida que la lógica para agregar y preservar los campos existentes se vuelva más compleja.
También observará que ahora hay un documento para cada usuario con todos los comentarios de ese usuario en un array. Si estuviéramos fusionando datos que tienen una relación de uno a uno en lugar de uno a muchos, sería plana y se podría utilizar simplemente una función de reducción como esta:
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;
};
Si quieres aplanar la colección users_comments
para que sea un documento por comentario, ejecuta adicionalmente esto:
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"});
Esta técnica definitivamente no debe realizarse sobre la marcha. Es adecuada para un trabajo cron o algo así que actualice los datos fusionados periódicamente. Probablemente querrás ejecutar ensureIndex
en la nueva colección para asegurarte de que las consultas que realices contra ella se ejecuten rápidamente (ten en cuenta que tus datos siguen estando dentro de una clave value
, así que si fueras a indexar comments_with_demographics
en la hora de creación del comentario, sería db.comments_with_demographics.ensureIndex({"value.created": 1});
.
Fragmento de código. Cortesía-Múltiples posts en stack overflow incluyendo este.
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();
Eso lo tienes que hacer en tu capa de aplicación. Si utilizas un ORM, podría utilizar anotaciones (o algo similar) para obtener las referencias que existen en otras colecciones. Sólo he trabajado con Morphia, y la anotación @Reference
obtiene la entidad referenciada cuando se consulta, por lo que puedo evitar hacerlo yo mismo en el código.