Rails・GraphQL基礎 – データ更新 & 認証編

ここでは、Rails での GraphQL の基本的な使い方を整理していきます。

内容は、Udemy『Basics of GraphQL with Ruby on Rails』のセクション3「Changing Data」とセクション4「Authentication, Authorisation, and Access Control」を参考にしています。

詳細はそちらをご参照ください。

Changing Data

データを更新する処理は、GraphQLでは「Mutations(ミューテーションズ)」と読んでいます。

リクエストとレスポンス

リクエストするエンドポイントは、前述のとおり1つで、「root_domain/graphql」のみです。

リクエストで送るGraphQLクエリは次のような感じです。

mutation {
  retePost(postId: 1, stars: 5) {
    stars
  }
}

mutation_type.rb

実装は「types/mutation_type.rb」に field を書いていきます。

例えば、Authorを新規作成するデータ更新(createAuthor)であれば、次のようになります。


# types/mutation_type.rb
class Types::MutationType < Types::BaseObject

  field :create_author, Types::AuthorType, mutation: Mutations::CreateAuthor
end

ポイントは「必要なパラメータ」と「処理」を「mutation: Mutations::CreateAuthor」で定義している点です。

「types/mutation_type.rb」に直書きするやり方もありますが、処理が増えるとごちゃごちゃするので「mutations配下」に書いていく設計が一般的です。

mutations

今回の例(createAuthor)だと、mutations配下に「create_author.rb」ファイルを作成し、「必要なパラメータ」と「処理」を次のように書いていきます。


# mutations/create_author.rb
class Mutations::CreateAuthor < GraphQL::Schema::Mutation
  null true

  argument :first_name, String, required: false, camelize: false
  argument :last_name, String, required: false, camelize: false
  argument :yob, Int, required: false
  argument :is_alive, Boolean, required: false, camelize: false

  def resolve(first_name:, last_name:, yob:, is_alive:)
    Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
  end
end

「argument」は引数の定義です。

mutations配下のファイルでは「resolveメソッド」に必要な処理を記載します。

実際のリクエストは次のようになります。

実際のリクエスト

必要なパラメータは、GraphQL文に直書きすることもできますが、このように「GraphQL文」と「変数」を切り分けるために、変数はJSONで渡すのが一般的です。

Input Types

上のGraphQL文「mutation createAuthor」は、引数の書き方が冗長です。

これを簡潔化するのが「Input Types」という設計です。

まず「types/author_type.rb」の「Types::AuthorType」の上部に「Types::AuthorInputType」を定義します。


class Types::AuthorInputType < GraphQL::Schema::InputObject
  graphql_name "AuthorInputType"
  description "All the attributes for creating an author"

  argument :id, ID, required: false
  argument :first_name, String, required: false, camelize: false
  argument :last_name, String, required: false, camelize: false
  argument :yob, Int, required: false
  argument :is_alive, Boolean, required: false, camelize: false
end

class Types::AuthorType < Types::BaseObject
  description "An author"

  field :id, ID, null: false
  field :first_name, String, null: true, camelize: false
  field :last_name, String, null: true, camelize: false
  field :yob, Int, null: false
  field :is_alive, Boolean, null: true, camelize: false
  field :full_name, String, null: true, camelize: false

  def full_name
    ([object.first_name, object.last_name].compact).join" "
  end

  field :coordinates, Types::CoordinatesType, null: false

  field :publication_years, [Int], null: false
end

そして先ほどの「mutations/create_author.rb」を次のように更新します。


class Mutations::CreateAuthor < GraphQL::Schema::Mutation
  null true

  argument :author, Types::AuthorInputType, required: true

  def resolve(author:)
    Author.create author.to_h
  end
end

こうすると、必要なパラメータは「Types::AuthorInputType」を参照することになります。

その結果、リクエストは次のようになります。

mutation createAuthor($author: AuthorInputType!) {
  createAuthor(author: $author) {
    id
    full_name
  }
}
{ 
  "author": {
    "first_name": "Keisuke",
    "last_name": "Inaba",
    "yob": 1988,
    "is_alive": true
  }
}

リクエスト

シンプルになりましたね!

更新・削除の例

新規作成以外に、更新・削除のコード例もみておきましょう。

以下の例では「mutaion_type.rb」に実装を直書きしています。


