2016-03-02 14 views
9

Come si scrivono i risolutori di query in GraphQL che si comportano bene con un database relazionale?Qual è il modo idiomatico e performante per risolvere gli oggetti correlati?

Utilizzando lo schema di esempio da this tutorial, supponiamo di disporre di un semplice database con users e stories. Gli utenti possono creare più storie, ma le storie hanno solo un utente come autore (per semplicità).

Quando si esegue una query per un utente, si potrebbe anche voler ottenere un elenco di tutte le storie create da tale utente. Una possibile definizione di una query GraphQL per gestire tale (rubato dal tutorial sopra linkato):

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

Ciò funzionerà come previsto; se interrogassi un utente specifico, sarei in grado di ottenere anche le storie di quell'utente se necessario. Tuttavia, questo non ha prestazioni ideali. Richiede due viaggi al database, quando una singola query con un JOIN sarebbe stata sufficiente. Il problema si amplifica se interrogo più utenti: ogni utente aggiuntivo comporterà una query di database aggiuntiva. Il problema peggiora in modo esponenziale, più approfondisco le mie relazioni oggettuali.

Questo problema è stato risolto? Esiste un modo per scrivere un risolutore di query che non genererà query SQL inefficienti?

risposta

8

Esistono due approcci a questo tipo di problema.

Un approccio, utilizzato da Facebook, consiste nell'accodare richieste in un tick e combinarle prima dell'invio. In questo modo invece di fare una richiesta per ogni utente, puoi fare una richiesta per recuperare informazioni su diversi utenti. Dan Schafer ha scritto un good comment explaining this approach. Facebook ha rilasciato Dataloader, che è un'implementazione di esempio di questa tecnica.

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

Anche se questo non risolve a SQL efficiente, ancora potrebbe essere abbastanza buono per molti casi d'uso e farà correre più veloce roba. È anche un buon approccio per i database non relazionali che non consentono i JOIN.

Un altro approccio consiste nell'utilizzare le informazioni sui campi richiesti nella funzione di risoluzione per utilizzare JOIN quando è rilevante. Risolvi contesto ha il campo fieldASTs che ha analizzato AST della parte di query correntemente risolta. Guardando attraverso i figli di quel AST (selectionSet), possiamo prevedere se abbiamo bisogno di un join. Un esempio molto semplificato e clunky:

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

Si noti che questo potrebbe avere problemi con, ad esempio, frammenti. Tuttavia, si può anche gestirli, è solo questione di ispezionare la selezione in modo più dettagliato.

C'è attualmente un PR nel repository graphql-js, che consentirà di scrivere una logica più complessa per l'ottimizzazione delle query, fornendo un "piano di risoluzione" nel contesto.

+0

Ah, ho notato che il parametro 'fieldASTs' prima, ma ha molto più senso ora che vedo un caso d'uso concreto. Grazie! – ean5533

Problemi correlati