🚂

RailsでGraphQL APIを実装するとき、Railsエンジンを使うと便利

2022/12/21に公開

はじめに

マイベストでフロントエンドエンジニアをしているteppeitaです
(今回はRailsの話ですが、業務でRailsのコードも書いています)

マイベストではGraphQLを採用していて、バックエンドをRailsで実装しており、ライブラリとしてはgemのgraphql-rubyを利用しています。

https://github.com/rmosolgo/graphql-ruby

(graphql-rubyの詳細について書き始めると長くなってしまうので割愛します)

GraphQLの設計に関して色々と試行錯誤している中で、Railsエンジンと組み合わせるとコードを柔軟に分割できそうだ、という発見が有ったのでその話をしたいと思います。

Railsエンジンとは

https://railsguides.jp/engines.html

Railsエンジンについては、簡単に説明すると、メインのRailsアプリケーション内に、別の名前空間で、MVCを含むアプリケーションを用意することができるもので、
例えば管理画面をエンジンで作ることによって、管理画面内でのみ共通で使いたい処理をエンジンの中にまとめることができたりします。
(※使いすぎには注意です)
https://techracho.bpsinc.jp/hachi8833/2022_07_21/119838

Railsエンジンを使った分割

Railsエンジンを使うと名前空間を分離できます。
これがGraphQLを使っていて困ることに対して有効に使えるケースが有るので紹介します。

GraphQLで困ること

例えば、同じArticleで、Webとアプリでタイトルを変えたいことが有ります。(WebはSEO用のキーワードを入れたい、など)
あるいは、管理画面では入力が無ければ空にしたいけど、ユーザーが見る画面(以下フロント画面と呼びます)ではデフォルトタイトルを表示したいケースなど。

こんな時は名前空間が使えれば良いなと思います。

ところで、Production Ready GraphQLという良書が有るのですが、
そこでは「命名を頑張れば、名前空間はほとんど必要ない」と書かれていました。(超意訳)

(どんな本なのかは以下の記事に詳しいです)
https://zenn.dev/adwd/articles/4b1e38796bd763

確かに命名が上手くできれば(=それぞれのfieldに対して適切で具体的な名前をつけられれば)、問題にはならなそうです。
が、ゆるふわな命名をしてしまうとどうなるかというと、、、

class ArticleType < BaseType
  field :title, String, null: true # 管理画面用
  field :title_for_web, String, null: false
  field :title_for_mobile, String, null: false
end

こうなります。
実装当初は問題無いかもしれません。要求に従って必要なfieldを定義しただけなので、余計な混乱は起きづらいです。
しかし、時が経って別の要求によりこのfieldを使いたい時に混乱します。
例えば「新しく作ったリストに、タイトルを表示したいからtitleを使ったらnullが返ってきた」というような問題が起きます。

そして残念なことに、命名スキルや時間の問題で、こうした命名をしてしまうことも往々にしてあるのではないでしょうか。(私はあります😇)

そこで、残念な命名スキル 時間の問題を補うために、Railsエンジンを使った分割をするとどうなるか紹介します。

エンドポイントを分ける

まずRailsエンジンの名前空間ごとに、エンドポイントを分けることができます。
エンジン上にcontrollerを作成し、そこから呼び出すschemaを専用のものにすることで、エンドポイントごとに呼び出せるroot queryを変えることができます。
/graphql/admin/graphqlのように)

module Admin
  class GraphqlController < ApplicationController
    def execute
      ...
      result = Admin::HogeSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
      ...
    end
  end
end
module Admin
  class HogeSchema < GraphQL::Schema
    query(ObjectTypes::AdminQueryType)
    mutation(ObjectTypes::AdminMutationType)
  end
end

これによって管理画面とそうでないフロント画面で処理を分けることができるのですが、
例えば以下のようなことが名前空間ごとに簡単に実現できます。

  • フロント画面が表示専用な場合は、mutationを実行できないようにする
  • フロント画面でのみURLのパラメータによる分岐処理を追加する
  • base_resolverをそれぞれ作成し、管理画面でのみ認証を必須にする

typesを分ける

typesもエンジンごとに分けることができます。
先述のfield命名に関しても以下のように分けることができます

module Admin
  class ArticleType < BaseType
    field :title, String, null: true
  end
end
module Front
  class ArticleType < BaseType
    field :title, String, null: false # 空の時はデフォルト値を返す
  end
end

これで先ほどの命名問題は解決しました(?)

一方で、typesをメインのアプリケーション(/app)に置くことで、共通化する、という選択肢も有ります。
例えば、schemaやroot queryは別、root fieldとそのresolverは別、object_typesは共通、
というように、共通部分と分離する部分を柔軟に設計可能です。

まとめ

Railsエンジンを使うとshcemaを柔軟に分割できます。
一方で、GraphQLはなるべく分割しない方が良い、命名を頑張れば分割しなくても運用できる、という話も有ります。
私としては、組織によっては命名が上手くいかないのっぴきならない事情も有ると思うので、そういった場合には、名前空間の分離によって腐敗を防ぐのも現実的・合理的だと思います。

本記事では、そういったケースに対応するためにRailsエンジンという手段が有りますよ、という紹介をしました。
(※もちろん、良い命名ができるように努力し続けるのは必須で、「名前重要」がエンジニアの基本姿勢だと思っています。)

弊社では現在その辺りのschema設計をどうしていくべきなのか運用しながら試行錯誤中ですが、良い形が見つかればまた記事にしたいと思います。

Discussion