GraphQL Advent Calendar 2018の9日目です。
前日はja_rascalさんの記事でした。
GraphQL歴はやっと半年くらいのHiroyuki_OSAKIがお送りします。GraphQLに関しては初記事から10件ちょいくらい書いたでしょうか。
今回は、いろんなAPIをパクって自分のサービスを開発するために、
「コピる」「混ぜる」「いじる」を実現するGraphQLの機能
「リモートスキーマ」「スキーマスティッチング」「スキーマトランスフォーム」を紹介します。
APIをパクって改良する(≒ジャイアニズム?)
他サービスのAPIを(ルールを守って)パクって改良する機会は、これから増えていくと思います。特に、他社APIでなく、すでに自社の中に存在するAPIを再利用して新たなサービスを開発する場合に、よく当てはまります。私自身、会社・私事いずれにおいても再利用、再利用というのが口癖です。
ちょっと視点を変えまして、「ジャイアニズム」という言葉があります。ドラえもんに出てくるジャイアンの名言「おまえのものはおれのもの、おれのものもおれのもの」が表す彼の生き様です。これをAPI開発方法論と解釈すると、「すでに他者によって公開されているAPIをそのまま自サービスとして援用し、さらに自分のオリジナルな要素も追加する」ということだと思います。
上の絵の「ジャイアン」というサービスはこんな感じになっていて、実は中身はパクったものである、というイメージです。
新規サービスのアジャイル開発を推し進めるためには、もしかしたらジャイアニズムを迅速に効率的に実施できるかどうかが鍵となるかも。
肝となるのは「コピる」「混ぜる」「いじる」
APIをパクる。単純なようですが、よく見ると3つの実施するのです。
ジャイアンでいうと | APIでいうと |
---|---|
おまえのものはおれのもの の部分 |
①コピる ②混ぜる |
おれのものもおれのもの の部分 | 完全オリジナル ③いじる |
下の絵を見てください。
今回の主役であるジャイアンというサービスは、いくつかコンテンツ(REST API)を持っています。
「リサイタル」だけは完全オリジナルのコンテンツなので例外として、その他はどうやら他人からパクっていますね。
- 「漫画」は実はのび太のものコピっており、「ゲーム」はスネ夫のものをコピっています。
- のび太とスネ夫の知識を混ぜて一個のサービスにしています。
- 「俺の漫画(大全)」はのび太の「漫画」に好き勝手にいじってしまっています(例:自分の感想を書き加えたり持ち主情報を削除する)
コピる とは
APIはもともとのび太やスネ夫など別のサーバで動いており、それぞれの仕様を持っているわけなので、コピると言っても簡単ではなく、こんなことが必要になります。この辺はREST APIでも、3ScaleやApigeeなどのAPI gatewayを使うと比較的簡単にできます。
- リクエストのフォワード
- API仕様の援用
混ぜる とは
- パクったAPI同士の仕様の合成
いじる とは
- 既存の型に独自のフィールドを追加
- 既存クエリ、型の削除・隠蔽
「コピる」「混ぜる」「いじる」を実現するには
1.と2.はまさに「おまえのものはおれのもの」。
3.は他人由来ではあるものの「おれのものもおれのもの」ですね。
こんなにひどい所業ができてしまうのでしょうか。GraphQLなら簡単にできるのです。
それではこれから皆さんでジャイアンになりましょう。
GraphQLで「コピる」「混ぜる」「いじる」
ここからが本題。「コピる」「混ぜる」「いじる」、これらを実現するのがGraphQLの「リモートスキーマ」「スキーマスティッチング」「スキーマトランスフォーム(+デリゲーション)」です。
基礎知識:GraphQLの「API仕様自動やり取り」
GraphQLではGraphQL Schema Languageという書き方(→ドキュメント)で型定義さえ作れば、サーバが勝手にAPIとドキュメントを自動生成し公開します。
たとえばNobitaというサービスは下記の型定義がしてあります。
"""
Manga API
"""
type Manga {
"漫画の名前"
Name: String
"漫画の巻数"
Volume: Int
"漫画に登場する人物"
Characters: [String]
}
型定義には仕様(名前と型)や説明文を一緒に記述しています。
型定義をApolloなどGraphQLサーバに投入すると、勝手にAPI(リクエスト受付やチェック処理)が生成されます。それと同時に、クライアントとサーバ間でやり取りするAPI仕様ドキュメントも自動生成します(これをIntrospectionといい、__schema
というクエリで所得可能)。
このおかげで、サーバ・クライアント間でやり取りしたり、他のサービスともやり取りできます。
APIをコピる「リモートスキーマ」
さてジャイアンによりAPIがコピられていく様子を見てみます。
のび太のGraphQLサーバは先ほどの自動やり取りの仕組みがあり仕様は公開しています。
その仕様を参照してAPIをコピるには、graphql-tools
のmakeRemoteExecutableSchema
(→ドキュメント)にAPIのURLを指定して、自動的にスキーマを取得して、自サービスで使えるスキーマに変換します。introspectSchema
と一緒に下記のように使われるのが定番です。
import {makeRemoteExecutableSchema, introspectSchema} from 'graphql-tools';
import fetch from 'node-fetch'
import { HttpLink } from 'apollo-link-http'
// リモートスキーマを作る関数
const createRemoteSchema = async (uri: string) => {
const link = new HttpLink({uri, fetch})
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link
});
}
// nobitaのGraphQL APIを使ってリモートスキーマを作っている、つまりパクっている
const nobitaSchema = await createRemoteSchema(
'https://nobita/'
)
動きはこんな感じで、APIドキュメントとして公開されている__schemaを参照してnobitaから仕様を取り出し、それに基づいて実行可能なスキーマをジャイアン内部に生成します。
パクったAPIを公開するGraphQLサーバ起動は以下で完了。
import {graphqlExpress} from 'apollo-server-express'
const app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({schema: nobitaSchema}));
APIを混ぜる「スキーマスティッチング」
さて、のび太とスネ夫から取得した2つのAPIのスキーマを混ぜるのがスキーマスティッチング(Schema stitching)です。
graphql-tools
のmergeSchemas
(→ドキュメント)を使います。
import { mergeSchemas } from 'graphql-tools'
//のび太のスキーマをコピー
const nobitaSchema = await createRemoteSchema('https://nobita/')
//スネ夫のスキーマをコピー
const suneoSchema = await createRemoteSchema('https://suneo/')
//★★ココ★★ ジャイアンのスキーマはのび太のスキーマとスネ夫のスキーマを混ぜて作る
const gianSchema = mergeSchemas({
schemas: [nobitaSchema, suneoSchema],
})
const app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({schema: gianSchema}));
動きはこんな感じです。のび太とスネ夫のスキーマはそれぞれ前項のリモートスキーマで取得し配列に入れます。mergeSchemas
の引数のschemas
にその配列を指定します。すると、自動でスキーマの合成ができます。サービスとしてはちゃんとmanga
とgame
という両方のAPIが動作します。ここから生成されるAPIドキュメントにはもちろんのび太とスネ夫が作ったドキュメントが反映されています。
APIをちょいいじる「スキーマデリゲーション」
コピったAPIに細かいカスタマイズをする場合、例えば「処理はそのままにしてちょっとクエリ名を変えたい」などのときに、同じくgraphql-tools
のSchema Delegation
(→ドキュメント)を使用します。新しいクエリのresolver
の中でdelegateToSchema
使うと、リクエストを提供元サーバURLの指定クエリに転送してくれます。
以下では、もともとmanga
というクエリだったAPIの中身はそのままに、ちゃっかり名前をorenomanga
と変えてしまっています。
//自分オリジナルのクエリ名を作る
const linkSchemaDefs = `
extend type Query {
orenomanga: Manga
}
`
const gianSchema = mergeSchemas({
schemas: [nobitaSchema, suneoSchema, linkSchemaDef], //これを追加
resolvers: {
Query: {
orenomanga: { //オリジナルのクエリ名を指定して、処理を記載
resolve: async (parent: any, args: any, context: any, info: any) => {
return info.mergeInfo.delegateToSchema({ //この辺はresolverの決まり文句
schema: nobitaSchema, //実はこのスキーマからパクっている
operation: 'query',
fieldName: 'manga', //パクるクエリ
args: {where: {name: args.name}}, //
context,
info
})
}
},
これの動きはこう。delegateというのは「任せる」ということで、orenomanga
という新たな名前ではあるものの、基本的にはnobitaSchemaのmangaというAPIに処理を任せたということになります。
APIをがっつりいじる「スキーマトランスフォーム」
コピったAPIをがっつりカスタマイズする場合、例えば「元あったクエリを消しちゃう」などのときに、同じくgraphql-tools
のSchema Transforms
(→ドキュメント)を使用します。
以下では、のび太によって公開されていたmangaというクエリがこっそりジャイアンによって消されていることがわかります。
const transformedGianSchema = transformSchema(gianSchema, [
new FilterRootFields((operation: string, rootField: string) => {
// falseを返すと消される。
// つまり元あったQuery.mangaはこの文がfalseとなり消される。
return 'Query.manga' !== `${operation}.${rootField}`
}
),
])
transformSchemaの第二引数には変換方法オブジェクトが入るのですが、今回はかわいそうなことに、FilterRootFieldsが指定されのび太のmangaは消されてしまいました。
Schema Transformsはまだまだ発展中の感がありますが、いくつか既定のTransformがあり、今回はそのうちのFilterRootFieldsを使っているわけです。
Schema Transformsの名前 | 機能 | 使うかどうか(私の主観) |
---|---|---|
FilterTypes | Typeを抹消 | 使うかも |
RenameTypes | Typeの名前を変更 | 結構使うかも |
TransformRootFields | RootField(Query, Mutation, Subscriptionのどれか)に変更を加える | 難しくて使わない |
FilterRootFields | Fieldを抹消 | 結構使う |
RenameRootFields | Fieldの名前を変更 | 結構使う |
ExtractField | パスを指定したパスに変更 | よく分からない、使わない |
WrapQuery | 何でもできそうな変更処理 | 難しすぎて使えない |
今回できたこと
GraphQLのgraphql-tools
の各機能を使うと、こんなことができました。
実現する機能 |
graphql-tools の機能 |
使うかどうか(私の主観) |
---|---|---|
スキーマのコピー |
makeRemoteExecutableSchema + introspectSchema
|
結構使う |
スキーマの合成 | mergeSchemas |
使える |
スキーマに変更を加える | transformSchema |
使うかも |
まだ紹介していない機能として、Schema Directivesがあります。認証によってフィールドをアクセス不可にしたり、いろいろできるみたいです。
残る課題
APIをパクれるようになると、以下のような課題が出てくることがある。
- N+1問題
- スキーマスティッチングで複数のAPIをパクり合成すると、1回のクエリからパクリ元へのN+1回のリクエストが発生してしまうことがある。解決するにはDataloaderを使ってバッチ化するのがよい。Dataloader自体はGraphQLの共同作者であるFacebookのLee Byronが開発。Dataloaderの考え方はyuku_tさんの5日目の記事に詳しい。
- コピーライト問題
- 問題というわけではないですが、他人のAPIをパクったら、そこに書いてあるコピーライトを読んでちゃんとルールを守りましょうねってことです。例えばcreative commonsのcc-byだったら作者名は記載してあげないといけないわけです。じゃあどこに書くかといったときに、いきなりデータの中に突っ込んだら「あー配列の全要素に作者名が入ってしまってうざいー」みたいなことになるので、メタデータに追記する方法(別記事)を使うといいと思います。
- 分散によるボトルネック検証
- API参照したらなんか遅い。そんな時にはボトルネック検証、となるのですが、さてパクったAPI側なのか自分なのか、パクったAPIのどれなのか、分散してて多少面倒くさいです。そういったときに使いたいのが分散トレーシングなどの技術です。GraphQLと分散トレーシングOpenTracing/Jaegerをくっつけた話はyamitzkyさんの1日目の記事が詳しいです。ちゃんとすべてのAPI提供者がOpenTracing等のなんらかの仕組みに乗る合意ができれば、ボトルネック検証がやりやすくなるのではないかなと思います。ちなみに、サーバ内の処理だけならApolloTracingとかが使えるようです。
まとめ
GraphQLでリモートスキーマとスキーマスティッチングを使うと、他のAPIを簡単にパクれます。スキーマスティッチングをうまく使いこなすと、複数のAPIをパクって、それを合成して新しいサービスを作ることもできます。ジャイアニズムを実践し、新しいサービスをどんどん生み出したいものです。
実際のサンプルソースコード等やり方は別の記事1.Schema Stitching: 複数GraphQL APIをくっつけて新しいAPIを作ろう、2.Schema Stitching: graphql-bindingで簡単にちょっとスキーマをカスタマイズするに書きましたので、ご覧ください。
今回使ったgraphql-tools
は、主にApolloなどのGraphQLクリエイターたちががんがん機能追加・更新していってます。まさに彼らは、のび太、スネ夫のみならずジャイアンをジャイアンたらしめるツールを惜しみなく提供するドラえもんといえるでしょう。
明日はubnt_intrepidさんです。