その他
    ホーム技術発信Ruby on RailsエンジニアがGraphQLを使うと簡単に最強のWebAPIを作れる話。(デモもあるよ!)

    Ruby on RailsエンジニアがGraphQLを使うと簡単に最強のWebAPIを作れる話。(デモもあるよ!)

    API、好きですか?!

    データイノベーション部 AI-labo所属の吉岡です。
    私は今入社3年目で、入社当初からRuby on Railsを使っていました。
    入社2年目から配属してるプロジェクトでは、Rails + GraphQLでバックエンド開発をしております。
    過去に自分は趣味で色んなWebAPIを触ってきましたが、GraphQLはすごく画期的です。
    Railsに慣れたエンジニアがWebAPIを開発しようと思ったとき、GraphQLを使うとこういう利点があるよ!というのを伝えたいと思います。

    こういう人に読んでもらいたい!

    主に以下に当てはまる人は、より楽しめる記事になっているかと思います!

    • Ruby on Railsのフロント・バックエンドの開発をした経験があり、controllerやmodelの概念がある程度わかっている人
    • Web APIやREST APIについてある程度どんなものかわかっている人
    • バックエンドをAPIにして、フロントエンドをReactやVueなどを使ってスマホアプリを作ってみたい人

    GraphQLについて

    GraphQLとは、Facebook社が開発したAPI向けのクエリ言語です。
    ……といっても私自身が言葉的にそんなに深く理解しているわけではないので、実際何ができるかを中心に説明して行きたいと思います。

    例えばこういうことができる

    以下のようなカラム構成のテーブルがあると想定します。
    users テーブル

    • id
    • name
    • created_at
    • updated_at

    articles テーブル

    • id
    • user_id
    • body
    • created_at
    • updated_at

    comments テーブル

    • id
    • article_id
    • user_id
    • text
    • created_at
    • updated_at

    もし、あるページであるuserのname、そのユーザに紐づいたarticlesのbodyとcreated_at、さらにそのarcitlesに紐づいたcommentsのtextを取るページがあったとき、どのようにAPIを実装・取得しますか?

    この条件で取得できるようなAPIを、例えばRESTで実装するとき、いくつか懸念点が出てくると思います。

    • users、articles、commentsを取る時にそれぞれリクエストを投げるか?
    • 1回のリクエストをするとして、そのためのエンドポイントを新たに作らなければいけないのか?
    • articlesやcommentsの全カラムを取得するAPIはあるが、使わないカラムのデータも受け取らなければいけないのか?
    • もしこのページで他のデータが必要になったときに、どう対応するか?

    Railsでフロント込みの開発に慣れていると「N+1にならないようにしなきゃ……」くらいの懸念点になるかと思います。
    しかしフロントエンドを別物として開発する場合、フロント側にはmodelという概念はないため、APIにする場合は上記のようなことも重要になってきます。

    そんなとき、GraphQLは以下のようなqueryを書けば1発で解決できちゃうんです。

    query {
      user(id: 1){
        articles{
          body
          createdAt
          comments{
            text
          }
        }
      }
    }

    「えっそんな簡単にできるの?」「実際どう投げるのか教えてもらわないとよくわかんない」「queryって何?」っていう人の為に、もう少し詳細に説明したいと思います!

    どう使うの?

    APIを使う上で最初に知りたいのってやはりエンドポイントですよね。
    GraphQLのエンドポイントは、以下のようになります。

    〇〇.com/graphql

    「え?これだけ?これで情報どうやって取るの?」って思うかもしれません。
    実は、やってる内容的にはGETなのですが、GraphQLの場合は全部POST形式で投げています。
    1つのエンドポイントは固定で、リクエストbodyに上記のqueryを入れてPOSTで投げます。
    リクエストに含まれるqueryをrails側で解釈し、queryに指定されている内容をレスポンスするという形です。

    メリット

    ここまで軽くGraphQLについて説明してきましたが、上のことを踏まえると以下のようなメリットが見えてきます。

    • エンドポイントが1つで済む。
    • 欲しい情報だけを取得することができる。
    • テーブル構成が複雑な場合でも1回のリクエストで様々な情報を取りきることができる。

    中でもエンドポイントが1つで済むのは一番の利点だと思っていて、バックエンド側から返す情報が変更もしくは新しく増えたときでも、フロントエンドではGraphQLのqueryの記述を変更すれば良いだけになります。

    ローカル環境・本番環境での確認に役立つツールがあって便利!

    GraphQLには、ローカル環境での確認に便利な GraphiQL という機能があります。
    これはローカルでサーバを立ち上げ、 localhost:3000/graphiql/ にアクセスすると、queryを試せるという機能になっています。(urlは初期設定で、変更もできます。)

    また、chromeの拡張機能に Altair GraphQL Client というものがあります。
    こちらはGraphiQLと同じ機能を本番環境等で実際に試すことができます。
    また、Altairではリクエストヘッダの設定もできますので、ヘッダの情報が必要な場合にも対応できます!

    RailsでGraphQLを動かしてみる!!

    GraphQLが便利なことはわかった……けど、実装が難しかったら使いたくないですよね。
    Railsに触れてたユーザがGraphQLを使用するのに必要なことは以下の4つです。

    • gemが追加できること
    • controllerを作成して、ルーティングできること
    • modelが作れること
    • hashが書けること

    はい、これだけでできます。
    Rails チュートリアルで学習する内容さえあれば完ペキにAPIを作ることができます。

    1. まずは環境構築する

    どのような形でも良いので、まずはRailsのアプリケーションを立ち上げます。
    gemさえ入れれば既存のRailsプロジェクトでも動きますので、とりあえず立ち上げについては省略します。
    rails s が動作する状態にしてから以下の手順を実行してみてください。

    Railsのアプリケーションのディレクトリに入り、Gemfileに以下を追加します。

    gem 'graphql'          # <- 追加
    group :development do
      gem 'graphiql-rails'  # <- developmentのgroup内に追加
    end

    インストールを実行します。

    bundle install
    bundle exec rails g graphql:install

    そうすると以下が追加されているかと思います

    app/graphql
    ├── {アプリケーション名}_schema.rb
    ├── mutations
    └── types
        ├── mutation_type.rb
        └── query_type.rb

    また、config/rountes.rb に以下のような記述が追加されているかと思います

      if Rails.env.development?
        mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
      end
      post "/graphql", to: "graphql#execute"

    これでGraphQLを使う準備が出来ました!
    試しにrails sを立ち上げ、 http://localhost:3000/graphiql を立ち上げると先ほどのGraphiQLの画面が出てくるかと思いますので、以下のqueryを記述して実行してみてください。

    {
      testField
    }

    以下のようなレスポンスが返ってきたら成功です!

    {
      "data": {
        "testField": "Hello World!"
      }
    }

    エラー注意!

    rails・graphqlのバージョンによっては app/assets/config/manifest.js を以下のように修正する必要があります。
    もし Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show のようなエラーが出たら修正してみてください。

    //= link graphiql/rails/application.css
    //= link graphiql/rails/application.js

    2. queryの構造についての説明

    早速queryを書いていきますが、先にqueryについての説明をします。
    queryは基本以下の構造になります。

    query queryの名前 {
      fieldName1
      fieldName2(arg: argument)
      fieldName3 {
        fieldName3-1
        fieldName3-2
      }
    }
    • query: queryかmutationを指定します。
    • query: 情報を取得するためのもの
    • mutation: 情報を更新するためのもの(本稿では説明しません)
    • queryの名前: queryを書く人(フロント開発者など)が分かりやすくするためにつけるもの
    • fieldName: 何の情報を取るかを指定する(階層構造にできる)
    • arg: 引数指定ができます。

    注意!

    以下でrailsから定義していきますが、rubyファイルでの定義はスネークケース、queryの記述ではローワーキャメルケース(先頭だけ小文字のキャメルケース)で書きます!
    ごっちゃにならないように注意してください!

    筆者もよくqueryをスネークケースで書いてエラーに苦しんだりしてます。
    エラーが出たら、まずは書き方を疑ってみてください。

    3. とりあえず何かを返すqueryを作る

    とりあえずGraphQLを導入したので、まずは簡単なqueryを作成してみたいと思います。
    以下のような情報を返したいとします。

    query todayInfo {
      currentDate       # 今日の日付
      todayWeather {    # 今日の天気
        weather            # 天気
        temperature        # 温度
      }
    }

    先ほど作成された app/graphql/types/query_type.rb に以下の行を追加します。

    module Types
      class QueryType < Types::BaseObject
        ### (省略) ###
    
        ##### ここから追加
        field :current_date, String, null: false, description: '今日の日付'
        def current_date
          Date.today.strftime("%Y年 %m月 %d日")
        end
    
        field :today_weather, WeatherType, null: false, description: '今日の天気'
        def today_weather
          { weather: '晴れ', temperature: 60 }
        end
        ##### ここまで追加
      end
    end

    そして、 app/graphql/types/weather_type.rb を作成し、以下のコードを書いてください(丸コピで大丈夫です!)

    module Types
      class WeatherType < Types::BaseObject
        field :weather, String, null: false, description: '天気'
        def weather
          object[:weather]
        end
    
        field :temperature, Int, null: false, description: '温度'
        def temperature
          object[:temperature]
        end
      end
    end

    上記のコードの解説です!
    まず以下の1行でfieldの定義をします。

    field :query名, 型, null: nullを許可するか, description: 'クエリの説明'

    そして、query名と同じ名前で定義されたメソッドを リゾルバ(resolver) といい、このメソッド内の内容を実際にqueryで返します。

    def query名
      返す値を記述
    end

    つまり、current_dateは以下のように解釈されます

    • field名はcurrent_date(GraphQL上では currentDate )
    • String型
    • nullは許可していない
    • 返ってくる値は Date.today.strftime("%Y年 %m月 %d日") になる

    続いて、todayWeatherの解説です。
    最初の方でも述べましたが、GraphQLは階層構造で返すことができます。
    これを実現するためには、fieldの中にfieldがあって……という構造にする必要があります。
    先ほど、型はStringで指定していましたが、自分で作った型を指定することもできます。
    以下の1行ではWeatherTypeを指定しています。

    field :today_weather, WeatherType, null: false, description: '今日の天気'

    WeatherTypeは weather, temperature という2つのfieldがありますので、 todayWeather のfieldでは weather, temperature という情報を取ることができます。

    また、作った型を指定したときは、リゾルバの内容が型に渡されます

    field :today_weather, WeatherType, null: false, description: '今日の天気'
    def today_weather
      { weather: '晴れ', temperature: 60 } # <- この値がWeatherTypeに渡される
    end

    渡されたリゾルバの内容は、 object という変数に入っています。

    field :weather, String, null: false, description: '天気'
    def weather
      object[:weather] # <- objectの中身は { weather: '晴れ', temperature: 60 }
    end

    この状態でrails sでサーバを立ち上げてGraphiQL( http://localhost:3000/graphiql )にアクセスします。
    左側に以下の記述をして、画面上部の実行ボタン(右向き三角のボタン)を押してください。

    query todayInfo {
      currentDate       # 今日の日付
      todayWeather {    # 今日の天気
        weather            # 天気
        temperature        # 温度
      }
    }

    そうすると以下のようなレスポンスが返ってきます。

    {
      "data": {
        "currentDate": "2020年 09月 30日",
        "todayWeather": {
          "weather": "晴れ",
          "temperature": 60
        }
      }
    }

    これがGraphQLの基礎になります。

    余裕があったら!

    today_weatherのリゾルバ内の記述を変えてみてください!

    def today_weather
      { weather: '雨', temperature: 10 } # <- この値がWeatherTypeに渡される
    end

    レスポンスが以下のように変わるはずです!

    {
      "data": {
        "currentDate": "2020年 09月 30日",
        "todayWeather": {
          "weather": "雨",
          "temperature": 10
        }
      }
    }

    4. 引数と複数件の情報

    情報を取るのに配列や引数が必要になってきます。
    以下のようなqueryを想定しましょう

    query monthInfo{
      month(monthNum: 1) { # 月の情報(月指定で1件)
        name                  # 英名
        days                  # 日数
      }
      months {             # 月の情報(全件)
        name                  # 英名
        days                  # 日数
      }
    }

    こちらのqueryは、現在日と月の英名およびその月の日数を返す想定です。

    先ほど作成された app/graphql/types/query_type.rb に以下の行を追加します。

    module Types
      class QueryType < Types::BaseObject
        ### (省略) ###
    
        ##### ここから追加
        field :month, MonthType, null: false, description: '月の情報(月を指定して1件)' do
          argument :month_num, Int, '月の数字', required: true
        end
        def month(month_num:)
          month_hash_array[month_num - 1]
        end
    
        field :months, [MonthType], null: :false, description: '月の情報(全件)'
        def months
          month_hash_array
        end
    
        # 返すためのデータ
        def month_hash_array
          [
            { name: 'January',   days: 31 },
            { name: 'February',  days: 28 },
            { name: 'March',     days: 31 },
            { name: 'April',     days: 30 },
            { name: 'May',       days: 31 },
            { name: 'June',      days: 30 },
            { name: 'July',      days: 31 },
            { name: 'August',    days: 31 },
            { name: 'September', days: 30 },
            { name: 'October',   days: 31 },
            { name: 'November',  days: 30 },
            { name: 'December',  days: 31 }
          ]
        end
        ##### ここまで追加
      end
    end

    そして、 app/graphql/types/month_type.rb を作成し、以下のコードを書いてください(こちらも丸コピで大丈夫です!)

    module Types
      class MonthType < Types::BaseObject
        field :name, String, null: false, description: '英名'
        def name
          object[:name]
        end
    
        field :days, Int, null: false, description: '日数'
        def days
          object[:days]
        end
      end
    end

    引数の付け方

    まずは引数の付け方の解説です。
    fieldに augument を定義します。(ブロック内で指定しています。)

    field :month, MonthType, null: false, description: '月の情報(月を指定して1件)' do
      argument :month_num, Int, '月の数字', required: false
    end

    引数の指定については以下のようになっています。
    Stringを指定すれば文字列にできますし、引数用の型を定義すれば複雑な引数指定もできます。(本稿では省略します。)

    argument :引数名, 型, '説明', required: 必須にするか

    続いてリゾルバについてです。
    引数を指定したfieldについてはキーワード引数としてリゾルバを定義すれば、引数の内容が取得できます。
    (※ 1月と指定した場合、1月の情報はmonth_hash_array[0]にあるので、 month_num - 1 になっています。)

    def month(month_num:)
      month_hash_array[month_num - 1]
    end

    配列を返す

    続いて配列の返し方です。
    複数件のレスポンスになるfieldは、以下のように型指定時に [] で囲むだけで、配列として返すことができます。

    field :months, [MonthType], null: :false, description: '月の情報(全件)'
    def months
      month_hash_array
    end

    上記の例だと、 { name: '月名', days:日数} の塊が、いくつもある形のレスポンスができます。
    また、全ての型を配列化ができます
    例 )

    • [‘いぬ’, ‘ねこ’, ‘アルパカ’] -> 型を [String] で指定する。
    • [10, 23, 37] -> 型を [Int] で指定
    • [{ weather: ‘晴れ’, temperature: 40 }, { weather: ‘雨’, temperature: 10 }] -> 型を [WeatherType] で指定( WeatherTypeについては前項を参照 )

    実行してみる!

    先ほどと同様にrails sでサーバを立ち上げてGraphiQL( http://localhost:3000/graphiql )にアクセスします。
    左側に以下の記述をして、画面上部の実行ボタン(右向き三角のボタン)を押してください。

    query monthInfo{
      month(monthNum: 1) { # 月の情報(月指定で1件)
        name                  # 英名
        days                  # 日数
      }
      months {             # 月の情報(全件)
        name                  # 英名
        days                  # 日数
      }
    }

    以下のようなレスポンスが返ってくると思います。

    {
      "data": {
        "month": {
          "name": "January",
          "days": 31
        },
        "months": [
          {
            "name": "January",
            "days": 31
          },
          {
            "name": "February",
            "days": 28
          },
          {
            "name": "March",
            "days": 31
          },
          {
            "name": "April",
            "days": 30
          },
          {
            "name": "May",
            "days": 31
          },
          {
            "name": "June",
            "days": 30
          },
          {
            "name": "July",
            "days": 31
          },
          {
            "name": "August",
            "days": 31
          },
          {
            "name": "September",
            "days": 30
          },
          {
            "name": "October",
            "days": 31
          },
          {
            "name": "November",
            "days": 30
          },
          {
            "name": "December",
            "days": 31
          }
        ]
      }
    }

    これでqueryはバッチリですね!

    余裕があったら

    引数を変えてみてください!
    情報が変わります!

    query monthInfo{
      month(monthNum: 5) { # <- 変えてみる
        name
        days
      }
      months {
        name
        days
      }
    }

    6. モデルの内容を返すqueryを書く

    ※ 本稿ではモデルについての詳細な説明は省略します。modelやデータは自分で作ってみてください!

    例えば、以下のようなモデルがあったとします。
    Userモデル(id以外はstring)

    • id
    • last_name
    • first_name
    • profile
    • created_at
    • updated_at

    そして以下のようなqueryを作りたいとします。

    query {
      user(id: 1) { # Userモデルのデータ1件取得
        full_name     # last_name と first_nameを半角スペースで繋げたもの
        profile       # profileのデータをそのまま
        created_at    # created_atの年月日までの情報
      }
      users {       # Userモデルのデータを全部取得
        full_name     # last_name と first_nameを半角スペースで繋げたもの
        profile       # profileのデータをそのまま
        created_at    # created_atの年月日までの情報
      }
    }

    その場合、以下のようにTypeを定義します。
    app/graphql/types/query_type.rb に以下を記述します。

    module Types
      class QueryType
        ### (省略) ###
    
        ##### ここから追加
        field :user, UserType, null: false, description: 'ユーザ情報(id指定で1件)' do
          argument :id, Int, 'ユーザid', required: true
        end
        def user(id:)
          User.find_by(id: id)
        end
    
        field :users, [UserType], null: false, description: 'ユーザ情報(全件)'
        def users
          User.all
        end
        ##### ここまで追加
      end
    end

    app/graphql/types/user_type.rb に以下を記述します。

    module Types
      class UserType
        field :full_name, String, null: false, description: '姓名'
        def full_name
          object.last_name + ' ' + object.first_name
        end
    
        field :created_at, String, null: false, description: '作成日時'
        def created_at
          object.created_at.strftime("%Y年 %m月 %d日")
        end
    
        field :profile, String, null: false, description: 'プロフィール'
        def profile
          object.profile
        end
      end
    end

    解説!

    先ほどはhashで指定しましたが、リゾルバから型に送るもの(object)はhashでもインスタンスでもActiveRecord::Relationでもいけます!
    以下の2つのリゾルバを見てください。
    このように指定すれば、1件の方には User.find_by(id: id) したレコードを、全件の方には User.all のRelationを入れることができます。

    field :user, UserType    ### 省略 ###
    def user(id:)
      User.find_by(id: id)
    end
    
    field :users, [UserType] ### 省略 ###
    def users
      User.all
    end

    Userインスタンスについても、objectに入ります。
    なので以下のリゾルバではobjectの中にはUserのインスタンスが入っています。

    field :full_name, String, null: false, description: '姓名'
    def full_name
      object.last_name + ' ' + object.first_name
    end

    実行してみよう!

    以下のqueryを実行してみましょう!
    成功したら、成功です!(細かいレスポンスは省略します)

    query {
      user(id: 1) { # Userモデルのデータ1件取得
        full_name     # last_name と first_nameを半角スペースで繋げたもの
        profile       # profileのデータをそのまま
        created_at    # created_atの年月日までの情報
      }
      users {       # Userモデルのデータを全部取得
        full_name     # last_name と first_nameを半角スペースで繋げたもの
        profile       # profileのデータをそのまま
        created_at    # created_atの年月日までの情報
      }
    }

    7. 便利な術!

    GraphQLの記述を書くのに当たって便利な技を紹介します。

    処理しなくて良いものはリゾルバが不要!

    例えば先ほどのWeatherTypeをご覧ください。

    module Types
      class WeatherType < Types::BaseObject
        field :weather, String, null: false, description: '天気'
        def weather
          object[:weather]
        end
    
        field :temperature, Int, null: false, description: '温度'
        def temperature
          object[:temperature]
        end
      end
    end

    weatherのfieldでは object[:weather] を返していますが、hashまたはインスタンスで何も成形が不要なときはリゾルバの定義が不要になります。

    なので、WeatherTypeは以下のように書き換えることができます。

    module Types
      class WeatherType < Types::BaseObject
        field :weather, String, null: false, description: '天気'
        field :temperature, Int, null: false, description: '温度'
      end
    end

    スッキリしていいですね!

    UserTypeいついても同様です!

    module Types
      class UserType
        field :full_name, String, null: false, description: '姓名'
        ## 成形しているため、省略不可能
        def full_name
          object.last_name + ' ' + object.first_name
        end
    
        field :profile, String, null: false, description: 'プロフィール'
        ## 成形していないため、省略可能
        def profile
          object.profile
        end
      end
    end

    profileは何も成形していないので、以下のように書き換えることができます。

    module Types
      class UserType
        field :full_name, String, null: false, description: '姓名'
        ## 成形しているため、省略不可能
        def full_name
          object.last_name + ' ' + object.first_name
        end
    
        field :profile, String, null: false, description: 'プロフィール'
      end
    end

    ちなみに、 full_name をUserモデルのインスタンスメソッドにした場合は object.full_name という形にできるので、 full_name のリゾルバも省略することができます!

    module Types
      class UserType
        field :full_name, String, null: false, description: '姓名'
        ## full_nameをUserのインスタンスメソッドにした場合は、full_nameも省略可能
        # def full_name
        #   object.fullname
        # end
    
        field :profile, String, null: false, description: 'プロフィール'
      end
    end

    いいですね!!

    Loaderを使って、N+1問題を解決!

    最初に挙げたこのテーブル構造を思い出してください。

    users テーブル
    - id
    - name
    - created_at
    - updated_at
    
    articles テーブル
    - id
    - user_id
    - body
    - created_at
    - updated_at
    
    comments テーブル
    - id
    - article_id
    - user_id
    - text
    - created_at
    - updated_at

    これを実装する場合、 user.articles の中で article.comments を普通に呼んでしまうと、N+1問題が発生します。
    いくら1回のqueryで呼べたとしても、N+1が発生しては意味ないですよね?

    そんなときにはLoaderを指定します。
    loaderには association_loader.rb というhas_manyの子レコードに対するloaderと record_loader というbelongs_toの親レコードに対するloaderの2つが存在します。

    本稿では詳しく説明しませんが、 graphql-batch というgemを入れ、 graphql/loader/ 配下にそれぞれのファイルを入れれば使えます。
    複雑なテーブル構造で、データを呼びたいときは試してみてください。

    これでもう最強ですね!

    最後に

    Railsを使ってきた人ならきっとgraphql-rubyを使いこなせると思います。
    GraphQLは「AWS AppSync」というAWSのサービスでも採用されていて、主流なクエリ言語になりつつあると考えています!
    RailsエンジニアでWebAPIやスマホやタブレットなどのアプリケーションを作りたいと考えている方、その知識を使って便利なAPIを作ってみませんか?