5/5/2016著者Steven Luscher
フロントエンドのWeb開発者やモバイル開発者から、同じ願望を何度も耳にします。彼らはRelayやGraphQLのような新技術が提供する開発効率の向上を熱望していますが、既存のREST APIには長年の蓄積があります。移行のメリットを明確に示すデータがないため、GraphQLインフラストラクチャへの追加投資を正当化するのが難しいと感じています。
この記事では、JavaScriptのみを使用して、既存のREST APIの上にGraphQLエンドポイントを迅速かつ低コストで構築できる方法の概要を説明します。このブログ投稿の作成中に、バックエンド開発者が被害を受けることはありません。
既存のREST APIへの呼び出しをラップするGraphQLスキーマ(データの宇宙を記述する型システム)を作成します。このスキーマは、すべてクライアント側でGraphQLクエリを受信して解決します。このアーキテクチャには、本質的なパフォーマンス上の欠陥がありますが、実装が迅速で、サーバー側の変更は必要ありません。
/people/
エンドポイントを通じて、Person
モデルとその関連する友達を参照できるREST APIがあるとします。
人々とその属性(first_name
やemail
など)と、友情を通して他の個人との関連性をモデル化するGraphQLスキーマを構築します。
まず、スキーマ構築ツールが必要です。
npm install --save graphql
最終的には、クエリを解決するために使用できるGraphQLSchema
をエクスポートしたいと考えています。
import { GraphQLSchema } from "graphql"
export default new GraphQLSchema({ query: QueryType,})
すべてのGraphQLスキーマの根本には、その定義を私たちが提供し、ここではQueryType
として指定したquery
という型があります。では、QueryType
を構築しましょう。これは、取得したい可能性のあるすべてのものを定義する型です。
REST APIのすべての機能を複製するために、QueryType
に2つのフィールドを公開しましょう。
allPeople
フィールド(/people/
に相当)person(id: String)
フィールド(/people/{ID}/
に相当)各フィールドは、戻り値の型、オプションの引数の定義、およびクエリ対象のデータを取得するJavaScriptメソッドで構成されます。
import { GraphQLList, GraphQLObjectType, GraphQLString,} from 'graphql';
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: root => // Fetch the index of people from the REST API, }, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => // Fetch the person with ID `args.id`, }, }),});
現時点ではレゾルバーをスケッチとして残し、PersonType
の定義に進みましょう。
import { GraphQLList, GraphQLObjectType, GraphQLString,} from 'graphql';
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: {type: GraphQLString}, username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => // Fetch the friends with the URLs `person.friends`, }, }),});
PersonType
の定義について2点注意が必要です。まず、email
、id
、またはusername
のレゾルバーを提供していません。デフォルトのレゾルバーは、フィールドと同じ名前のプロパティをperson
オブジェクトから単純にアクセスします。これは、プロパティ名がフィールド名と一致しない場合(例:フィールドfirstName
はREST APIからのレスポンスオブジェクトのfirst_name
プロパティと一致しません)、またはプロパティにアクセスしても目的のオブジェクトが得られない場合(例:friends
フィールドには、URLのリストではなく、personオブジェクトのリストが必要です)を除いて、どこでも機能します。
では、REST APIから人を取得するレゾルバーを作成しましょう。ネットワークから読み込む必要があるため、すぐに値を返すことはできません。幸いなことに、resolve()
は値または値のPromise
のいずれかを返すことができます。これを利用して、最終的にPersonType
に準拠するJavaScriptオブジェクトに解決されるREST APIへのHTTPリクエストを送信します。
そして、これがスキーマの最初の完全な試みです。
import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const PersonType = new GraphQLObjectType({ /* ... */ fields: () => ({ /* ... */ friends: { type: new GraphQLList(PersonType), resolve: person => person.friends.map(fetchPersonByURL), }, }),});
const QueryType = new GraphQLObjectType({ /* ... */ fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => fetchPersonByURL(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
通常、RelayはHTTPを介してサーバーにGraphQLクエリを送信します。作成したスキーマを使用してクエリを解決するために、@taionのカスタムrelay-local-schema
ネットワークレイヤーを挿入できます。このコードは、Relayアプリをマウントする前に実行されることが保証されている場所に配置してください。
npm install --save relay-local-schema
import RelayLocalSchema from 'relay-local-schema'; import schema from './schema'; Relay.injectNetworkLayer( new RelayLocalSchema.NetworkLayer({ schema }));
以上です。Relayはすべてのクエリをカスタムのクライアント常駐スキーマに送信し、そのスキーマは既存のREST APIへの呼び出しを行うことでクエリを解決します。
上記で説明したクライアントサイドREST APIラッパーを使用すると、Relayバージョンのアプリ(またはアプリの一部)を迅速に試すことができます。
ただし、前述のように、このアーキテクチャには、GraphQLが依然として非常にネットワーク負荷の高い基盤となるREST APIを呼び出している方法が原因で、本質的なパフォーマンス上の欠陥があります。次のステップとして、スキーマをクライアントサイドからサーバーサイドに移動して、ネットワークの待ち時間を最小限に抑え、レスポンスをキャッシュするための機能を向上させることをお勧めします。
NodeとExpressを使用して、上記のGraphQLラッパーのサーバーサイドバージョンを構築する様子を10分間ご覧ください。
上記で開発したスキーマは、ある時点まではRelayで機能します。それは、Relayに既にダウンロードしたレコードのデータを再取得するように要求する時点です。Relayの再取得サブシステムは、GraphQLスキーマがデータ宇宙内の任意のエンティティをGUIDで取得できる特別なフィールドを公開することに依存しています。これをノードインターフェースと呼びます。
ノードインターフェースを公開するには、クエリルートにnode(id: String!)
フィールドを提供し、すべてのIDをGUID(グローバル一意識別子)に変更する必要があります。
graphql-relay
パッケージには、これを行うためのヘルパー関数がいくつか含まれています。
npm install --save graphql-relay
まず、PersonType
のid
フィールドをGUIDに変更しましょう。これには、graphql-relay
のglobalIdField
ヘルパーを使用します。
import { globalIdField } from "graphql-relay"
const PersonType = new GraphQLObjectType({ name: "Person", description: "Somebody that you used to know", fields: () => ({ id: globalIdField("Person"), /* ... */ }),})
内部的には、globalIdField
は、型名'Person'
とREST APIによって返されたIDをハッシュすることでid
をGraphQLString
に解決するフィールド定義を返します。後でfromGlobalId
を使用して、このフィールドの結果を'Person'
とREST APIのIDに戻すことができます。
graphql-relay
の一連のヘルパーが、ノードフィールドの開発に役立ちます。あなたの仕事は、ヘルパーに2つの関数を提供することです。
import { fromGlobalId, nodeDefinitions } from "graphql-relay"
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId) if (type === "Person") { return fetchPersonByURL(`/people/${id}/`) } }, object => { if (object.hasOwnProperty("username")) { return "Person" } })
上記のオブジェクトから型名へのレゾルバーはエンジニアリングの驚異ではありませんが、アイデアはわかります。
次に、nodeInterface
とnodeField
をスキーマに追加するだけです。完全な例を以下に示します。
import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';import { fromGlobalId, globalIdField, nodeDefinitions,} from 'graphql-relay';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId); if (type === 'Person') { return fetchPersonByURL(`/people/${id}/`); } }, object => { if (object.hasOwnProperty('username')) { return 'Person'; } },);
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: globalIdField('Person'), username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => person.friends.map(fetchPersonByURL), }, }), interfaces: [ nodeInterface ],});
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, node: nodeField, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => fetchPersonByURL(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
次の友達の友達の友達のクエリを考えてみましょう。
query { person(id: "1") { firstName friends { firstName friends { firstName friends { firstName } } } }}
上記で作成したスキーマは、同じデータに対してREST APIへの複数回の往復を生成します。
これは明らかに避けたいことです。少なくとも、これらのリクエストの結果をキャッシュする方法が必要です。
このようなクエリを抑制するために、DataLoaderと呼ばれるライブラリを作成しました。
npm install --save dataloader
特別な注意事項として、ランタイムがネイティブまたはポリフィルされたバージョンのPromise
とMap
を提供していることを確認してください。詳細はDataLoaderサイトをご覧ください。
DataLoader
を作成するには、キーのリストが与えられた場合にオブジェクトのリストを解決できるメソッドを提供します。この例では、キーはREST APIにアクセスするURLです。
const personLoader = new DataLoader(urls => Promise.all(urls.map(fetchPersonByURL)))
このデータローダーがその存続期間中にキーを複数回検出した場合、レスポンスのメモ化(キャッシュ)されたバージョンを返します。
personLoader
のload()
メソッドとloadMany()
メソッドを使用して、URLを一度に1つのURLより多くREST APIをヒットする心配をすることなくロードできます。完全な例を以下に示します。
import DataLoader from 'dataloader';import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';import { fromGlobalId, globalIdField, nodeDefinitions,} from 'graphql-relay';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const personLoader = new DataLoader( urls => Promise.all(urls.map(fetchPersonByURL)));
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const {type, id} = fromGlobalId(globalId); if (type === 'Person') { return personLoader.load(`/people/${id}/`); } }, object => { if (object.hasOwnProperty('username')) { return 'Person'; } },);
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: globalIdField('Person'), username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => personLoader.loadMany(person.friends), }, }), interfaces: [nodeInterface],});
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, node: nodeField, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => personLoader.load(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
これで、病的なクエリは、REST APIへの次の重複除去されたリクエストセットを生成します。
REST APIが既に関連付けを熱心にロードできる設定を提供している可能性があります。おそらく、個人とその直接の友達をすべてロードするには、/people/1/?include_friends
というURLにアクセスする必要があるでしょう。GraphQLスキーマでこれを使用するには、クエリ自体の構造(例:friends
フィールドがクエリの一部であるかどうか)に基づいて解決計画を作成する機能が必要です。
高度な解決戦略に関する現在の考え方に興味のある方は、プルリクエスト#304にご注目ください。
この記事が、機能的なGraphQLエンドポイントとユーザーとの間のいくつかの障壁を取り除き、既存のプロジェクトでGraphQLとRelayを試すきっかけになったことを願っています。