でぃするだいありー?

そんな気はないんだれど、でぃすっちゃってる。 でぃすでれ?

GraphQL備忘録

本稿を記す動機

そもそもの発端としては、node.js で作成した Web API があり、それが参照する既存テーブルがCRUDを意識していないものだから、date とか time とかいうカラムを持ち、それを結合したフィールドで範囲指定したデータを抽出するような機能を追加したいと思ったときに、適切と思えるエンドポイントが思いつかないというところから始まった。そもそも Web API の経験が少ないので、厚顔無恥なURLでエイヤっとやるような勢いを持っていなかったわけである。

どのようなやり方がふさわしいのかと調べているうちに GraphQLというワードを拾い、興味を覚えて、2019/07頃から試行を始め、結果として、エンドポイントに悩むようなAPIについて非常に優位性のある技術ではないかという感想を得た。
ただし、テーブルが二つしかなく、かつ結合条件での問い合わせなどない環境であるので、GraphQLを語るときに(おそらくは rails を念頭に置いて)も抜きにはできなくなったN+1問題とかには触れていない。

これと前後してRust に興味を持ち、チュートリアル後の課題を探したとき、まずは既存の自作プログラムと似たようなものを作ろうとして赤外線リモコン送信を試みたが挫折し、いじけていたところに、よせばいいのにGraphQLを思いついてしまった。
モジュールのバージョンが1.0未満だったり、参考にした記事がちょっと古いだけでもう役目を終えていたり、なかなかこれといった情報に巡り合えず、これも一時断念。逃避的思考からか、GO言語でもできるのかなとふと思い立ち寄り道してしまったが、Rustと同様のアレコレに出くわして思うようにはかどらなかった。2019/08~09は、挑戦がことごとく失敗する魔の期間であった。

躓きすぎて投げだしそうになったが、あきらめきれずもう一度 Rust で試したところ、diesel の使い方がわかって進捗を得た。railsのようなフレームワークの使用経験はプライベートにとどまり、あまり熱心に学んでもおらず、オマジナイとして使用してきたのだが、少しやる気を出して理解してみたカンジ。ひとつわかってみればというところで、gqlgen の使用方法の理解にもつながり、GO言語での実装も果たせた。

以上の取り組みにあたって初期に設定した命題と、結果的に比較することになってしまった実体験から得られた差異と所感について以下に述べる。

試行時の命題

CRUDを意識していない既存のテーブルに対するアプローチとして、どんな方法が採れるか、どの方法が向いているのか。
(migrationをせず、フレームワークを活かせるか)

差異について

GO と Rust で実装した GraphQL APIインターフェイスは、node.js で作成したものとは異なり、Mutation 時に Playground でいうところの、query variables を使用している。初心に戻って apollo-server でも同じような実装が可能かどうか調べてみたところ、2019/10現在では、Mutation の値は引数渡しで、query variable を用いるものではない。
Pythonの graphene も apollo-server に近い方式である。

所感

どれが便利なのかと問えば一長一短か。curl での実行してみるとわかるが、query variables を使用する方法は、まあ、大変である。とはいえ、これは項目の多寡によって変動すると感じられ、現在の所感としては、

  • 項目が少ない場合は apollo-server が採用している「引数で項目ごとに値を渡す方法」が楽であろうと思う。
  • 項目が多い場合はdiesel や gqlgen が採用している「query variables で値を渡す方法」が無難であろうと思う。

使用した言語とモジュール、命題への回答

1. node.js

  • apollo-server
  • sequelize
  • graphql
命題への回答

sequelize がSQLでの問い合わせを強力にサポートしているので、CRUDを意識していないタイプのテーブルにも順応性が高い。

2. GO

  • gqlgen
  • gorm
命題への回答

gqlgen は、テーブルは既存が前提で、テーブル定義に対する構造体(schema.graphql)さえかければ、gqlgen が必要な定義(generated.go)を自動生成するので、あとは不足を補ってくださいというスタイルだ。テーブル定義を変更した後、再度 gqlgen することによって先の定義を再生成するので、変更箇所もわかりやすいかもしれない。

  • timestamp や datetime の扱いにちょっとした細工が必要。
  • クエリの引数でオプショナルな指定ができない?ため、SQLのWHERE句に相当する文字列を与えるようにするか、ちょっとアレなインターフェイスにするしかないっぽい。
  • schema.graphql はそもそも手作りなので、既存テーブルに際しても特に躓きはない。(migration しないと特定のファイルが生成されない、というようなことがない)
  • gqlgen によって models_gen.go を自動生成した後、実テーブルの対応を手作りせねばならない。gorm に親しんでいれば問題はないと思われるが、カラムの型によっては悩みどころになるかもしれない。
  • gqlgen によって生成された generated.go にインターフェイスが定義されるので、それを resolver.go に手作りしていくという流れが理解できるまで、誰が何をしてどうせねばならないのかわからなかった。

3. Rust

命題への回答

