2017-10-30 3 views
1

Heute renne ich in eine Situation Ich muss eine mongoDB Sammlung zu vertica (SQL Datenbank) synchronisieren, wo meine Objektschlüssel die Spalten der Tabelle in SQL sein werden. Ich benutze mongoDB aggregation framework, um das gewünschte Ergebnisdokument zuerst abzufragen, zu manipulieren und zu projizieren, und dann synchronisiere ich es mit vertica.Abwickeln mehrerer Dokumentarrays in neue Dokumente

Das Schema I aggregieren möchte wie folgt aussieht:

{ 
    userId: 123 
    firstProperty: { 
    firstArray: ['x','y','z'], 
    anotherAttr: 'abc' 
    }, 
    anotherProperty: { 
    secondArray: ['a','b','c'], 
    anotherAttr: 'def' 
    } 
} 

Da Array-Werte nicht mit anderen Arrays Wert verwandt sind, was ich brauche, ist, dass jeder Wert von verschachtelten Array, in einem separaten Ergebnisdokument sein wird . dass Denn ich das folgende Aggregationsrohr verwenden:

db.collection('myCollection').aggregate([ 
      { 
       $match: { 
        $or: [ 
         {'firstProperty.firstArray.1': {$exists: true}}, 
         {'secondProperty.secondArray.1': {$exists: true}} 
        ] 
       } 
      }, 
      { 
       $project: { 
        userId: 1, 
        firstProperty: 1, 
        secondProperty: 1 
       } 
      }, { 
       $unwind: {path:'$firstProperty.firstAray'} 
      }, { 
       $unwind: {path:'$secondProperty.secondArray'}, 
      }, { 
       $project: { 
        userId: 1, 
        firstProperty: '$firstProperty.firstArray', 
        firstPropertyAttr: '$firstProperty.anotherAttr', 
        secondProperty: '$secondProperty.secondArray', 
        seondPropertyAttr: '$secondProperty.anotherAttr' 
       } 
      }, { 
       $out: 'another_collection' 
      } 
     ]) 

Was erwarte ich, ist das folgende Ergebnis:

{ 
    userId: 'x1', 
    firstProperty: 'x', 
    firstPropertyAttr: 'a' 
} 
{ 
    userId: 'x1', 
    firstProperty: 'y', 
    firstPropertyAttr: 'a' 
} 
{ 
    userId: 'x1', 
    firstProperty: 'z', 
    firstPropertyAttr: 'a' 
} 
{ 
    userId: 'x1', 
    secondProperty: 'a', 
    firstPropertyAttr: 'b' 
} 
{ 
    userId: 'x1', 
    secondProperty: 'b', 
    firstPropertyAttr: 'b' 
} 
{ 
    userId: 'x1', 
    secondProperty: 'c', 
    firstPropertyAttr: 'b' 
} 

Stattdessen bekomme ich so etwas wie die:

{ 
    userId: 'x1', 
    firstProperty: 'x', 
    firstPropertyAttr: 'b' 
    secondProperty: 'a', 
    secondPropertyAttr: 'b' 
} 
{ 
    userId: 'x1', 
    firstProperty: 'y', 
    firstPropertyAttr: 'b' 
    secondProperty: 'b', 
    secondPropertyAttr: 'b' 
} 
{ 
    userId: 'x1', 
    firstProperty: 'z', 
    firstPropertyAttr: 'b' 
    secondProperty: 'c', 
    secondPropertyAttr: 'b' 
} 

Was ich genau bin fehlt, und wie kann ich es beheben?

+0

Ihr erwartetes Ergebnis enthält Dokumente mit unterschiedlichen Feldnamen. Einige von ihnen haben firstProperty, während andere secondProperty haben. Wollen Sie das wirklich erreichen? – mickl

+0

Beachten Sie, dass sowohl Ihre erwartete Ausgabe als auch Ihre aktuelle Ausgabe nicht mit den angegebenen Daten übereinstimmen oder mit der bereitgestellten Aggregationspipeline korrelieren. Aber die Pipeline macht tatsächlich deutlich, woher die Daten "kommen" sollen. Ihre erwartete Ausgabe sieht insbesondere so aus, als ob Sie die Benennung verloren hätten, aber ich glaube, ich folge dem allgemeinen Kern von "Objektschlüsseln als Namen von Spalten", die Sie präsentieren wollten. –

+0

Nun, ja. Ich brauche die endgültigen Eigenschaften unterschiedlich benannt, da sie verschiedene Quellen von Datenfeldern darstellen. Aber genau das mache ich in der finalen Projektion in der Pipeline. – yudarik

Antwort

0

Dies ist eigentlich ein viel "lockerer" Problem, als Sie vielleicht denken, und es läuft alles auf "benannte Schlüssel", die in der Regel ein echtes Problem sind und Ihre Daten sollten nicht "Datenpunkte" verwenden "in der Benennung solcher Schlüssel.

