MongoDB: combina i dati da più raccolte in una .. come?

Come posso (in MongoDB) combinare i dati di più raccolte in un’unica raccolta?

Posso usare map-reduce e se sì allora come?

Gradirei molto l’esempio dato che sono un novizio.

Sebbene non sia ansible farlo in tempo reale, è ansible eseguire la mappa-ridurre più volte per unire i dati utilizzando l’opzione “riduci” in MongoDB 1.8+ map / reduce (consultare http://www.mongodb.org/ display / DOCS / MapReduce # MapReduce-Outputoptions ). È necessario disporre di una chiave in entrambe le raccolte che è ansible utilizzare come _id.

Ad esempio, supponiamo tu abbia una raccolta di users e una raccolta di comments e desideri avere una nuova raccolta che contiene alcune informazioni demografiche dell’utente per ogni commento.

Supponiamo che la raccolta users contenga i seguenti campi:

  • _ID
  • nome di battesimo
  • cognome
  • nazione
  • Genere
  • età

E poi la raccolta dei comments ha i seguenti campi:

  • _ID
  • ID utente
  • commento
  • creato

Faresti questa mappa / riduci:

 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 

A questo punto, avrai una nuova raccolta chiamata users_comments che contiene i dati uniti e ora puoi usarla. Queste collezioni ridotte hanno tutte _id che è la chiave che stavi emettendo nelle funzioni della mappa e quindi tutti i valori sono un object secondario all’interno della chiave value – i valori non sono al livello più alto di questi documenti ridotti.

Questo è un esempio un po ‘semplice. È ansible ripetere questo con più collezioni tanto quanto si desidera continuare a build la raccolta ridotta. Potresti anche fare riepiloghi e aggregazioni di dati nel processo. Probabilmente si definirebbe più di una funzione di riduzione poiché la logica per l’aggregazione e la conservazione dei campi esistenti diventa più complessa.

Noterai inoltre che esiste ora un documento per ogni utente con tutti i commenti di quell’utente in un array. Se stessimo fondendo dati che hanno una relazione uno-a-uno piuttosto che uno-a-molti, sarebbe piatta e potresti semplicemente usare una funzione di riduzione come questa:

 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; }; 

Se vuoi appiattire la raccolta users_comments modo che sia un documento per commento, esegui anche questo:

 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"}); 

Questa tecnica non dovrebbe assolutamente essere eseguita al volo. È adatto per un cron job o qualcosa del genere che aggiorna periodicamente i dati uniti. Probabilmente vorrai eseguire ensureIndex sulla nuova raccolta per assicurarti che le query che esegui ensureIndex eseguite rapidamente (tieni presente che i tuoi dati sono ancora all’interno di una chiave di value , quindi se indichi comments_with_demographics sull’ora di created del commento, sarebbe db.comments_with_demographics.ensureIndex({"value.created": 1});

MongoDB 3.2 ora consente di combinare i dati di più raccolte in uno attraverso la fase di aggregazione della ricerca $ . Come esempio pratico, diciamo che hai dati sui libri suddivisi in due raccolte diverse.

Prima raccolta, chiamata books , con i seguenti dati:

 { "isbn": "978-3-16-148410-0", "title": "Some cool book", "author": "John Doe" } { "isbn": "978-3-16-148999-9", "title": "Another awesome book", "author": "Jane Roe" } 

E la seconda raccolta, denominata books_selling_data , con i seguenti dati:

 { "_id": ObjectId("56e31bcf76cdf52e541d9d26"), "isbn": "978-3-16-148410-0", "copies_sold": 12500 } { "_id": ObjectId("56e31ce076cdf52e541d9d28"), "isbn": "978-3-16-148999-9", "copies_sold": 720050 } { "_id": ObjectId("56e31ce076cdf52e541d9d29"), "isbn": "978-3-16-148999-9", "copies_sold": 1000 } 

Per unire entrambe le raccolte è solo questione di utilizzare $ lookup nel modo seguente:

 db.books.aggregate([{ $lookup: { from: "books_selling_data", localField: "isbn", foreignField: "isbn", as: "copies_sold" } }]) 

Dopo questa aggregazione, la raccolta di books sarà simile alla seguente:

 { "isbn": "978-3-16-148410-0", "title": "Some cool book", "author": "John Doe", "copies_sold": [ { "_id": ObjectId("56e31bcf76cdf52e541d9d26"), "isbn": "978-3-16-148410-0", "copies_sold": 12500 } ] } { "isbn": "978-3-16-148999-9", "title": "Another awesome book", "author": "Jane Roe", "copies_sold": [ { "_id": ObjectId("56e31ce076cdf52e541d9d28"), "isbn": "978-3-16-148999-9", "copies_sold": 720050 }, { "_id": ObjectId("56e31ce076cdf52e541d9d28"), "isbn": "978-3-16-148999-9", "copies_sold": 1000 } ] } 

È importante notare alcune cose:

  1. La raccolta “da”, in questo caso books_selling_data , non può essere ridotta.
  2. Il campo “as” sarà un array, come nell’esempio sopra.
  3. Entrambe le opzioni “localField” e “foreignField” nella fase di ricerca $ verranno considerate null per scopi di corrispondenza se non esistono nelle rispettive raccolte (i documenti di ricerca $ ne hanno un esempio perfetto).

Quindi, per concludere, se vuoi consolidare entrambe le raccolte, avendo, in questo caso, un campo copy_sold flat con le copie vendute totali, dovrai lavorare un po ‘di più, probabilmente usando una collezione intermedia che, quindi, essere $ out per la raccolta finale.

Se non ci sono inserimenti di massa in mongodb, facciamo un loop di tutti gli oggetti in small_collection e li inseriamo uno alla volta nella big_collection :

 db.small_collection.find().forEach(function(obj){ db.big_collection.insert(obj) }); 

Esempio molto semplice con la ricerca $.

 db.getCollection('users').aggregate([ { $lookup: { from: "userinfo", localField: "userId", foreignField: "userId", as: "userInfoData" } }, { $lookup: { from: "userrole", localField: "userId", foreignField: "userId", as: "userRoleData" } }, { $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }}, { $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }} ]) 