class Types::MutationType < Types::BaseObject

  field :create_author, Types::AuthorType, mutation: Mutations::CreateAuthor

  field :update_author, Boolean, null: false, description: "Update an author" do
    argument :author, Types::AuthorInputType, required: true
  end

  def update_author(author:)
    existing = Author.where(id: author[:id]).first
    existing&.update author.to_h
  end

  field :delete_author, Boolean, null: false, description: "Delete an author" do
    argument :id, ID, required: true
  end

  def delete_author(id:)
    Author.where(id: id).destroy_all
    true
  end
end

ポイントとしては、「update_author(更新)」「delete_author(削除)」共に、「Boolean」のみを返すようにしています。

これは一般的な設計で、リクエスト側はどのデータを更新 or 削除したか知っているので、処理結果だけ返してあげれば十分だからです。

ActiveRecordのエラーを活用

GraphQLデフォルトのエラーメッセージは分かりにくいので、ActiveRecordのエラーを活用したカスタマイズをしてみます。

まず AuthorType に errors フィールドを追加します。


class Types::AuthorType < Types::BaseObject
  description "An author"

  field :errors, [Types::ErrorType], null: true

  def errors
    object.errors.map { |e| {field_name: e, errors: object.errors[e] }}
  end
end

そして「types/error_type.rb」を新規作成します。


class Types::ErrorType < Types::BaseObject
  description "An ActiveRecord error"

  field :field_name, String, null: false, camelize: false
  field :errors, [String], null: false
end

これで、エラーが出た時は、ActiveRecordのエラーが表示されます。

Authentication, Authorisation, and Access Control

最後に認証や権限管理などの実装をみていきます。

実務上は、APIを公開する上で一番重要なポイントですね。

セキュリティレベル

一般的に、APIのセキュリティレベルは次の3つに分類できます。

セキュリティレベル内容
Authentication認証キーの発行(ログイン)
Authorisation認証キーの検証
Access Controlユーザ権限の管理

一般的なフロー

一般的なAPI利用認証の流れ

上図のとおり、一般的なAPI利用認証の流れは次のとおりです。

フローフロント・バック内容
1フロント認証のリクエスト
2バックリクエスト内容の検証
3バックセッションキーの発行
4フロントセッションキーを含んだリクエスト
5バックセッションキーの検証
6バック結果をレスポンス

Authentication

それでは、ユーザ認証(ログイン管理)の実装例をみていきます。

モデルの設計

まずは必要なモデルの作成。

パスワードの暗号化には「gem 'bcrypt'」を用います。

gem 'bcrypt'
$ bundle install

Userモデルの作成。

$ rails g model User email:string password_digest:string is_super_admin:boolean

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  has_many :sessions
end

Sessionモデルの作成。

セキュリティリスクにつながるので、「key」は null にならないようにしておきましょう

$ rails g model Session user_id:integer key:string

# app/models/session.rb
class Session < ApplicationRecord
  belongs_to :user

  before_create do
    self.key = SecureRandom.hex(20)
  end
end

そして、マイグレーションの実行。

$ rails db:migrate

これで必要な準備は完了です。

ログイン

それでは、ログイン処理(keyの発行)を「query_type.rb」に書いていきます。

「login」というフィールドを作成し、Email と Password が一致するユーザが存在すれば、セッションキーを発行します。


# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
  # 省略

  field :login, String, null: true, description: 'Login a user'do
    argument :email, String, required: true
    argument :password, String, required: true
  end

  def login(email:, password:)
    if user = User.find_by(email: email)&.authenticate(password)
      session = user.sessions.create!
      session.key
    end
  end
end

テスト用のユーザをコンソールから作ります。

(irb):1:  User.create(email: "test@test.com", password: "test", is_super_admin: true)

ログインのテストをしてみると、ちゃんとレスポンスが返ってきます。

レスポンス

Authorisation

ログインで発行したキーを使って、リクエストユーザの検証をおこないます。

ログインユーザ

GraphQLでのAPIリクエストでは、すべての場合において「graphql_controller.rb」の execute メソッドが走ります。

そのため、アクセスコントロールする時に必要な「context」は、この execute メソッド内に定義していきます。

実装例は次のとおりです。


class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]

    session = Session.find_by(key: request.headers['Authorization'])

    context = {
      current_user: session&.user,
      session_id:   session&.id
    }

    result = BookshelfSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end

