Feedforce Developer Blog

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

GraphQL API をスキーマファーストで開発したいけどスキーマと実装で乖離を起こしたくない

feedforce Advent Calendar 2017 14日目の記事です。(遅くなってごめんなさい!)

こんばんは、エンジニアの id:kielze です。

先日Qiitaに GraphQL Ruby の使い方 (基礎編) という記事を投稿しました。この記事ではgraphql-rubyを使った際のGraphQL APIの実装について、QueryからMutationからテストまで、実際のコードとともに解説してみました。Mutationまで書いてある日本語の記事は少ないと思うので、是非参考にしてGraphQLを実装してみてください。

GraphQLのスキーマと実装

さて、この記事の中で私は graphql-rubyのschemaが正しいか確認するテスト について書きました。このテストを自動で走らせることによって、実装とスキーマに乖離が起こることがなくなり、スキーマやドキュメントを常に最新に保てるという内容です。

このテストは非常に良かったのですが、バックエンドの実装が終わらないとスキーマの定義が上がってこないという欠点があります。このQiita記事を書いた時はフロントエンドに対してバックエンドが先行してAPIの実装を進めていたので良かったのですが、うちのチームのフロントエンドは開発が爆速なのですぐに追いつかれてしましました。また、実装後にスキーマの議論をすることは見通しが悪く、後から修正依頼が入ってくることもあったりして良いことばかりではありませんでした。

そのため、チームで話し合いスキーマファーストでGraphQL APIを実装しないとね、ということになりました。その過程で一度上記のテストは止めることになりました。

スキーマファーストで開発するために

先日投稿された「GraphQL APIをスキーマファースト開発するためのモックサーバをRailsとApolloで作る」という記事も非常に勉強になりました。

まだ試しながらなので完全に固まっているわけではないのですが、いまうちのチームで行っているやり方も書いてみます。といってもモックサーバーは今のところ用意しておらず、もう少し簡素です。

スキーマを定義する

まずは スキーマを定義していきます。上記の記事と同じように、graphql-rubyのDSLでfieldを書いてresolverを書かずにコミットする、という方法が今のところ使われています。フロントエンド側で定義したい場合には schema.graphql に直接手書きする予定なのですが、まだ実際にやっていないのでどうなるか分かりません。しかし、できればバックエンドやフロントエンドの垣根なく、スキーマが定義できれば理想的です。

合意を取った上で実装する

スキーマを定義したらレビューを行い、合意を取ります。合意が取れたら、定義したスキーマを基に中身を実装していきます。

以上です、簡素ですね。そのうちモックサーバーを用意したりするかもしれませんが、今のところスキーマ定義だけで実装を進められています。GraphQLのスキーマは型の定義があるので基本的なAPIの仕様はスキーマを見れば理解できますし、何か分からないところがあればお互いにいつでも聞けるようにしています。

スキーマファーストにして良かったこと

当たり前ですが、スキーマファーストにしたことでフロントエンドの開発がスタックすることは減りました。また、スキーマを先に議論できているので、実装してから「ここはこうじゃないと思うんですが...」みたいになることはなくなり、バックエンドも中身の実装に集中することができるようになりました。バックエンド的にはAPIを実装する前にモデルやその他ロジックを書かなければならないことも多いので、スキーマとそれらを分けられたのは開発の柔軟性を高めてくれています。

その上で実装とスキーマの乖離をなくしたい

スキーマファーストで開発するようになって良くなったことも多いのですが、やはり気がかりなのは「スキーマと実装が正しいかのテスト」がないことです。スキーマを先に定義しているので完全に一致していることをテストするのは不可能なのですが、バックエンドが実際に書いたAPIと事前に定義したスキーマが正しく紐付いているかどうかは確認しやすい方がいいです。

そのために、一度消したスキーマのテストを今までと異なる形で復活させました。

どうしたかというと、普段使う schema.graphql とは別に、テストで使う用の schema.graphql を spec/graphql/ 以下に出力するようにしました。そこにはバックエンドで実装済みのスキーマのみをdumpするようにします。このスキーマが常にAPIが実装済みのスキーマの最新になるように、テストを修正しました。

Rakeタスクを以下のように修正します。

desc 'Dump schema.graphql under spec/ dir'
namespace :spec do
  GraphQL::RakeTask.new(schema_name: 'GraphqlRubyDemoSchema', directory: Rails.root.join('spec', 'graphql'))
end

テストはこんな感じ。

require 'rails_helper'

RSpec.describe 'GraphqlRubyDemoSchema' do
  let(:current_definition) { GraphqlRubyDemoSchema.to_definition }
  let(:printout_definition) { File.read(Rails.root.join('spec', 'graphql', 'schema.graphql')) }  # ここだけ修正

  it 'equals dumped schema, `rake graphql:schema:dump` please!' do
    expect(current_definition).to eq(printout_definition)
  end
end

この「スキーマ(実装済み)」があることで、colordiff コマンドなどでスキーマ(未実装)との差分を取ることができます。それにより、スキーマ(実装済み)がスキーマ(未実装)と乖離していないかや、これから実装するスキーマをサッと確認することができます。

f:id:kielze:20171225232022p:plain

よくあるのが ! のつけ忘れで、オプショナルでない方がよいfieldを null able にしてしまうことがあります。RubyのDSLを書いているとやりがちなミスなのですが、完全に目grapだと見逃しがちなので大変助かります。

f:id:kielze:20171225232017p:plain

現状はここまでです。

これからやりたいこと

これでもだいぶ助かっているのですが、1つ問題があります。それは、スキーマの内容が合っていたとしても、手書きしたスキーマと自動生成したスキーマのフォーマットが異なるとそれが差分として出てしまうということです。これを解決するためにはRubyのDSLなどでスキーマを自動生成するか、手書きした schema.graphql をフォーマッタか何かにかけてフォーマットを揃える、ということが必要です。

前者はすでにできるのですが、できれば後者の方もできるようにしたいです。後者の場合、おそらく一度 schema.graphql を読み込み、その定義を基に自動生成したスキーマで上書きするような仕組みがあればいけそうです。探してみたのですが、そのようなことをしてくれるライブラリは今のところ見当たりませんでした。

もしそのようなライブラリがあれば、スキーマのテストと同じようにしてスキーマのフォーマットのテストをすることができます。CIと組み合わせれば、手書きのスキーマであっても常に機械的なフォーマットが施されたスキーマを維持することもできそうです。

そこで graphql-rubyのコードを使えないかなと思い調べてみたのですが、やりたいことができそうなコードを見つけました。Railsのルートディレクトリに schema.graphql がある場合、以下のコードでそれを読み込んでスキーマを再度生成し、フォーマットを揃えた上で文字列を出力することができます。

GraphQL::Schema.from_definition(Rails.root.join('schema.graphql').to_s).to_definition

まだ何か作ったわけではないのですが、今後これを使ってスキーマのフォーマットができる仕組みを作りたいなーと考えています。うまくできたらまた報告します!