Qui è usato

  { $unwind: { path: "$userInfoData", preserveNullAndEmptyArrays: true }}, { $unwind: { path: "$userRoleData", preserveNullAndEmptyArrays: true }} 

Invece di

 { $unwind:"$userRoleData"} { $unwind:"$userRoleData"} 

Poiché {$ unwind: “$ userRoleData”} restituirà risultato vuoto o 0 se nessun record corrispondente viene trovato con $ lookup.

utilizzare più $ di ricerca per più raccolte in aggregazione

query:

 db.getCollection('servicelocations').aggregate([ { $match: { serviceLocationId: { $in: ["36728"] } } }, { $lookup: { from: "orders", localField: "serviceLocationId", foreignField: "serviceLocationId", as: "orders" } }, { $lookup: { from: "timewindowtypes", localField: "timeWindow.timeWindowTypeId", foreignField: "timeWindowTypeId", as: "timeWindow" } }, { $lookup: { from: "servicetimetypes", localField: "serviceTimeTypeId", foreignField: "serviceTimeTypeId", as: "serviceTime" } }, { $unwind: "$orders" }, { $unwind: "$serviceTime" }, { $limit: 14 } ]) 

risultato:

 { "_id" : ObjectId("59c3ac4bb7799c90ebb3279b"), "serviceLocationId" : "36728", "regionId" : 1.0, "zoneId" : "DXBZONE1", "description" : "AL HALLAB REST EMIRATES MALL", "locationPriority" : 1.0, "accountTypeId" : 1.0, "locationType" : "SERVICELOCATION", "location" : { "makani" : "", "lat" : 25.119035, "lng" : 55.198694 }, "deliveryDays" : "MTWRFSU", "timeWindow" : [ { "_id" : ObjectId("59c3b0a3b7799c90ebb32cde"), "timeWindowTypeId" : "1", "Description" : "MORNING", "timeWindow" : { "openTime" : "06:00", "closeTime" : "08:00" }, "accountId" : 1.0 }, { "_id" : ObjectId("59c3b0a3b7799c90ebb32cdf"), "timeWindowTypeId" : "1", "Description" : "MORNING", "timeWindow" : { "openTime" : "09:00", "closeTime" : "10:00" }, "accountId" : 1.0 }, { "_id" : ObjectId("59c3b0a3b7799c90ebb32ce0"), "timeWindowTypeId" : "1", "Description" : "MORNING", "timeWindow" : { "openTime" : "10:30", "closeTime" : "11:30" }, "accountId" : 1.0 } ], "address1" : "", "address2" : "", "phone" : "", "city" : "", "county" : "", "state" : "", "country" : "", "zipcode" : "", "imageUrl" : "", "contact" : { "name" : "", "email" : "" }, "status" : "ACTIVE", "createdBy" : "", "updatedBy" : "", "updateDate" : "", "accountId" : 1.0, "serviceTimeTypeId" : "1", "orders" : [ { "_id" : ObjectId("59c3b291f251c77f15790f92"), "orderId" : "AQ18O1704264", "serviceLocationId" : "36728", "orderNo" : "AQ18O1704264", "orderDate" : "18-Sep-17", "description" : "AQ18O1704264", "serviceType" : "Delivery", "orderSource" : "Import", "takenBy" : "KARIM", "plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"), "plannedDeliveryTime" : "", "actualDeliveryDate" : "", "actualDeliveryTime" : "", "deliveredBy" : "", "size1" : 296.0, "size2" : 3573.355, "size3" : 240.811, "jobPriority" : 1.0, "cancelReason" : "", "cancelDate" : "", "cancelBy" : "", "reasonCode" : "", "reasonText" : "", "status" : "", "lineItems" : [ { "ItemId" : "BNWB020", "size1" : 15.0, "size2" : 78.6, "size3" : 6.0 }, { "ItemId" : "BNWB021", "size1" : 20.0, "size2" : 252.0, "size3" : 11.538 }, { "ItemId" : "BNWB023", "size1" : 15.0, "size2" : 285.0, "size3" : 16.071 }, { "ItemId" : "CPMW112", "size1" : 3.0, "size2" : 25.38, "size3" : 1.731 }, { "ItemId" : "MMGW001", "size1" : 25.0, "size2" : 464.375, "size3" : 46.875 }, { "ItemId" : "MMNB218", "size1" : 50.0, "size2" : 920.0, "size3" : 60.0 }, { "ItemId" : "MMNB219", "size1" : 50.0, "size2" : 630.0, "size3" : 40.0 }, { "ItemId" : "MMNB220", "size1" : 50.0, "size2" : 416.0, "size3" : 28.846 }, { "ItemId" : "MMNB270", "size1" : 50.0, "size2" : 262.0, "size3" : 20.0 }, { "ItemId" : "MMNB302", "size1" : 15.0, "size2" : 195.0, "size3" : 6.0 }, { "ItemId" : "MMNB373", "size1" : 3.0, "size2" : 45.0, "size3" : 3.75 } ], "accountId" : 1.0 }, { "_id" : ObjectId("59c3b291f251c77f15790f9d"), "orderId" : "AQ137O1701240", "serviceLocationId" : "36728", "orderNo" : "AQ137O1701240", "orderDate" : "18-Sep-17", "description" : "AQ137O1701240", "serviceType" : "Delivery", "orderSource" : "Import", "takenBy" : "KARIM", "plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"), "plannedDeliveryTime" : "", "actualDeliveryDate" : "", "actualDeliveryTime" : "", "deliveredBy" : "", "size1" : 28.0, "size2" : 520.11, "size3" : 52.5, "jobPriority" : 1.0, "cancelReason" : "", "cancelDate" : "", "cancelBy" : "", "reasonCode" : "", "reasonText" : "", "status" : "", "lineItems" : [ { "ItemId" : "MMGW001", "size1" : 25.0, "size2" : 464.38, "size3" : 46.875 }, { "ItemId" : "MMGW001-F1", "size1" : 3.0, "size2" : 55.73, "size3" : 5.625 } ], "accountId" : 1.0 }, { "_id" : ObjectId("59c3b291f251c77f15790fd8"), "orderId" : "AQ110O1705036", "serviceLocationId" : "36728", "orderNo" : "AQ110O1705036", "orderDate" : "18-Sep-17", "description" : "AQ110O1705036", "serviceType" : "Delivery", "orderSource" : "Import", "takenBy" : "KARIM", "plannedDeliveryDate" : ISODate("2017-08-26T00:00:00.000Z"), "plannedDeliveryTime" : "", "actualDeliveryDate" : "", "actualDeliveryTime" : "", "deliveredBy" : "", "size1" : 60.0, "size2" : 1046.0, "size3" : 68.0, "jobPriority" : 1.0, "cancelReason" : "", "cancelDate" : "", "cancelBy" : "", "reasonCode" : "", "reasonText" : "", "status" : "", "lineItems" : [ { "ItemId" : "MMNB218", "size1" : 50.0, "size2" : 920.0, "size3" : 60.0 }, { "ItemId" : "MMNB219", "size1" : 10.0, "size2" : 126.0, "size3" : 8.0 } ], "accountId" : 1.0 } ], "serviceTime" : { "_id" : ObjectId("59c3b07cb7799c90ebb32cdc"), "serviceTimeTypeId" : "1", "serviceTimeType" : "nohelper", "description" : "", "fixedTime" : 30.0, "variableTime" : 0.0, "accountId" : 1.0 } } 

Mongorestore ha questa caratteristica di accodare tutto ciò che è già presente nel database, quindi questo comportamento potrebbe essere usato per combinare due raccolte:

  1. collezione mongodump1
  2. collection2.rename (collection1)
  3. mongorestore

Non l’ho ancora provato, ma potrebbe funzionare più velocemente rispetto all’approccio map / reduce.

Snippet di codice. Cortesia: post multipli su overflow dello stack, incluso questo.

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

Sì, puoi: Prendi questa funzione di utilità che ho scritto oggi:

 function shangMergeCol() { tcol= db.getCollection(arguments[0]); for (var i=1; i 

Puoi passare a questa funzione qualsiasi numero di raccolte, la prima sarà quella di destinazione. Tutte le raccolte restanti sono fonti da trasferire a quella di destinazione.

Devi farlo nel tuo livello di applicazione. Se stai utilizzando un ORM, potrebbe utilizzare annotazioni (o qualcosa di simile) per estrarre riferimenti esistenti in altre raccolte. Ho lavorato solo con Morphia e l’annotazione @Reference recupera l’ quadro referenziata durante la query, quindi sono in grado di evitare di farlo nel codice.