Architektur
ArchitekturDataloading-Engine

Dataloading-Engine

Gato GraphQL verwendet serverseitige Komponenten, um das Datenmodell darzustellen (keine Graphen oder Bäume). Schauen wir uns an, wie der Datenladevorgang zur Auflösung der GraphQL-Query abläuft.

Um die Daten zu verarbeiten, müssen wir die Komponenten in Typen umwandeln (<FeaturedDirector> => Director, <Film> => Film, <Actor> => Actor), sie in der Reihenfolge ihres Auftretens in der Komponentenhierarchie ordnen (Director, dann Film, dann Actor) und sie in „Iterationen" behandeln, wobei die Objektdaten für jeden Typ in einer eigenen Iteration abgerufen werden, so:

Behandlung von Typen in Iterationen

Die Dataloading-Engine des Servers muss den folgenden (Pseudo-)Algorithmus implementieren, um die Daten zu laden:

Vorbereitung:

  1. Eine leere Warteschlange anlegen, um die Liste der IDs der Objekte zu speichern, die aus der Datenbank abgerufen werden müssen, geordnet nach Typ (jeder Eintrag lautet: [type => Liste von IDs])
  2. Die ID des hervorgehobenen Regisseur-Objekts abrufen und in der Warteschlange unter dem Typ Director ablegen

Schleife, bis keine Einträge mehr in der Warteschlange sind:

  1. Den ersten Eintrag aus der Warteschlange holen: den Typ und die Liste der IDs (z. B.: Director und [2]), und diesen Eintrag aus der Warteschlange entfernen
  2. Mit dem TypeDataLoader-Objekt des Typs eine einzelne Query gegen die Datenbank ausführen, um alle Objekte dieses Typs mit diesen IDs abzurufen
  3. Falls der Typ relationale Felder hat (z. B.: Typ Director hat das relationale Feld films vom Typ Film), alle IDs dieser Felder aus allen in der aktuellen Iteration abgerufenen Objekten sammeln (z. B.: alle IDs im Feld films aus allen Objekten vom Typ Director) und diese IDs in der Warteschlange unter dem entsprechenden Typ ablegen (z. B.: IDs [3, 8] unter Typ Film).

Am Ende der Iterationen haben wir alle Objektdaten für alle Typen geladen, so:

Behandlung von Typen in Iterationen

Beachte, wie alle IDs für einen Typ gesammelt werden, bis der Typ in der Warteschlange verarbeitet wird. Wenn wir zum Beispiel ein relationales Feld preferredActors zum Typ Director hinzufügen, würden diese IDs in der Warteschlange unter dem Typ Actor abgelegt und zusammen mit den IDs aus dem Feld actors des Typs Film verarbeitet:

Behandlung von Typen in Iterationen

Wenn ein Typ jedoch bereits verarbeitet wurde und danach weitere Daten dieses Typs geladen werden müssen, ist das eine neue Iteration für diesen Typ. Wenn zum Beispiel ein relationales Feld preferredDirector zum Typ Author hinzugefügt wird, wird der Typ Director erneut zur Warteschlange hinzugefügt:

Iteration über einen wiederholten Typ

Nachdem wir alle Objektdaten abgerufen haben, müssen wir sie in die erwartete Antwortform bringen, die die GraphQL-Query widerspiegelt. Wie zu sehen ist, haben die Daten jedoch nicht die erforderliche Baumstruktur. Stattdessen enthalten relationale Felder die IDs des verschachtelten Objekts und emulieren damit die Darstellung von Daten in einer relationalen Datenbank. Daher können die für jeden Typ abgerufenen Daten, analog zu diesem Vergleich, als Tabelle dargestellt werden, so:

Tabelle für Typ Director:

IDnamecountryavatarfilms
2George LucasUSAgeorge-lucas.jpg[3, 8]

Tabelle für Typ Film:

IDtitlethumbnailactors
3The Phantom Menaceepisode-1.jpg[4, 6]
8Attack of the Clonesepisode-2.jpg[6, 7]

Tabelle für Typ Actor:

IDnameavatar
4Ewan McGregormcgregor.jpg
6Nathalie Portmanportman.jpg
7Hayden Christensenchristensen.jpg

Da alle Daten als Tabellen organisiert sind und wir wissen, wie die einzelnen Typen miteinander in Beziehung stehen (d. h. Director referenziert Film über das Feld films, Film referenziert Actor über das Feld actors), kann der GraphQL-Server die Daten problemlos in die erwartete Baumstruktur umwandeln:

Baumartige Antwort

Schließlich gibt der GraphQL-Server den Baum aus, der die Form der erwarteten Antwort hat:

{
  data: {
    featuredDirector: {
      name: "George Lucas",
      country: "USA",
      avatar: "george-lucas.jpg",
      films: [
        {
          title: "Star Wars: Episode I",
          thumbnail: "episode-1.jpg",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "mcgregor.jpg",
            },
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            }
          ]
        },
        {
          title: "Star Wars: Episode II",
          thumbnail: "episode-2.jpg",
          actors: [
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            },
            {
              name: "Hayden Christensen",
              avatar: "christensen.jpg",
            }
          ]
        }
      ]
    }
  }
}

Analyse der Zeitkomplexität der Lösung

Analysieren wir die Big-O-Notation des Datenladealgorithrnus, um zu verstehen, wie die Anzahl der gegen die Datenbank ausgeführten Queries mit der Anzahl der Eingaben wächst, und sicherzustellen, dass diese Lösung performant ist.

Die Dataloading-Engine lädt Daten in Iterationen, die jedem Typ entsprechen. Zu Beginn einer Iteration liegt bereits die vollständige Liste aller IDs aller abzurufenden Objekte vor, sodass eine einzige Query ausgeführt werden kann, um alle Daten der entsprechenden Objekte abzurufen. Daraus folgt, dass die Anzahl der Datenbankabfragen linear mit der Anzahl der in der Query beteiligten Typen wächst. Mit anderen Worten, die Zeitkomplexität ist O(n), wobei n die Anzahl der Typen in der Query ist (wenn ein Typ jedoch mehr als einmal iteriert wird, muss er mehr als einmal zu n gezählt werden).

Diese Lösung ist sehr performant, deutlich besser als die exponentielle Komplexität, die beim Umgang mit Graphen zu erwarten ist, oder die logarithmische Komplexität beim Umgang mit Bäumen.

Implementierter PHP-Code

Der Datenladevorgang findet in der Funktion getComponentData der Klasse Engine im Paket Component Model statt.