Das andere offensichtliche Problem in Ihrem Versuch wird ein "kartesisches Produkt" genannt. Dies ist, wo Sie $unwind ein Array und dann $unwind ein anderes, was dazu führt, dass die Elemente von der "ersten" $unwind für jeden Wert in der "zweiten" wiederholt wird.

Adressierung dieses zweite Problem, ist der grundlegende Ansatz, "die Arrays zu kombinieren", damit Sie nur $unwind aus einer Hand. Dies ist bei allen anderen Ansätzen ziemlich üblich.

Die Ansätze unterscheiden sich in der verfügbaren MongoDB-Version und der allgemeinen Anwendbarkeit. Also lassen Sie sich durch sie Schritt:

Entfernen Sie die genannten Tasten

Der einfachste Ansatz hier ist einfach nicht genannt Schlüssel in der Ausgabe zu erwarten, und statt sie markieren als "name" ihre Quelle in der endgültigen Ausgabe zu identifizieren. Alles, was wir tun wollen, ist, jeden "erwarteten" Schlüssel innerhalb der Konstruktion eines anfänglichen "kombinierten" Arrays anzugeben, und dann einfach $filter, dass für alle null Werte, die aus benannten Pfaden resultieren, die im vorliegenden Dokument nicht existieren.

db.getCollection('myCollection').aggregate([ 
    { "$match": { 
    "$or": [ 
     { "firstProperty.firstArray.0": { "$exists": true } }, 
     { "anotherProperty.secondArray.0": { "$exists": true } } 
    ] 
    }}, 
    { "$project": { 
    "_id": 0, 
    "userId": 1, 
    "combined": { 
     "$filter": { 
     "input": [ 
      { 
      "name": { "$literal": "first" }, 
      "array": "$firstProperty.firstArray", 
      "attr": "$firstProperty.anotherAttr" 
      }, 
      { 
      "name": { "$literal": "another" }, 
      "array": "$anotherProperty.secondArray", 
      "attr": "$anotherProperty.anotherAttr" 
      } 
     ], 
     "cond": { 
      "$ne": ["$$this.array", null ] 
     } 
     } 
    } 
    }}, 
    { "$unwind": "$combined" }, 
    { "$unwind": "$combined.array" }, 
    { "$project": { 
    "userId": 1, 
    "name": "$combined.name", 
    "value": "$combined.array", 
    "attr": "$combined.attr" 
    }} 
]) 

Aus den Daten in Ihrer Frage enthalten würde dies produzieren:

/* 1 */ 
{ 
    "userId" : 123.0, 
    "name" : "first", 
    "value" : "x", 
    "attr" : "abc" 
} 

/* 2 */ 
{ 
    "userId" : 123.0, 
    "name" : "first", 
    "value" : "y", 
    "attr" : "abc" 
} 

/* 3 */ 
{ 
    "userId" : 123.0, 
    "name" : "first", 
    "value" : "z", 
    "attr" : "abc" 
} 

/* 4 */ 
{ 
    "userId" : 123.0, 
    "name" : "another", 
    "value" : "a", 
    "attr" : "def" 
} 

/* 5 */ 
{ 
    "userId" : 123.0, 
    "name" : "another", 
    "value" : "b", 
    "attr" : "def" 
} 

/* 6 */ 
{ 
    "userId" : 123.0, 
    "name" : "another", 
    "value" : "c", 
    "attr" : "def" 
} 

Merge Objekte - Benötigt MongoDB 3.4.4 Minimum

tatsächlich zu verwenden "genannt Schlüssel" Wir brauchen die $objectToArray und $arrayToObject Operatoren, die erst seit MongoDB 3.4.4 verfügbar waren.Mit diesen und die $replaceRoot Pipeline-Stufe wir einfach auf die gewünschte Ausgabe verarbeiten kann, ohne explizit in jeder Phase die Schlüssel zur Ausgabe Benennung:

