11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GraphQLAdvent Calendar 2018

Day 9

GraphQL/スキーマスティッチングで自由にAPIをパクる ~ジャイアンになろう~

Last updated at Posted at 2018-12-08

GraphQL Advent Calendar 2018の9日目です。
前日はja_rascalさん記事でした。
GraphQL歴はやっと半年くらいのHiroyuki_OSAKIがお送りします。GraphQLに関しては初記事から10件ちょいくらい書いたでしょうか。

今回は、いろんなAPIをパクって自分のサービスを開発するために、
「コピる」「混ぜる」「いじる」を実現するGraphQLの機能
「リモートスキーマ」「スキーマスティッチング」「スキーマトランスフォーム」を紹介します。

APIをパクって改良する(≒ジャイアニズム?)

他サービスのAPIを(ルールを守って)パクって改良する機会は、これから増えていくと思います。特に、他社APIでなく、すでに自社の中に存在するAPIを再利用して新たなサービスを開発する場合に、よく当てはまります。私自身、会社・私事いずれにおいても再利用、再利用というのが口癖です。

ちょっと視点を変えまして、「ジャイアニズム」という言葉があります。ドラえもんに出てくるジャイアンの名言「おまえのものはおれのもの、おれのものもおれのもの」が表す彼の生き様です。これをAPI開発方法論と解釈すると、「すでに他者によって公開されているAPIをそのまま自サービスとして援用し、さらに自分のオリジナルな要素も追加する」ということだと思います。

image.png

上の絵の「ジャイアン」というサービスはこんな感じになっていて、実は中身はパクったものである、というイメージです。

image.png

新規サービスのアジャイル開発を推し進めるためには、もしかしたらジャイアニズムを迅速に効率的に実施できるかどうかが鍵となるかも。

肝となるのは「コピる」「混ぜる」「いじる」

APIをパクる。単純なようですが、よく見ると3つの実施するのです。

ジャイアンでいうと APIでいうと
おまえのものはおれのもの の部分 ①コピる
②混ぜる
おれのものもおれのもの  の部分 完全オリジナル
③いじる

下の絵を見てください。
今回の主役であるジャイアンというサービスは、いくつかコンテンツ(REST API)を持っています。
「リサイタル」だけは完全オリジナルのコンテンツなので例外として、その他はどうやら他人からパクっていますね。

  1. 「漫画」は実はのび太のものコピっており、「ゲーム」はスネ夫のものをコピっています。
  2. のび太とスネ夫の知識を混ぜて一個のサービスにしています。
  3. 「俺の漫画(大全)」はのび太の「漫画」に好き勝手にいじってしまっています(例:自分の感想を書き加えたり持ち主情報を削除する)

image.png

コピる とは

APIはもともとのび太やスネ夫など別のサーバで動いており、それぞれの仕様を持っているわけなので、コピると言っても簡単ではなく、こんなことが必要になります。この辺はREST APIでも、3ScaleやApigeeなどのAPI gatewayを使うと比較的簡単にできます。

  • リクエストのフォワード
  • API仕様の援用

混ぜる とは

  • パクったAPI同士の仕様の合成

いじる とは

  • 既存の型に独自のフィールドを追加
  • 既存クエリ、型の削除・隠蔽

「コピる」「混ぜる」「いじる」を実現するには

1.と2.はまさに「おまえのものはおれのもの」。
3.は他人由来ではあるものの「おれのものもおれのもの」ですね。
こんなにひどい所業ができてしまうのでしょうか。GraphQLなら簡単にできるのです

それではこれから皆さんでジャイアンになりましょう。

GraphQLで「コピる」「混ぜる」「いじる」

ここからが本題。「コピる」「混ぜる」「いじる」、これらを実現するのがGraphQLの「リモートスキーマ」「スキーマスティッチング」「スキーマトランスフォーム(+デリゲーション)」です。

image.png

基礎知識:GraphQLの「API仕様自動やり取り」

GraphQLではGraphQL Schema Languageという書き方(→ドキュメント)で型定義さえ作れば、サーバが勝手にAPIとドキュメントを自動生成し公開します。
たとえばNobitaというサービスは下記の型定義がしてあります。

nobita.graphql
"""
Manga API
"""
type Manga {
    "漫画の名前"
    Name: String
    "漫画の巻数"
    Volume: Int
    "漫画に登場する人物"
    Characters: [String]
}

型定義には仕様(名前と型)や説明文を一緒に記述しています。
型定義をApolloなどGraphQLサーバに投入すると、勝手にAPI(リクエスト受付やチェック処理)が生成されます。それと同時に、クライアントとサーバ間でやり取りするAPI仕様ドキュメントも自動生成します(これをIntrospectionといい、__schemaというクエリで所得可能)。

image.png

このおかげで、サーバ・クライアント間でやり取りしたり、他のサービスともやり取りできます。

APIをコピる「リモートスキーマ」

