mongodb - graphlookup - mongoose aggregate



Matching ObjectId to String für $ graphLookup (1)

Sie verwenden derzeit eine Entwicklungsversion von MongoDB, für die einige Funktionen aktiviert sind, die voraussichtlich mit MongoDB 4.0 als offizielle Version veröffentlicht werden. Beachten Sie, dass einige Funktionen vor der endgültigen Veröffentlichung geändert werden können. Daher sollte der Produktionscode dies berücksichtigen, bevor Sie sich dazu verpflichten.

Warum $ convert hier fehlschlägt

Der wahrscheinlich beste Weg, dies zu erklären, besteht darin, sich das geänderte Beispiel anzusehen, es jedoch durch ObjectId Werte für _id und "strings" für diejenigen unter den Arrays zu ersetzen:

{
  "_id" : ObjectId("5afe5763419503c46544e272"),
   "name" : "cinco",
   "children" : [ { "_id" : "5afe5763419503c46544e273" } ]
},
{
  "_id" : ObjectId("5afe5763419503c46544e273"),
  "name" : "quatro",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e272" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e274"),
  "name" : "seis",
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e275"),
  "name" : "um",
  "children" : [ { "_id" : "5afe5763419503c46544e276" } ]
}
{
  "_id" : ObjectId("5afe5763419503c46544e276"),
  "name" : "dois",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e275" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e277"),
  "name" : "três",
  "ancestors" : [
    { "_id" : "5afe5763419503c46544e273" },
    { "_id" : "5afe5763419503c46544e274" },
    { "_id" : "5afe5763419503c46544e276" }
  ]
},
{ 
  "_id" : ObjectId("5afe5764419503c46544e278"),
  "name" : "sete",
  "children" : [ { "_id" : "5afe5763419503c46544e272" } ]
}

Das sollte eine allgemeine Simulation dessen geben, womit Sie arbeiten wollten.

Sie haben versucht, den _id Wert über $project in einen "String" zu konvertieren, bevor Sie die $graphLookup Stufe $graphLookup . Der Grund, warum dies fehlschlägt, ist, dass Sie, während Sie ein anfängliches $project "innerhalb" dieser Pipeline durchgeführt haben, das Problem ist, dass die Quelle für $graphLookup in der Option "from" immer noch die unveränderte Auflistung ist und Sie daher nicht die korrekten Details zu erhalten nachfolgende "Lookup" -Iterationen.