リクエストヘッダに有効なセッションキーが存在する場合、「context」の「current_user」と「session_id」が入ってきます。

これらの情報は、GraphQLの各ファイルから context[:current_user] や context[:session_id] で呼ぶことができます。

ログアウト

ログアウト処理は、該当するセッションキーを削除すればOKです。

実装例は次のとおりです。


# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
  # 省略

  field :logout, Boolean, null: false

  def logout
    Session.where(id: context[:session_id]).destroy_all
    true
  end
end

Access Control

Authorisation で定義した「context」を用いて、権限管理をおこなっていきます。

3つの権限管理

GraphQLでは、次の3つに分けて権限を管理していきます。

  • Visibility(閲覧権限)
  • Accessibility(アクセス権限)
  • Authorization(実行権限)

Visibility(閲覧権限)

特定の条件で任意のフィールドを非表示にします。

例えば、ログインしていないユーザには、見せたくないフィールドなどです。

次の例では、UserType の current_user フィールドを非表示にしています。


# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
  field :current_user, Types::UserType, null: true, description: 'The currently logged in user'

  def current_user
    context[:current_user]
  end
end

# app/graphql/types/user_type.rb
class Types::UserType < Types::BaseObject
  description "A user"

  field :id, ID, null: true
  field :email, String, null: true
  field :is_super_admin, Boolean, null: true, camelize: false

  def self.visible?(context)
    !!context[:current_user]
  end
end

「self.visible?」はGraphQLで用意されているメソッドです。

Accessibility(アクセス権限)

特定の条件で任意のフィールドへのアクセスを拒否します。

次の例では、スーパー管理者以外のユーザによる CreateAuthor へのアクセスを拒否しています。


# app/graphql/mutations/create_author.rb
class Mutations::CreateAuthor < GraphQL::Schema::Mutation
  null true

  argument :author, Types::AuthorInputType, required: true

  def resolve(author:)
    Author.create author.to_h
  end

  def self.accessible?(context)
    context[:current_user]&.is_super_admin?
  end
end

「self.accessible?」はGraphQLで用意されているメソッドです。

Authorization(実行権限)

特定の条件で任意の処理の実行を拒否します。

次の例では、latitude が10を上回り、longitude が10を下回る場合に、CoordinatesTypeの実行を制限しています。


# app/graphql/types/coordinates_type.rb
class Types::CoordinatesType < Types::BaseObject
  field :latitude, Float, null: true
  field :longitude, Float, null: true

  def latitude
    object.first
  end

  def longitude
    object.last
  end

  def authorized?(object, context)
    object.first > 10 && object.last < 10
  end
end

「self.authorized?」はGraphQLで用意されているメソッドです。

エラーメッセージのカスタマイズ

エラーメッセージをカスタマイズしたい場合は、「project_schema.rb」でメッセージを上書きします。

例えば次のとおりです。


class BookshelfSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

  def self.unauthorized_object(error)
    raise GraphQL::ExecutionError, "Permissions configuration do not allow the object you requested"
  end
end

GraphQLを学んだ感想

簡単で分かりやすい

GraphQLを使えば、「簡単に理解しやすいAPIを作れる」と思いました。

テストツール「GraphiQL」も分かりやすいし、エンドポイントも1つでシンプルです。

「REST API」と比較するとまだまだですが、過去5年間の検索件数(アメリカ)は増加傾向にあります。

GraphQL vs REST

色々と議論もある

枯れてる技術ではないので、色々と議論もあります。

批判的な意見もチラホラありますが、「簡単に作れて、簡単に使える」というのはGraphQLの長所と考えて良さそうです。

「スケーラビリティ」や「パフォーマンス」が短所に挙げられたりしますが、Facebook(GraphQLの発明者)が使っているので、そこら辺はなんとかなるんだと思います。

また、Facebook 以外にも、GraphQLを導入しているサービスはちょこちょこあります。

結論

次に機会があれば、勉強がてらGraphQLでAPIを作ってみてもいいかなぁと思いました。

兵庫県西宮市生まれのフリーランスRailsエンジニア。案件によってWordPressの作業も請け負ったりしてます。2014年から2016年にかけてオーストラリアで生活。 現在は東京を拠点に活動。/ 前職・資格:公認会計士 / プログラミング言語:Ruby, JavaScript, HTML, CSS / 日本語・英語
コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です