Feedforce Developer Blog

フィードフォース開発者ブログ

GraphQLを使ったアプリケーションがリリースされたので勘所を考えた

f:id:kogainotdan:20180319095637p:plain:w760

小飼です。Dropbox上場のニュースをみて『Rustで上場』という標語を考えたんですが、ロジックが乱暴過ぎるとの評価を頂きました。

さて、フィードフォースでは去る3月8日広告出稿・運用支援ツール『EC Booster』をリリースしました。
この新サービスにはクライアント・サーバ間コミュニケーションのインターフェースにGraphQLを採用しています。

GitHub, Apolloなど、海外では採用事例の増えてきている印象のあるGraphQLですが、国内における採用事例はまだあまり多くはないようです。
そこで本稿では、フィードフォースで実際のプロダクションに採用してみての、初期の使用感などをお伝えしたいと思います。

なお、本アプリケーションはAPIサーバ及びアセット配信サーバとしてのRailsアプリケーションが、 React/Apolloで構築されたクライアント側アプリケーションと、GraphQLで定義されたインターフェースに基いてコミュニケーションを取るという構成になっています。

ところで正直な所自分は、実地で使い込んでみるまではGraphQLが単にRESTful APIに近似した、別のAPIプロトコルの域を出ないものと思っていました。

特に、RESTful APIのリソースに相当するものとして捉えられるQuery/Mutationが、仕様の変更に強い(実装の変更を要しない、の意味で)というイメージには今でも懐疑的で、 実際に仕様に変更が生じてみないと確たることは言えないものの、恐らくサーバ側の実装を省略できる『場合もある』ぐらいの期待感でいるのが妥当であろう、という印象です。

むしろ、クエリ言語によってリクエストを表現するということはGraphQLの表層的な一面に過ぎず、クエリ言語を用いることによりアプリケーションの状態を宣言的に管理できるように促されるところが勘所であるように思いました。
(実際の所、いわゆるControllerとGraphQLのResolverに本質的な違いは無さそうです)

端的に言うと、RESTful APIを置き換えるものというよりはむしろ、Flux/Reduxを置き換えるようなものである、という印象です。
(同僚に『乱暴に言えばサーバ側に状態を持ったRedux』というような表現で伝えてみた所、その感じが伝わったようです)

FacebookはReactによってUIの描画を宣言的にしましたが、同様にGraphQLを提唱することによって状態管理をも宣言的にしようとしている、と言っても良いと思います。
逆に、既存のRESTful APIのイメージを継承して『命令的なリクエスト発行』という発想でいると、思わぬハマり方をするかも知れません。

我々もこのような誤謬を侵しており、特に最初期に設計されたコードには命令的な発想で設計してしまっているコードが残っており、この部分の改善は課題として感じています。

抽象的な感想話に終始してしまいそうなので、コードでご説明します。
まず以下のコードを御覧ください。

// schema.graphql
type Query {
  author: Author!
}

type Mutation {
  author(name: String!): Author!
}

type Author {
  id: String!
  name: String!
}

// query.gql
query Q {
  author {
    id
    name
  }
}

// mutation.gql
mutation M {
  author(name: $name) {
    id
    name
  }
}

この時、ページAではAuthor.nameを掲示しており、ページBではAuthor.nameを変更し得るようなアプリケーションを開発していると想定します。

// PageA.js
export const PageA = graphql(query)((props) => <div>{props.data.author.name}</div>);

// PageB.js
export const PageB = graphql(mutation)((props) => <button onClick={() => props.mutation()}>著者名を更新する</button>);

ここで、以下のようなユーザの操作手順を考えます

  1. PageAを訪問して著者名を確認する
  2. PageBに遷移して、著者名を更新する
  3. PageAに再遷移して、著者名を確認する

このような手順を考える時に、ナイーブにRESTful APIと同様のイメージで発想すると

  1. 著者名取得のリクエストを発行する
  2. 著者名更新のリクエストを発行する
  3. 著者名取得のリクエストを再度発行する

というような手順になるでしょう。

ところがGraphQLにおけるリクエストのモデルはこのような命令的な手順では行われません。
(正確にはそのような手順も実行され得るが、偶然手順が一致したに過ぎない、というようなイメージ)

ここで前述のQuery・Mutationの宣言に注目して頂きたいのですが

// query.gql
query Q {
  author {
    id
    name
  }
}

// mutation.gql
mutation M {
  author(name: $name) {
    id
    name
  }
}

この宣言は『query Qは Author型におけるnameを保持するもの』、『mutation MはAuthor型におけるnameを変更するもの』というような読み解き方ができます。

例えばこれを以下のような宣言の仕方をした場合,

// mutation.gql
mutation M {
  author {
    id
  }
}

『mutation MはAuthor型を変更するが、何を変更するかはわからないもの』というような読み解きになります。

従って、この場合実行時においてはquery QはAuthor型に何かしらの変更があったことは知りうるかも知れませんが、それがなにであるかわからないため、

  1. PageAを訪問して著者名を確認する
  2. PageBに遷移して、著者名を更新する
  3. PageAに再遷移して、著者名を確認する

の3では、1の著者名をそのまま表示することになります。
間違えてしまいやすいのは、

mutation M {
  author(name: $name) {
    id
    name
  }
}

このmutation Mにおける nameは、mutationの結果の返り値『ではなく』(実際の通信上のレスポンスとしてはそのように返ってきますが)、Author型に発生した何かしらの変更の種類(つまり著者名が変更されたこと)を現しています。

前述の乱暴な例えを用いるならば、name変更のActionを発行した、というような捉え方もできると思います。
また、他の宣言的なモデルと同様、GraphQLとそのクライアントライブラリ実装(この場合はApollo)ではこの種の宣言が内部で命令的な処理に転換されており、 あるAuthorが変更された時に、それがどのAuthorの実体に起こったことなのかを同定する必要があり、Author型に定義されたidはそのために用いられているようです。

我々も実際の開発の場面においてこの勘所が見抜けていない時期には、mutationを発行したのにページが変わると元の状態に戻ってしまっている、というような挙動を作ってしまったことがありました。
(正直に言うとまだこのような実装は残っていて、queryを命令的な挙動で実行するfetch-onlyというオプションを使って凌いでいる箇所があります)

このように、GraphQL及びそれに付随するライブラリには、インターフェースの設計という表面的な部分だけでなく、 むしろ状態管理というクライアント側アプリケーションの重心を刷新するような力があるようです。 そのことから、個人的にはサーバ・クライアントのコードを同じ人物が編集するようなチームにこそ、より強くフィットするであろうと感じています。

また副次的な効能として、静的な型定義を基盤に構築された言語仕様を持っているところから、チームにインターフェース境界を強く意識して設計する動機が生まれたようにも思います。
(特にインターフェースと実装は全く別のもの、という考え方を共有する機会になってくれたのは良かった)

もちろんこれはRESTful APIを用いていた場合にもSwaggerなどのツールで達成し得るものですが、言語仕様に静的な型定義が策定されていることから、その強制力はより強いものになっているようです。

というところで、GraphQLの使用感をお伝えしてみました。
参考になれば幸いです。