db.getCollection('myCollection').aggregate([ 
    { "$match": { 
    "$or": [ 
     { "firstProperty.firstArray.0": { "$exists": true } }, 
     { "anotherProperty.secondArray.0": { "$exists": true } } 
    ] 
    }}, 
    { "$project": { 
    "_id": 0, 
    "userId": 1, 
    "data": { 
     "$reduce": { 
     "input": { 
      "$map": { 
      "input": { 
       "$filter": { 
       "input": { "$objectToArray": "$$ROOT" }, 
       "cond": { "$not": { "$in": [ "$$this.k", ["_id", "userId"] ] } } 
       } 
      }, 
      "as": "d", 
      "in": { 
       "$let": { 
       "vars": { 
        "inner": { 
        "$map": { 
         "input": { "$objectToArray": "$$d.v" }, 
         "as": "i", 
         "in": { 
         "k": { 
          "$cond": { 
          "if": { "$ne": [{ "$indexOfCP": ["$$i.k", "Array"] }, -1] }, 
          "then": "$$d.k", 
          "else": { "$concat": ["$$d.k", "Attr"] } 
          } 
         }, 
         "v": "$$i.v" 
         } 
        } 
        } 
       }, 
       "in": { 
        "$map": { 
        "input": { 
         "$arrayElemAt": [ 
         "$$inner.v", 
         { "$indexOfArray": ["$$inner.k", "$$d.k"] } 
         ] 
        }, 
        "as": "v", 
        "in": { 
         "$arrayToObject": [[ 
         { "k": "$$d.k", "v": "$$v" }, 
         { 
          "k": { "$concat": ["$$d.k", "Attr"] }, 
          "v": { 
          "$arrayElemAt": [ 
           "$$inner.v", 
           { "$indexOfArray": ["$$inner.k", { "$concat": ["$$d.k", "Attr"] }] } 
          ] 
          } 
         } 
         ]] 
        } 
        } 
       } 
       } 
      } 
      } 
     }, 
     "initialValue": [], 
     "in": { "$concatArrays": [ "$$value", "$$this" ] } 
     } 
    } 
    }}, 
    { "$unwind": "$data" }, 
    { "$replaceRoot": { 
    "newRoot": { 
     "$arrayToObject": { 
     "$concatArrays": [ 
      [{ "k": "userId", "v": "$userId" }], 
      { "$objectToArray": "$data" } 
     ] 
     } 
    } 
    }} 
]) 

die sich von Umwandlung der „Schlüssel“ in ein Array ziemlich monströs wird, dann die "Unterschlüssel" in ein Array und Mapping der Werte von diesen inneren Arrays auf das Schlüsselpaar in der Ausgabe.

Die Schlüsselteile $objectToArray zu sein, ist im wesentlichen erforderlich, um Ihre „nested Schlüssel“ -Strukturen in Arrays von "k" und "v" repräsentiert der „Name“ des Schlüssels und der „Wert“ zu „transformieren“. Dies wird zweimal aufgerufen, einmal für die "äußeren" Teile des Dokuments und ohne die "konstanten" Felder wie "_id" und "userId" in einer solchen Array-Struktur. Dann wird der zweite Aufruf für jedes dieser "Array" -Elemente verarbeitet, um diese "inneren Schlüssel" zu einem ähnlichen "Array" zu machen.

Die Zuordnung erfolgt dann unter Verwendung von $indexOfCP, um herauszufinden, welcher "innere Schlüssel" derjenige für den Wert ist und welcher der "Attr" ist. Die Schlüssel werden dann hier in den "äußeren" Schlüsselwert umbenannt, auf den wir zugreifen können, denn das ist ein "v" mit freundlicher Genehmigung von $objectToArray.

dann für den „inneren Wert“, die ein „Array“ ist, möchten wir $map jeden Eintrag in ein kombiniertes „array“, die im Wesentlichen die Form hat:

[ 
    { "k": "firstProperty", "v": "x" }, 
    { "k": "firstPropertyAttr", "v": "abc" } 
] 

Dies geschieht für jeden „inneren Anordnung "Element, für das $arrayToObject den Prozess umkehrt und jede "k" und "v" in" Schlüssel "und" Wert "eines Objekts jeweils umwandelt.

Da der Ausgang immer noch ein „Array von Arrays“ des „inneren Schlüssel“ an diesem Punkt ist, wickelt ich die $reduce, dass die Ausgabe und wendet $concatArrays während jeden Verarbeitungselement, um "data" in ein einziges Array zu „verbinden“.

Alles, was übrig bleibt, ist einfach $unwind das Array von jedem Quelldokument erzeugt, und wenden Sie dann $replaceRoot, das ist der Teil, der tatsächlich "verschiedene Schlüsselnamen" an der "Wurzel" jedes Dokuments Ausgabe erlaubt.

Die „merging“ werden hier durch Zuführen einen Array von Objekt der gleichen "k" und "v" Konstruktion für "userId" notiert gemacht, und „concatentating“, dass mit den $objectToArray den "data" transformiert. Natürlich wird dieses "neue Array" dann über $arrayToObject ein letztes Mal in ein Objekt umgewandelt, das als Ausdruck das "Objekt" -Argument zu "newRoot" bildet.

Sie tun so etwas, wenn es eine große Anzahl von "benannten Schlüsseln" gibt, die Sie nicht explizit benennen können. Und es tatsächlich gibt Ihnen das gewünschte Ergebnis:

