esm アジャイル事業部 開発者ブログ

永和システムマネジメントアジャイル事業部の開発者ブログです。

GraphQL でファイルをアップロードしたい

こんにちは、hibariya です。さいきん GraphQL でのファイルアップロード方法について知りたいなと思いちょっと検索してみたところ、すぐにはこれといった方法に辿りつけなかったので気になって調べました。手元で試した感じだと GraphQL multipart request specification という仕様が良さそうでしたので、今日はそのことについて紹介したいと思います。

GraphQL でのファイルアップロードはめんどう

現在のところ、GraphQL の仕様ではファイルアップロード方法については特に規定されていないため、自分たちで方法を決めて実装する必要があります。が、これは少し面倒です。HTTP で GraphQL を使う場合、サーバに渡す値はたいてい JSON のようなフォーマットでシリアライズしますが、FileオブジェクトはそのままJSONにはエンコードできません。

{
  "query": "mutation CreateMessage($input: CreateMessageInput!) { ... }",
  "variables": {"input": {"message": "hi", "file": "<アップロードしたい File をここに...??>"}}
}

ですから、GraphQL でファイルをアップロードしようというときには少し工夫が必要になります。方法としてよく挙げられているのは以下の3つでしょうか。

  1. ファイルを Base64 エンコードして JSON に含める
  2. ファイルのアップロードには GraphQL とは別の API を使う
  3. リクエストを multipart/form-data として送る

最初の方法1は、そのままではシリアライズできないファイルを Base64 エンコードすることで JSON として扱えるようにする方法です。クライアントとサーバ間の通信方法を大きく変える必要がなさそうで、シンプルそうに見えます。ただ、巨大なファイルを扱う場合には、あまり考えずにサーバ側のアプリケーションを実装すると巨大な JSON をオンメモリで扱うことになって面倒そうな気がします *1。あとはファイルサイズが若干増えますし、ファイル名のようなメタ情報を自分で渡さないといけないのも少し面倒ですね。

次の2はファイルアップロードを GraphQL API では扱わず、別途用意した REST API などを併用するという方法です。確かに方法のひとつとしては考えられますが、使い方によっては、元々ひとつだったトランザクションを分ける必要が出てくるなど用途を選びそうです。

最後の3は GraphQL のリクエストを multipart/form-data として送る方法です。ここで紹介する GraphQL multipart request specification は、この方法を採用しています。

GraphQL multipart request specification

リクエストに multipart/form-data を使う方法であれば、巨大なファイルは (例えば Ruby なら) Rack のレイヤで少しずつファイルに書き出される *2 ため、サーバサイドのメモリ使用量についての心配はひとまず置いておけます。また、この方法にはいくらか認知されている仕様として、GraphQL multipart request specification というものがあります。

仕様が定めているのは、multipart でどうやって GraphQL リクエストを送るのか、特に GraphQL の変数の値としてとしてどうやってファイルをマッピングするのかというところです。仕様そのものは単純なので、例をまじえて説明します。 README とほぼ同じですが、冒頭の例で説明するとクライアントは以下のような感じの multipart message をサーバに送ることになります。

--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="operations"

{
  "query": "mutation CreateMessage($input: CreateMessageInput!) { ... }",
  "variables": {"input": {"message": "hi", "file": null}}
}

--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="map"

{ "0": ["variables.file"] }

--------------------------cec8e8123c05ba25
Content-Disposition: form-data; name="0"; filename="hi.png"
Content-Type: image/png
Content-Transfer-Encoding: base64

(Base64 エンコード済みのファイル)

--------------------------cec8e8123c05ba25--

最初の part を見ると、operations というパラメータの中に GraphQL のクエリや変数が格納されています。ここは相変わらず JSON ですね。ところで肝心の変数 file (variables.input.file) が null となっています。これはtypo ではなくそういうもので、ファイル本体は別の part となっているため、サーバ側で適切にマッピングして、この null を上書きする必要があります。この例では、ファイルは最後の part にある hi.png です。真ん中には map というパラメータの part があります。これは、最初に登場した null と実際のファイルをマッピングするための情報です。この例の { "0": ["variables.file"] } でいうと「0 というパラメータは、variables.file の中身ですよ」といった意味になります。

