2016-03-02 1 views
9

Wie schreiben Sie Abfrage-Resolver in GraphQL, die sich gut gegen eine relationale Datenbank eignen?Was ist der idiomatische, performante Weg, um verwandte Objekte zu lösen?

Verwenden Sie das Beispielschema von this tutorial, sagen wir, ich habe eine einfache Datenbank mit users und stories. Benutzer können mehrere Stories erstellen, aber Stories haben nur einen Benutzer als Autor (zur Vereinfachung).

Wenn man nach einem Benutzer fragt, möchte man vielleicht auch eine Liste aller Geschichten erhalten, die von diesem Benutzer verfasst wurden. Eine mögliche Definition eine GraphQL Abfrage, damit umzugehen (gestohlen aus der oben verlinkten Tutorial):

const Query = new GraphQLObjectType({ 
    name: 'Query', 
    fields:() => ({ 
    user: { 
     type: User, 
     args: { 
     id: { 
      type: new GraphQLNonNull(GraphQLID) 
     } 
     }, 
     resolve(parent, {id}, {db}) { 
     return db.get(` 
      SELECT * FROM User WHERE id = $id 
      `, {$id: id}); 
     } 
    }, 
    }) 
}); 

const User = new GraphQLObjectType({ 
    name: 'User', 
    fields:() => ({ 
    id: { 
     type: GraphQLID 
    }, 
    name: { 
     type: GraphQLString 
    }, 
    stories: { 
     type: new GraphQLList(Story), 
     resolve(parent, args, {db}) { 
     return db.all(` 
      SELECT * FROM Story WHERE author = $user 
     `, {$user: parent.id}); 
     } 
    } 
    }) 
}); 

Dies wird wie erwartet; Wenn ich einen bestimmten Benutzer abfrage, kann ich bei Bedarf auch die Storys dieses Benutzers abrufen. Dies funktioniert jedoch nicht optimal. Es erfordert zwei Fahrten zur Datenbank, wenn eine einzige Abfrage mit JOIN genügt hätte. Das Problem wird verstärkt, wenn ich mehrere Benutzer abfrage - jeder weitere Benutzer führt zu einer zusätzlichen Datenbankabfrage. Das Problem wird umso schlimmer, je tiefer ich meine Objektbeziehungen durchlaufe.

Wurde dieses Problem gelöst? Gibt es eine Möglichkeit, einen Abfrage-Resolver zu schreiben, der nicht dazu führt, dass ineffiziente SQL-Abfragen generiert werden?

Antwort

8

Es gibt zwei Ansätze für diese Art von Problem.

Ein Ansatz, der von Facebook verwendet wird, besteht darin, Anfragen, die in einem Tick stattfinden, in die Warteschlange zu stellen und sie vor dem Senden zu kombinieren. Auf diese Weise können Sie anstelle einer Anforderung für jeden Benutzer eine Anforderung zum Abrufen von Informationen zu mehreren Benutzern ausführen. Dan Schäfer schrieb eine good comment explaining this approach. Facebook veröffentlicht Dataloader, die eine Beispielimplementierung dieser Technik ist.

// Pass this to graphql-js context 
const storyLoader = new DataLoader((authorIds) => { 
    return db.all(
    `SELECT * FROM Story WHERE author IN (${authorIds.join(',')})` 
).then((rows) => { 
    // Order rows so they match orde of authorIds 
    const result = {}; 
    for (const row of rows) { 
     const existing = result[row.author] || []; 
     existing.push(row); 
     result[row.author] = existing; 
    } 
    const array = []; 
    for (const author of authorIds) { 
     array.push(result[author] || []); 
    } 
    return array; 
    }); 
}); 

// Then use dataloader in your type 
const User = new GraphQLObjectType({ 
    name: 'User', 
    fields:() => ({ 
    id: { 
     type: GraphQLID 
    }, 
    name: { 
     type: GraphQLString 
    }, 
    stories: { 
     type: new GraphQLList(Story), 
     resolve(parent, args, {rootValue: {storyLoader}}) { 
     return storyLoader.load(parent.id); 
     } 
    } 
    }) 
}); 

Während dies zu einem effizienten SQL nicht beheben, könnte es immer noch gut genug sein, um für viele Anwendungsfälle und Sachen schneller laufen. Es ist auch ein guter Ansatz für nicht-relationale Datenbanken, die keine JOINs zulassen.

Ein anderer Ansatz besteht darin, die Informationen über angeforderte Felder in der Auflösungsfunktion zu verwenden, um JOIN zu verwenden, wenn es relevant ist. Resolve context hat fieldASTs Feld, das AST des aktuell aufgelösten Abfrageteils analysiert hat. Wenn wir durch die Kinder dieses AST (selectionSet) schauen, können wir vorhersagen, ob wir einen Join brauchen. Ein sehr vereinfachtes und klobiges Beispiel:

const User = new GraphQLObjectType({ 
    name: 'User', 
    fields:() => ({ 
    id: { 
     type: GraphQLID 
    }, 
    name: { 
     type: GraphQLString 
    }, 
    stories: { 
     type: new GraphQLList(Story), 
     resolve(parent, args, {rootValue: {storyLoader}}) { 
     // if stories were pre-fetched use that 
     if (parent.stories) { 
      return parent.stories; 
     } else { 
      // otherwise request them normally 
      return db.all(` 
      SELECT * FROM Story WHERE author = $user 
     `, {$user: parent.id}); 
     } 
     } 
    } 
    }) 
}); 

const Query = new GraphQLObjectType({ 
    name: 'Query', 
    fields:() => ({ 
    user: { 
     type: User, 
     args: { 
     id: { 
      type: new GraphQLNonNull(GraphQLID) 
     } 
     }, 
     resolve(parent, {id}, {rootValue: {db}, fieldASTs}) { 
     // find names of all child fields 
     const childFields = fieldASTs[0].selectionSet.selections.map(
      (set) => set.name.value 
     ); 
     if (childFields.includes('stories')) { 
      // use join to optimize 
      return db.all(` 
      SELECT * FROM User INNER JOIN Story ON User.id = Story.author WHERE User.id = $id 
      `, {$id: id}).then((rows) => { 
      if (rows.length > 0) { 
       return { 
       id: rows[0].author, 
       name: rows[0].name, 
       stories: rows 
       }; 
      } else { 
       return db.get(` 
       SELECT * FROM User WHERE id = $id 
       `, {$id: id} 
      ); 
      } 
      }); 
     } else { 
      return db.get(` 
      SELECT * FROM User WHERE id = $id 
      `, {$id: id} 
     ); 
     } 
     } 
    }, 
    }) 
}); 

Beachten Sie, dass dies Probleme mit z. B. Fragmenten haben könnte. Man kann aber auch damit umgehen, es geht nur darum, das Auswahlset genauer zu inspizieren.

Im graphql-js-Repository befindet sich derzeit ein PR, der es ermöglicht, komplexere Logik für die Abfrageoptimierung zu schreiben, indem im Kontext ein "Auflösungs-Plan" bereitgestellt wird.

+0

Ah, ich habe das Feld "fieldASts" vorher bemerkt, aber es macht jetzt viel mehr Sinn, wenn ich einen konkreten Anwendungsfall sehe. Vielen Dank! – ean5533