diesel はおそらくCRUDを強く意識しており、migration 前提である。migrationを実行しないと生成されないファイルがあるため、既存テーブルの場合、その分、手間が増える。

  • mysql では? sql_query で実行時エラーとなる。シリアライズに問題を残しているらしい。
  • 既存テーブルの場合、schema.rs を手作りする必要がある。migration を行うこと前提であるため、そうしない場合の労力が増える。
  • ある一つのテーブルに対して、テーブル定義に相当する構造体、新規作成時のインターフェイスとしての構造体、新規作成時のDBへのデータエントリのための構造体、更新時のインターフェイスとしての構造体が必要になった。テーブルの構造に依存するので特例ともいえるかもしれないが、テーブルのキーがidのみではないような場合には、そうせざるを得ないだろうと思う。

4. Rails (2019/10/08 追記)

命題への回答

migration 必須と思い込んでいたのだがそうではなく、思いこんでいたよりもはるかに自由度が高かった。自由度の高さは最大級、上記のうち node.js を上回るかもしれない。

  • railsの作法を覚えれば非常に容易である。scope が非常に強力、感動的。
  • 予約語カラム名になっているとちょっとハマる。
  • mutation の記述は非apollo-server方式。
  • Windows10、ruby 2.6.4p104、rails 6.0.0 という環境で Gemfile に記述する方法では mysq2 を正常にインストールできない。以下を参考に解消した。


5. Python (2019/10/31 追記)

  • uwsgi
  • graphene
  • Flask-GraphQL
  • SQLAlchemy
  • graphene_sqlalchemy
命題への回答

上に記した node.js の実装感に近い。今回試したものは自動生成する仕組みはないようで、すべて手書き。

  • 予約語がOKな個所とNGな個所がある。
  • mutation の記述はapollo-server方式に近いがちょっと違う。
  • 余談となるが、ariadne + uvicorn という組み合わせも試したが、サーバーを起動して一晩放置していたら、翌朝SSH接続もできないくらい重くなっていた。ラズパイにはむかないのかもしれないと感じたため、本稿のような組み合わせを採用した。

6. Python django (2019/11/02 追記)

命題への回答

上に記した Python の例と同機能を実装することができた。

  • django に限った話ではないが、フレームワークあるあるで django の学習コストがちとキツい印象。
  • Qオブジェクトは慣れれば使いやすいと感じられるかもしれないが、後付け感のある機能でフレームワークの例外という印象。
  • graphene を使用しているので、クエリのレイアウトは上記 Python と同じ。

まとめ

今回設定した命題については、node.jsによる実装が自由度が高いと感じられる。
とはいえ、GraphQL における mutation のスタンダードが diesel や gqlgen のようなものであるならば、少なくとも apollo-server では現時点でそれは実現できないようである。しかしながら、diesel や gqlgen はまだまだこれからという印象があり、また、どちらにも善し悪しがあり選び難い。まだまだ途上の技術である、というところか。
余談としては、prisma がよさげに思えたのだが、docker 必須であり、手元にある機器(windows10、raspbian)では実現できず、期待度は高いが試行環境の構築から躓いている。

2019/10/08 追記:
n+1問題について未検証でなんともいえないが、実装のスピーディさでは rails がかなり良い。言語を問わずGraphQLの実装検討を要望されたら、筆頭候補に挙げてもいいかもしれないと思える。
適用対象となるであろうラズパイでは rails は重すぎるという印象だったが、やってみたらそうでもなかった。ただし、buster だとrubyおよびrailsのインストールにやや難がある。

2019/10/31 追記:
node.jsとPythonによる実装は自由度や難易度的に同程度という印象。Pythonのgrapheneは、Pythonを書いているというより、ライブラリの作法に従っているという感があり、勘所を要する印象。その点については、apollo-server を使用した node.js の方が学習コストが低いかもしれない。あるいは、grapheneの方が良いサンプルを見つけにくいということかもしれない。
node.js、GO、Rust、RailsPythonと試してきたが、更新クエリの書き方として query variableを使用するもの(GO:gqlgen、Rust:dieselRails:graphiql-rails)と使用しないもの(node.js: apollo-server、Python:graphene)の比が3:2であり、どちらが主流なのかよくわからない。クエリとデータを明確に分けられるという意味で、バッチ的な作成更新処理を行うなら前者がよさそうな気もする。

2019/11/02 追記:
ここまでで、オススメ順は以下の通り。
4 >= 1 = 5 > 6 > 3 > 2
(1. node.js、2. GO、3. Rust、4. Rails、5. Python graphene、6. Python graphene + django
ただし、Rails は日付の扱いがフレームワーク依存らしいので、その辺を仕様で吸収できる場合に限る(DBには常にUTCで保存、表示時にJSTにする、とか)。

2019/11/12 追記:
Paginationについて、railspython、node.jsで検討し、以下のように変更。
4 >= 5 > 1 > 6 > 3 > 2
理由としては、node.js の Relay Cursor Pagination は、react とか既存のフレームワークを使えという指標なのか、GraphQL単体での実装方法が見つからなかったためである(既存のREST APIをデータソースにする方法、first・skipの形式のものは見つかった)。