さてジャイアンによりAPIがコピられていく様子を見てみます。:thinking:
のび太のGraphQLサーバは先ほどの自動やり取りの仕組みがあり仕様は公開しています。
その仕様を参照してAPIをコピるには、graphql-toolsmakeRemoteExecutableSchema(→ドキュメント)にAPIのURLを指定して、自動的にスキーマを取得して、自サービスで使えるスキーマに変換します。introspectSchemaと一緒に下記のように使われるのが定番です。

gian.ts
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から仕様を取り出し、それに基づいて実行可能なスキーマをジャイアン内部に生成します。 :astonished:

image.png

パクったAPIを公開するGraphQLサーバ起動は以下で完了。:stuck_out_tongue_winking_eye:

gian.ts
import {graphqlExpress} from 'apollo-server-express'
const app = express();
app.use('/graphql', bodyParser.json(), graphqlExpress({schema: nobitaSchema}));

APIを混ぜる「スキーマスティッチング」

さて、のび太とスネ夫から取得した2つのAPIのスキーマを混ぜるのがスキーマスティッチング(Schema stitching)です。 :thinking:

graphql-toolsmergeSchemas(→ドキュメント)を使います。

gian2.ts
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にその配列を指定します。すると、自動でスキーマの合成ができます。サービスとしてはちゃんとmangagameという両方のAPIが動作します。ここから生成されるAPIドキュメントにはもちろんのび太とスネ夫が作ったドキュメントが反映されています。:grinning:

image.png

APIをちょいいじる「スキーマデリゲーション」

コピったAPIに細かいカスタマイズをする場合、例えば「処理はそのままにしてちょっとクエリ名を変えたい」などのときに、同じくgraphql-toolsSchema Delegation(→ドキュメント)を使用します。新しいクエリのresolverの中でdelegateToSchema使うと、リクエストを提供元サーバURLの指定クエリに転送してくれます。

以下では、もともとmangaというクエリだったAPIの中身はそのままに、ちゃっかり名前をorenomangaと変えてしまっています。:flushed:

gian3.ts
//自分オリジナルのクエリ名を作る
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に処理を任せたということになります。:sunglasses:

image.png

APIをがっつりいじる「スキーマトランスフォーム」

コピったAPIをがっつりカスタマイズする場合、例えば「元あったクエリを消しちゃう」などのときに、同じくgraphql-toolsSchema Transforms(→ドキュメント)を使用します。

以下では、のび太によって公開されていたmangaというクエリがこっそりジャイアンによって消されていることがわかります。 :scream:

gian4.ts
const transformedGianSchema = transformSchema(gianSchema, [
	new FilterRootFields((operation: string, rootField: string) => {
			// falseを返すと消される。
			// つまり元あったQuery.mangaはこの文がfalseとなり消される。
			return 'Query.manga' !== `${operation}.${rootField}`
		}
	),
])

transformSchemaの第二引数には変換方法オブジェクトが入るのですが、今回はかわいそうなことに、FilterRootFieldsが指定されのび太のmangaは消されてしまいました。

image.png

Schema Transformsはまだまだ発展中の感がありますが、いくつか既定のTransformがあり、今回はそのうちのFilterRootFieldsを使っているわけです。

Schema Transformsの名前 機能 使うかどうか(私の主観)
FilterTypes Typeを抹消 使うかも
RenameTypes Typeの名前を変更 結構使うかも
TransformRootFields RootField(Query, Mutation, Subscriptionのどれか)に変更を加える 難しくて使わない
FilterRootFields Fieldを抹消 結構使う
RenameRootFields Fieldの名前を変更 結構使う
ExtractField パスを指定したパスに変更 よく分からない、使わない
WrapQuery 何でもできそうな変更処理 難しすぎて使えない

今回できたこと

GraphQLのgraphql-toolsの各機能を使うと、こんなことができました。 :beer:

実現する機能 graphql-toolsの機能 使うかどうか(私の主観)
スキーマのコピー makeRemoteExecutableSchema
+ introspectSchema
結構使う
スキーマの合成 mergeSchemas 使える
スキーマに変更を加える transformSchema 使うかも

まだ紹介していない機能として、Schema Directivesがあります。認証によってフィールドをアクセス不可にしたり、いろいろできるみたいです。

残る課題

APIをパクれるようになると、以下のような課題が出てくることがある。

  • N+1問題:hourglass:
    • スキーマスティッチングで複数のAPIをパクり合成すると、1回のクエリからパクリ元へのN+1回のリクエストが発生してしまうことがある。解決するにはDataloaderを使ってバッチ化するのがよい。Dataloader自体はGraphQLの共同作者であるFacebookのLee Byronが開発。Dataloaderの考え方はyuku_tさんの5日目の記事に詳しい。
  • コピーライト問題:eye:
    • 問題というわけではないですが、他人のAPIをパクったら、そこに書いてあるコピーライトを読んでちゃんとルールを守りましょうねってことです。例えばcreative commonsのcc-byだったら作者名は記載してあげないといけないわけです。じゃあどこに書くかといったときに、いきなりデータの中に突っ込んだら「あー配列の全要素に作者名が入ってしまってうざいー」みたいなことになるので、メタデータに追記する方法(別記事)を使うといいと思います。
  • 分散によるボトルネック検証:fire:
    • 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さんです。

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?