サーバ側は、この仕様の実装がアプリケーションより下の層にあれば、アプリケーションを大きく変更することなくファイルのアップロードを実現できます。Ruby なら Rack のミドルウェアとして実装して、ファイルを params[:operations][:variables][:input][:file] で参照できるようにする、という具合です。

クライアント側も、変更が必要なのは主にネットワークレイヤになります。実装のひとつに Apollo Link のミドルウェアとしてこの機能を提供しているものがあるので、もし Apollo を使っているならそれを導入することで対応できるでしょう。残念ながら現時点では Relay (Modern) の実装として公開されているものは見当らないのですが、Relay の Network Layer をカスタマイズすることでなんとかなりそうです。Relay の方はちゃんと試してはいないので、知見をお持ちの方がいれば教えてください。

良さと注意点

現時点だと GraphQL でファイルをアップロードする方法としてはこの GraphQL multipart request specification が良さそうだと感じています。理由としては、まず仕様として公開されていて、既にいくつか実装があるということ。また、それらの実装を使うことで既存のアプリケーションの実装を大きく変えなくてもよいというのも良いですね。様々なクライアントに対応できるのが GraphQL の良さのひとつですので、Apollo/Relay どちらからでも使えるというのも重要なポイントだと思います。それから、私はよく Ruby を使うので Ruby の実装があるのも嬉しい。

注意が必要な点としては、あくまで multipart/form-data を前提にしているので、それに対応できないサーバやクライアントでは採用できません。

Ruby でのサーバ側実装

サーバサイドではどんな感じで導入すれば良いのでしょう。最後に GraphQL multipart request specification の Ruby のサーバサイド実装として jetruby/apollo_upload_server-ruby を紹介します。この gem が提供するのは次の2つです。

  • アップロードファイルを表現する Upload というスカラ型
  • GraphQL multipart request specification を実装した Rack middleware

