GraphQLの規格とプロダクトの隙間をどう埋めるか 〜ファイルアップロード〜

introduction

しもやま(@_h_s_ / hshimoyama)です。

昨日 2018/01/30 に GraphQL Tokyo meetup #4 に参加してきました。

www.meetup.com

オープンスペース形式で行われる勉強会で、興味のあるセッションに参加でき、非常に近い距離で議論が出来る、とても良い会です。

今回は、前回に引き続き一枠セッションオーナーをやらせていただいたので、『GraphQLの規格とサービスの隙間をどう埋めるか』というテーマでディスカッションセッションを立てさせて頂きました(事前作成資料とか無くて申し訳なかった…(。

その中で、なかなか面白い知見が得られたと思うので、ブログにメモしておこうかなと思います。
(セッション開催時はサービスという単語を使っていたのですが、プロダクトの方が適切かと思いブログタイトルはそれとは変更しています)

 長くなったので今回はファイルアップロード編です。次回があればエラー処理について書くつもりです。

 

 

GraphQL規格とプロダクトの隙間?

GraphQL は非常に薄い層について規定した規格で、通信経路やビジネスロジックの解決方法などについての規定はありません。あくまでも、渡された Query, Mutation の文字列を GraphQL Type にパースし、ビジネスロジックレイヤに処理を delegate する。そして、結果を data (あるいは errors) として返すという層に限定して規定しているようです。

 

通信経路は Best practice Serving over HTTP で書かれている通り HTTP が扱いやすいとは思いますが、その他の経路も規格上特に問題は無く、response も JSON serialization が preferred と規格に記載されていますが、併せて GraphQL does not require a specific serialization format とも書かれています。

ビジネスロジックについては、こちらも Best practice Thinking in Graphs にて言及があり、REST, RPC そして GraphQL の層とビジネスロジック層とは切り離されています。

 

そのため、GraphQL を実際のプロダクトで使用していると、GraphQL 規格に明確に定義されていないレイヤをどのように定義し使っていくか、という問題が発生します。個人的に困った例やセッションで挙がって例としてはこんなところです。

  • ファイルアップロード
  • エラー処理
  • 言語規格の型とGraphQL型との親和性
  • スキーマの扱い (GraphQL Schema Language の IDL あるいは DSL で静的に解決するか、introspection で動的に解決するか)
  • 認証と権限管理
  • View decoration と GraphQL
  • etc. etc.

今回、そういったトピックの中から比較的多くのプロダクトで問題になりがちな、ファイルアップロードとエラー処理についてディスカッション出来ました。そのうち、ファイルアップロードの問題の概要と、その解消方法についてまとめたいと思います。

GraphQLとファイルアップロード

GraphQL の Mutation でファイルアップロードを扱いたい場合、GraphQL server が Mutation の文字列を受け取るだけの素朴な実装の場合、Mutation のString 型のフィールドに、Base64 エンコードしたバイナリを埋めて渡す、といった形になると思います。しかしその場合、

  • 大きなファイルを1リクエストで送る必要がある (multipart で送ることが出来ない)
  • オンメモリに展開される
  • Base64 エンコードでデータ量が増える (gzip 等である程度軽減は可能)

といった問題があり、これらを許容することの出来るプロダクトは多くないと思います。

この問題を解消する方法として幾つかの改善例があるので、それらを紹介していきます。

ファイルアップロードAPI + Mutation

Graphcool 等が採用している方式です。API ドキュメントは以下にあります。

File Management | Graphcool Docs

 

ファイルアップロード専用のAPIがあり、そこへアップロードしたいファイルを multipart/form-data で送ると、GraphQL の File Node を新しく作成します。response で ID やそのファイルの情報を返すため、別途 Mutation でそれらの情報を渡すことで関連を作ることが出来ます。Graphcool では ID で関連を作ることを推奨しているようです。

Graphcool 以外で似た方式を採用しているケースでは、アップロードAPIで GraphQL Node を作成せず、AWS S3 key や、アップロードされたファイルにアクセス出来る public URL を返し、mutation でそれを渡す方式があるようです。しかし、ファイルのアップロード先の権限管理や GraphQL Node への関連付けを行う処理をサーバ側で隠蔽できる点などを考慮すると、Graphcool のようにファイルアップロードと File Node の作成をセットで行い、関連を ID で扱う方法がベターではないかと思います。

 

この方式のメリットは、

  • シンプルなAPI (multipart/form-data でファイルを送るファイルアップロード API と、GraphQL API なのでシンプル)
  • ファイルのサーバーへのアップロードを同期的に扱う箇所があるため、悪意あるファイルを永続化する前にブロックすることが出来る (ウィルスチェック等を行うポイントを明確にし易い)

対してデメリットとしては、

  • 2 リクエストしなければいけない (アップロードと Mutation のリクエストに分かれている)
  • ファイルアップロード時に API サーバを経由するため、オーバーヘッドがある
  • ファイル内容のバリデーションがしにくい (アップロード時点で使用されるコンテキストが明確でないため、用途による validation が出来ない。コンテキストを渡すことも出来るが、クライアントの自己申告になってしまう。Mutation のタイミングなら可能だがオーバーヘッドが大きい)
  • クライアントが GraphQL の文脈と REST の文脈の両方を把握する必要がある (ファイルアップロードAPIは実質REST APIになると思います)

といったことがあります。

デメリットはそれなりにあるのですが、大抵のケースで致命的ではないものが多く、public な GraphQL API として提供する場合にはバランスの良い選択肢かなと思います。

Direct uploading + Mutation

(Public な採用例を挙げられないのですが) クライアントが API サーバを経由せずにファイルをアップロードし、その結果の URL あるいは key を Mutation で渡す方式です。↑の方式のファイルアップロード API が Direct uploading になったようなイメージですね。

Direct uploading のアップロード先はクライアントが静的に保持するか、時限式のアップロード先を何らかの手段で取得してアップロードする形になります。ネイティブアプリケーションクライアントはともかく、Webアプリケーションクライアントで静的に保持するのは難しいため、時限式のアップロード先を取得する形になるでしょう。その場合は、GraphQL Query で取得出来るようになっているケースが多いようです。

 

この方式のメリットは、

  • アップロードのオーバーヘッドが少ない (APIサーバを経由しない分、オーバーヘッドが少ない)
  • シンプルなAPI (サーバーは GraphQL API のみ提供)

対してデメリットとしては、

  • 2 or 3 リクエストしなければいけない ([アップロード先取得の Query、]アップロード、Mutation)
  • ファイルアップロードやアップロードトリガでのファイルチェックが AWS 等にロックインされる
  • ファイル内容のバリデーションがしにくい (↑の方式と大体同じです)
  • クライアント側が Direct uploading 対応が必要

という感じかなと思います。

↑の方式と同様、public な GraphQL API として提供する場合に、バランスの良い選択肢だとは思いますが、クライアントへ要求する事が多かったり、ファイルと Node との関連付けがサーバ側に集約されない等の違いがあるので、採用出来るケースは多少絞られるのかなと思います。

Mutation リクエストの拡張

apollo-upload-client / apollo-upload-server や類似ライブラリで行っているような方式です。

Mutation のリクエストを拡張し、Mutation の String とファイルとを分けた上で multipart/form-data で送ります。その際、Mutation 文のどのフィールドがどのファイルに対応付くのかを示すため indicator を埋め込むようです。そうすることで、サーバは受け取った multipart/form-data ファイルを一時ファイルとして書き出し、Mutation 処理の際に indicator からファイルを参照することが出来ます。

 

この方式のメリットは、

  • 1 リクエストで処理が完結する
  • アップロードのコンテキストが明確なため、バリデーションが行いやすい

対してデメリットとしては以下のようになります。

  • クライアント・サーバーの相互依存が大きい (クライアントはサーバ側がどう解釈するかを知っている必要があり、サーバはクライアントがどうリクエストを送るかを知っている必要があります。フレームワークどころか、使用しているプラグインライブラリまで共通化する必要があるでしょう)
  • ファイルアップロード時に API サーバを経由するため、オーバーヘッドがある

これについては、クライアント・サーバーの相互依存が大きいことが一番の問題になるでしょう。Apollo のアップロード拡張だけでも複数のライブラリがあり、それぞれでファイルの送信方法や Mutation への indicator の埋め込み方式が異なるため、クライアント・サーバのライブラリの組み合わせが異なると動作しなくなります。

public API で採用するのはほぼ不可能(サーバー側が対応しきれない)だと思いますが、private API ではそのあたりがハードルにならないように設計することが出来るため、ファイルアップロードの特殊処理を全てライブラリが吸収してくれる可能性があります。

自身では触れていないのですが、クライアント・サーバ共に js の環境では選択肢に入るのではないでしょうか。

 

ひとまず最後に

セッションまとめとしてまずは GraphQL のファイルアップロードについてまとめました。まだまだ勉強中なので勘違いや間違いもあるかもしれません(勢いで書いたので色々あるかも…)。また、私が知らない良い案もあるんじゃないかな、とも思います。そのあたり、どんどん指摘して頂けると嬉しいです。

また、GraphQL Tokyo meetup はこのへんの話もすることが出来る(初心者向けセッションもあります!)ミートアップなので、興味のある方は是非次回ディスカッションしましょう。