/* 1 */ 
{ 
    "userId" : 123.0, 
    "firstProperty" : "x", 
    "firstPropertyAttr" : "abc" 
} 

/* 2 */ 
{ 
    "userId" : 123.0, 
    "firstProperty" : "y", 
    "firstPropertyAttr" : "abc" 
} 

/* 3 */ 
{ 
    "userId" : 123.0, 
    "firstProperty" : "z", 
    "firstPropertyAttr" : "abc" 
} 

/* 4 */ 
{ 
    "userId" : 123.0, 
    "anotherProperty" : "a", 
    "anotherPropertyAttr" : "def" 
} 

/* 5 */ 
{ 
    "userId" : 123.0, 
    "anotherProperty" : "b", 
    "anotherPropertyAttr" : "def" 
} 

/* 6 */ 
{ 
    "userId" : 123.0, 
    "anotherProperty" : "c", 
    "anotherPropertyAttr" : "def" 
} 

Named Keys ohne MongoDB 3.4.4 oder länger

Ohne die Unterstützung des Bedieners, wie in der obigen Auflistung gezeigt, es ist einfach nicht möglich, dass die Aggregation Rahmen Dokumente mit verschiedenen Schlüsselnamen ausgeben.

So wäre es nicht möglich ist, den „Server“ zu beauftragen, dies über $out zu tun, können Sie natürlich auch einfach die Cursor durchlaufen und eine neue Kollektion

var ops = []; 

db.getCollection('myCollection').find().forEach(d => { 
    ops = ops.concat(Object.keys(d).filter(k => ['_id','userId'].indexOf(k) === -1) 
    .map(k => 
     d[k][Object.keys(d[k]).find(ki => /Array$/.test(ki))] 
     .map(v => ({ 
      [k]: v, 
      [`${k}Attr`]: d[k][Object.keys(d[k]).find(ki => /Attr$/.test(ki))] 
     })) 
    ) 
    .reduce((acc,curr) => acc.concat(curr),[]) 
    .map(o => Object.assign({ userId: d.userId },o)) 
); 

    if (ops.length >= 1000) { 
    db.getCollection("another_collection").insertMany(ops); 
    ops = []; 
    } 

}) 

if (ops.length > 0) { 
    db.getCollection("another_collection").insertMany(ops); 
    ops = []; 
} 

gleiche Art von Sache schreiben, wie in getan wird, die frühere Aggregation, sondern nur "extern". Es produziert im Wesentlichen und Array von Dokumenten für jedes Dokument der „inneren“ Arrays passend, etwa so:

[ 
    { 
     "userId" : 123.0, 
     "firstProperty" : "x", 
     "firstPropertyAttr" : "abc" 
    }, 
    { 
     "userId" : 123.0, 
     "firstProperty" : "y", 
     "firstPropertyAttr" : "abc" 
    }, 
    { 
     "userId" : 123.0, 
     "firstProperty" : "z", 
     "firstPropertyAttr" : "abc" 
    }, 
    { 
     "userId" : 123.0, 
     "anotherProperty" : "a", 
     "anotherPropertyAttr" : "def" 
    }, 
    { 
     "userId" : 123.0, 
     "anotherProperty" : "b", 
     "anotherPropertyAttr" : "def" 
    }, 
    { 
     "userId" : 123.0, 
     "anotherProperty" : "c", 
     "anotherPropertyAttr" : "def" 
    } 
] 

Dieser „Im Cache“ in ein großes Array erhalten, die, wenn sie, dass eine Länge von 1000 oder mehr erreicht, wird schließlich geschrieben zur neuen Kollektion via .insertMany(). Natürlich erfordert dies eine "Hin- und Her-Kommunikation" mit dem Server, aber die Aufgabe wird auf die effizienteste Weise erledigt, wenn Sie nicht über die Funktionen verfügen, die für die vorherige Aggregation verfügbar sind.

Fazit

Der hier Gesamt Punkt ist, dass, wenn Sie tatsächlich eine MongoDB haben, die es unterstützt, dann mit Ihnen gehen zu bekommen Dokumente nicht „verschiedenen Schlüsselnamen“ in der Ausgabe, allein aus der Aggregation Pipeline.

Also, wenn Sie diese Unterstützung nicht haben, gehen Sie entweder mit der ersten Option und verwenden Sie dann $out verwerfen Namen benannt haben. Oder Sie tun den letzten Ansatz und manipulieren einfach die Cursor-Ergebnisse und schreiben Sie zurück in die neue Sammlung.

+0

Die erste Lösung, die Sie vorgeschlagen haben ("Entfernen Sie die benannten Schlüssel"), macht genau das, was ich brauche. Meine Daten sehen in Wirklichkeit etwas anders aus, aber das Beispiel, das ich gab, vereinfacht es. Vielen Dank. – yudarik

Verwandte Themen