Gemfile に足すと middleware が挿入されるので、基本的にはそれで導入は完了です。ただ、ひとつ注意点があります。このミドルウェアはファイルを ApolloUploadServer::Wrappers::UploadedFile として params に入れてくれるのですが、これは ActionDispatch::Http::UploadedFileDelegateClass でラップしたオブジェクトです。そのため、Active Storage にそのまま渡せないという問題があります (jetruby/apollo_upload_server-ruby#10)。私が手元で検証した際は、workaround として この comment のように IO とファイル名をひとつずつ渡しました。

おわりに

GraphQL でのファイルアップロードについて現状の私の理解を書きました。ここで紹介した GraphQL multipart request specification は、良さそうとは言いつつも、本格的な導入はまだ残念ながらできていません。知見をお持ちの方がいればぜひ教えてください。

もうすぐ大晦日ですね。よいお年を!

参考

Rails / OSS パッチ会 2019年1月のお知らせ

コミュニティマネージャの koic です。

新年最初の Rails / OSS パッチ会を2019年1月24日(木)に開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には RubyRails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

前回 2018年12月のパッチ会には、a_matsuda が元 Rails コミッターで来日中の senny を連れてきてくれて、初の海外ゲストを迎えた会になりました。

f:id:koic:20181217150924j:plain:w400

以下、前回の活動が関わる成果です。

amatsuda: ghq_transfer

github.com

kamipo: Rails

github.com

koic: RuboCop

github.com

meganemura: OpenAPI Generator

github.com

sue445: RuboCop

github.com

unasuke: Itamae

github.com

unasuke: Rails

github.com

yahonda: Rails

github.com

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。特に募集ページなど設けませんので、興味のある方は永和システムマネジメントの神田オフィスまでお越し下さい。

agile.esm.co.jp

Ruby 2.6.0 リリース後の新年最初のパッチ会です。終わった後は有志で新年会などあるかもしれません。

Rails Developers Meetup 2018 Day 4 に colorbox と junk0612 が登壇、ランチスポンサーをします

2018年12月8日(土) に開催される Rails Developers Meetup 2018 Day 4 Nouvelle Vague に colorboxjunk0612 が登壇し、ランチスポンサーとして協賛します。

techplay.jp

junk0612 こと小林 純一の講演 (11:30-11:50 / トラックB)

junk0612 こと小林 純一の講演は 11:30 - 11:50 の枠で『Rails x パターン』というタイトルで、Rails プロジェクトにおけるソフトウェアデザインなどのパターンについて取り上げた話です。

新卒入社から3年目にあたり、これまで経験したいくつかの Rails プロジェクトで得たソフトウェアパターンの知見を共有した話になるようです。

スポンサーランチ (13:15-14:00 / トラックA)

数量限定ではありますが、ランチスポンサーとしてお弁当を用意します。

ランチをとりながら、弊社が開発しているビデオチャットサービス Linkup の紹介をします。

linkup.world

colorbox の講演 (14:35-14:55 / トラックA)

colorbox の講演は 14:35 - 14:55 の枠で『Kata の作り方』というタイトルで、Dave "達人" Thomas の CodeKata にインスパイアされた Kata の話をします。

codekata.com

Ruby 3.0 で話題に取り上げられている型 (Type) ではなく、武道や舞踊などの型 (Kata) をソフトウェア開発の流儀に型どっての話になるようです。

当日を楽しみに会場でお会いしましょう。

f:id:koic:20181130114625p:plain:w300


永和システムマネジメント アジャイル事業部では、Ruby / Rails を使ったアジャイルなソフトウェア開発を一緒にしていくメンバーを募集しています。

www.wantedly.com

新メンバーの入社歓迎会を行いました!

フィヨルド様のブログで既報の通り、ブートキャンプからの新しく入社してくれた @yoshinotaiki (通称「神」)の入社歓迎会を行いました。 神の新しい門出を祝うために、フィヨルド様から駒形さん、町田さんにも出席いただいて、楽しい時間になりました。

f:id:m_pixy:20181122154906j:plain 神による入社の挨拶。

f:id:m_pixy:20181122154951j:plain 駒形さんからの祝福の言葉。

f:id:m_pixy:20181122155046j:plain 事業部長の挨拶を聞かずにカメラ目線の神。

楽しい歓迎会をまたやりたいので、一緒に仕事をしてくれる仲間を募集しています。興味のある方はこちらをご覧ください。

f:id:m_pixy:20181122155244j:plain

Rails / OSS パッチ会 2018年12月のお知らせ

コミュニティマネージャの koic です。

今年最後の Rails / OSS パッチ会を2018年12月14日(金)に開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS への upstream にパッチを送る会です。

会には RubyRails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

前回の活動が関わる成果は以下でした。

kamipo: Rails

github.com

koic: Psych

github.com

koic: RuboCop

github.com

sue445: Itamae

github.com

y-yagi: Rails

github.com

github.com

yoshino: Danger

github.com

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。特に募集ページなど設けませんので、興味のある方は永和システムマネジメントの神田オフィスまでお越し下さい。

agile.esm.co.jp

Ruby 2.6.0 リリース前の今年最後のパッチ会です。終わった後は有志で忘年会などあるかもしれません。

f:id:koic:20181123163935j:plain:w400

RubyConf 2018 (Los Angeles, CA) に行きました

RuboCopActive Record Oracle enhanced adapter などの OSS コミッターをやっているコミュニティマネージャの @koic です。

2018年11月13日(火) から 2018年11月15日(木) の間、カルフォルニア州のロサンゼルスで開催された RubyConf 2018 に行きました。

rubyconf.org

渡航までの準備については、個人の日記の方に書いているのでそちらを参照してください。

今回参加した動機は RailsConf 2019 に向けた渡米の経験を積むのと、RubyConf といった古くからの Ruby カンファレンスの雰囲気を知るといったものでした。

カンファレンス

f:id:koic:20181119160446j:plain:w400

ここ最近の RubyKaigi への参加のモチベーションと同じくトークもさることながら、GitHub 上での OSS 開発でやりとりしている開発者とのオフ会への参加という側面での参加動機も大きかったのが今回です。

GitHub で何度かやりとりをしていた FactoryBot のコミッターでもある Daniel が自分を探して見つけてくれて、RuboCop 1.0 に向けた Gem の切り出しでの GitHub での活動について話したり、Rails issue team の Vipul を紹介してくれたり (Vipul は「前に Asakusa.rb に行ったよ」と話していました) して、一緒に @tagomoris@joker1007 の発表を聞いたりしていました。

また、タイムリーに RuboCop に提案の来ていた新 Cop について、rescue => e の変数名をデフォルトで exception に統一するというのに違和感があったため RubyConf に来ていた Ruby コミッターに意見を聞いたりで rescue => e にすべき論拠を集めたりした成果が以下です。

github.com

トークとしては、RubyKaigi のように tech talk を集めたものではなく、soft talk が広く採択されていたり、ランチ後に Eileen など著名 OSS 開発者などを壇上にあげて「Ruby を動物に例えたら?」みたいなゆるふわなクイズタイム (ここでそれ Python やろという "Snake" と回答を述べた Eileen すごい) を設けたりといった感じで、RubyConf は RubyConf というカラーがあるのだと空気を感じながら見ていました。

f:id:koic:20181119160411j:plain:w400

あと2日目の Lightning Talks に申し込んではいたのですが、時間が来て17人目くらいで打ち切られて順番がまわってこなかったため、どこかの機会に使おうと思います。また英語でのプレゼンはしたことがないので、作ったスライドに対してホテルの部屋に戻って事前にスクリプトを書いて声に出して練習するとか、普段まったくしないことをしていました。

f:id:koic:20181119160332j:plain:w300

生活面

パスポート、スマホ、クレジットカードの三つの携帯は必須。日本と違って印象的な点は、基本的にクレジットカードでの支払いとなり、タクシーや一部店舗では現金を取り扱っていなかったり、チップ文化で食事には 15% から 20% を上乗せとなります。何人かで食事をする場合は各人のカードを出して split してもらっていました。その後に各人のチップを上乗せした額を記載して支払い完了です。物価が高いと聞いていた US では肌感覚で日本の 1.4 倍以上は食費がかかった気がするので、今月のカードが怖いですね (ただしクラフトビールは質を考えると日本より安かったように思えます) 。

基本的に車移動となるため Lyft などのサービスを使います。Lyft については、あまり人が多くないところでお互いを探しやすい目的地への経路上で接触を計ると良いです。Uber は使わなかったのでわからないですが、Lyft はチップもカード払いできるので便利です。

カンファレンス会場となるホテルに宿泊していたので、たまたま向かいの部屋だった @amatsuda と部屋飲みしつつ Ruby 2.6 に向けて開いている PR への実装でのプライベートかつワークアラウンドのメソッド名の相談をしたりして過ごしていました。

github.com

謝辞

とりわけ渡航費、宿泊費、参加費、ほか諸経費を支えてくれた勤務先の永和システムマネジメント。渡航前にアドバイスをもらった @yahonda@masa_iwasaki 。行き帰りの飛行機を合わせてとって勤務先のルームシェアスポンサーに応募してくれた @284km (こういったスポンサーだったことに LAX✈️NRT の飛行機で決めました) 。渡航中に美味しいお店などを探してくれた @tagomoris@joker1007。折に触れて気にかけてくれたフェローの @kakutani に感謝します。

f:id:koic:20181113121623j:plain:w400