db.strcoll.aggregate([
  { "$match": { "name": "três" } },
  { "$addFields": {
    "_id": { "$toString": "$_id" }
  }},
  { "$graphLookup": {
    "from": "strcoll",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Stimmt auf der "Suche" daher nicht überein:

{
        "_id" : "5afe5763419503c46544e277",
        "name" : "três",
        "ANCESTORS_FROM_BEGINNING" : [ ]
}

Das Problem "patchen"

Dies ist jedoch das Hauptproblem und kein Fehler von $convert oder der Aliasnamen selbst. Damit dies tatsächlich funktioniert, können wir stattdessen eine "view" erstellen, die sich als Sammlung zur Eingabe darstellt.

Ich mache das umgekehrt und konvertiere die "Strings" über $toObjectId :

db.createView("idview","strcoll",[
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
])

Die Verwendung der "view" bedeutet jedoch, dass die Daten konsistent mit den konvertierten Werten angezeigt werden. Also die folgende Aggregation über die Sicht:

db.idview.aggregate([
  { "$match": { "name": "três" } },
  { "$graphLookup": {
    "from": "idview",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Gibt die erwartete Ausgabe zurück:

{
    "_id" : ObjectId("5afe5763419503c46544e277"),
    "name" : "três",
    "ANCESTORS_FROM_BEGINNING" : [
        ObjectId("5afe5763419503c46544e275"),
        ObjectId("5afe5763419503c46544e273"),
        ObjectId("5afe5763419503c46544e274"),
        ObjectId("5afe5763419503c46544e276"),
        ObjectId("5afe5763419503c46544e272")
    ]
}

Behebung des Problems

Das eigentliche Problem hierbei ist, dass Sie einige Daten haben, die wie ein ObjectId Wert "aussehen" und tatsächlich als ObjectId gültig ObjectId , jedoch als "Zeichenfolge" aufgezeichnet wurden. Das Grundproblem bei allem, was so funktioniert, ist, dass die beiden "Typen" nicht gleich sind und dies zu einer Nichtübereinstimmung der Gleichheit führt, wenn "Verknüpfungen" versucht werden.

Der eigentliche Fix ist also immer noch derselbe wie zuvor. Stattdessen müssen die Daten ObjectId und so ObjectId werden, dass die "Strings" tatsächlich auch ObjectId Werte sind. Diese _id dann mit den _id Schlüsseln _id , auf die sie verweisen sollen, und Sie sparen eine beträchtliche Menge an Speicherplatz, da eine ObjectId viel weniger Platz zum Speichern benötigt als ihre Zeichenfolgendarstellung in hexadezimalen Zeichen.

Mit MongoDB 4.0-Methoden können Sie " tatsächlich " die $toObjectId verwenden, um eine neue Sammlung zu schreiben, genau wie wir die "Ansicht" zuvor erstellt haben:

db.strcoll.aggregate([
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
  { "$out": "fixedcol" }
])

Oder natürlich, wo Sie die gleiche Sammlung "brauchen", bleibt die traditionelle "Schleife und Aktualisierung" dieselbe wie immer:

var updates = [];

db.strcoll.find().forEach(doc => {
  var update = { '$set': {} };

  if ( doc.hasOwnProperty('children') )
    update.$set.children = doc.children.map(e => ({ _id: new ObjectId(e._id) }));
  if ( doc.hasOwnProperty('ancestors') )
    update.$set.ancestors = doc.ancestors.map(e => ({ _id: new ObjectId(e._id) }));

  updates.push({
    "updateOne": {
      "filter": { "_id": doc._id },
      update
    }
  });

  if ( updates.length > 1000 ) {
    db.strcoll.bulkWrite(updates);
    updates = [];
  }

})

if ( updates.length > 0 ) {
  db.strcoll.bulkWrite(updates);
  updates = [];
}

Was eigentlich ein bisschen wie ein Vorschlaghammer ist, da das gesamte Array in einem Durchgang überschrieben wird. Keine gute Idee für eine Produktionsumgebung, aber genug als Demonstration für die Zwecke dieser Übung.

Fazit

Während MongoDB 4.0 diese "Casting" -Funktionen hinzufügt, die in der Tat sehr nützlich sein können, ist ihre eigentliche Absicht nicht wirklich für Fälle wie diesen. Sie sind in der Tat viel nützlicher, wie die "Konvertierung" in eine neue Sammlung unter Verwendung einer Aggregationspipeline zeigt, als die meisten anderen möglichen Verwendungen.

Wir können zwar eine "Ansicht " erstellen, die die Datentypen so umwandelt, dass Dinge wie $lookup und $project funktionieren, bei denen die tatsächlichen Sammlungsdaten unterschiedlich sind, dies ist jedoch nur eine "Hilfe" zum eigentlichen Problem der Daten Typen sollten sich wirklich nicht unterscheiden und sollten tatsächlich permanent konvertiert werden.

Die Verwendung einer "Ansicht" bedeutet tatsächlich, dass die Aggregationspipeline für die Erstellung jedes Mal effektiv ausgeführt werden muss, wenn auf die "Sammlung" (eigentlich eine "Ansicht") zugegriffen wird, wodurch ein echter Overhead entsteht.

Das Vermeiden von Overhead ist in der Regel ein Konstruktionsziel. Daher ist die Korrektur solcher Datenspeicherungsfehler unerlässlich, um eine echte Leistung Ihrer Anwendung zu erzielen, anstatt nur mit "Brute Force" zu arbeiten, die die Leistung nur verlangsamt.

Ein viel sichereres "Konvertierungs" -Skript, das auf jedes Array-Element "passende" Aktualisierungen anwendet. Der Code hier erfordert NodeJS v10.x und einen MongoDB-Knotentreiber 3.1.x der neuesten Version:

const { MongoClient, ObjectID: ObjectId } = require('mongodb');
const EJSON = require('mongodb-extended-json');

const uri = 'mongodb://localhost/';

const log = data => console.log(EJSON.stringify(data, undefined, 2));

(async function() {

  try {

    const client = await MongoClient.connect(uri);
    let db = client.db('test');
    let coll = db.collection('strcoll');

    let fields = ["ancestors", "children"];

    let cursor = coll.find({
      $or: fields.map(f => ({ [`${f}._id`]: { "$type": "string" } }))
    }).project(fields.reduce((o,f) => ({ ...o, [f]: 1 }),{}));

    let batch = [];

    for await ( let { _id, ...doc } of cursor ) {

      let $set = {};
      let arrayFilters = [];

      for ( const f of fields ) {
        if ( doc.hasOwnProperty(f) ) {
          $set = { ...$set,
            ...doc[f].reduce((o,{ _id },i) =>
              ({ ...o, [`${f}.$[${f.substr(0,1)}${i}]._id`]: ObjectId(_id) }),
              {})
          };

          arrayFilters = [ ...arrayFilters,
            ...doc[f].map(({ _id },i) =>
              ({ [`${f.substr(0,1)}${i}._id`]: _id }))
          ];
        }
      }

      if (arrayFilters.length > 0)
        batch = [ ...batch,
          { updateOne: { filter: { _id }, update: { $set }, arrayFilters } }
        ];

      if ( batch.length > 1000 ) {
        let result = await coll.bulkWrite(batch);
        batch = [];
      }

    }

    if ( batch.length > 0 ) {
      log({ batch });
      let result = await coll.bulkWrite(batch);
      log({ result });
    }

    await client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

Produziert und führt Massenoperationen wie diese für die sieben Dokumente aus:

{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e272"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e273"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e273"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e273"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e272"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e272"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e274"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e275"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e276"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e275"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e275"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e277"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e273"
        },
        "ancestors.$[a1]._id": {
          "$oid": "5afe5763419503c46544e274"
        },
        "ancestors.$[a2]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e273"
      },
      {
        "a1._id": "5afe5763419503c46544e274"
      },
      {
        "a2._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5764419503c46544e278"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e272"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e272"
      }
    ]
  }
}

Ich versuche, ein $graphLookup wie im $graphLookup gezeigt:

Ziel ist es, bei einem bestimmten Datensatz (dort $match auskommentiert) den vollständigen "Pfad" der Eigenschaft "instantAncestors" abzurufen. Wie Sie sehen können, geschieht es nicht.

Ich habe $convert hier eingeführt, um mit _id aus der Sammlung als string umzugehen. Ich bin der Ansicht, dass es möglich sein könnte, mit _id aus der Liste der immediateAncestors _id von Datensätzen (die ein string ) " _id ".

Also habe ich einen weiteren Test mit anderen Daten durchgeführt (keine ObjectId beteiligt):

db.nodos.insert({"id":5,"name":"cinco","children":[{"id":4}]})
db.nodos.insert({"id":4,"name":"quatro","ancestors":[{"id":5}],"children":[{"id":3}]})
db.nodos.insert({"id":6,"name":"seis","children":[{"id":3}]})
db.nodos.insert({"id":1,"name":"um","children":[{"id":2}]})
db.nodos.insert({"id":2,"name":"dois","ancestors":[{"id":1}],"children":[{"id":3}]})
db.nodos.insert({"id":3,"name":"três","ancestors":[{"id":2},{"id":4},{"id":6}]})
db.nodos.insert({"id":7,"name":"sete","children":[{"id":5}]})

Und die Abfrage:

db.nodos.aggregate( [
  { $match: { "id": 3 } },
  { $graphLookup: {
      from: "nodos",
      startWith: "$ancestors.id",
      connectFromField: "ancestors.id",
      connectToField: "id",
      as: "ANCESTORS_FROM_BEGINNING"
    }
  },
  { $project: {
      "name": 1,
      "id": 1,
      "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING.id"
    }
  }
] )

... der ausgibt, was ich erwartet habe (die fünf Datensätze, die direkt und indirekt mit dem mit der id 3 verbunden sind):

{
    "_id" : ObjectId("5afe270fb4719112b613f1b4"),
    "id" : 3.0,
    "name" : "três",
    "ANCESTORS_FROM_BEGINNING" : [ 
        1.0, 
        4.0, 
        6.0, 
        5.0, 
        2.0
    ]
}

Die Frage ist: Gibt es eine Möglichkeit, das eingangs erwähnte Ziel zu erreichen?

Ich verwende Mongo 3.7.9 (vom offiziellen Docker)

Danke im Voraus!





aggregation-framework