Vue×Apollo×Railsを使ってGraphQLを学んでみた

# はじめに

<a href="https://qiita.com/advent-calendar/2018/ateam-lifestyle-cyma" target="blank">Ateam Lifestyle x cyma Advent Calendar 2018</a>の25日目は、 <a href="https://life.a-tm.co.jp/" target="blank">株式会社エイチームライフスタイル</a> のエンジニア @sakupa80  が担当します。

最近よくHeadlessCMSなどで使われているGraphQLの話をよく聞くので、チュートリアルがてらに触ってみました。
バックエンドは普段は開発に使っているRailsを使用し、フロントエンド側はとっかかりやすいVue.jsを選びました。
この記事の順序どおりに進めればGraphQLの基本は学べるようにしておりますので、これからGraphQLを触ってみたいという方の参考になれば幸いです。

# 開発環境
* macOS High Sierra
* Ruby 2.5.1
* Rails 5.2.2
* graphql-ruby 1.8.11
* Vue 2.5.2
* MySQL 5.7.21

graphql-rubyは1.8なのでclassベースでの記述で進めます。

# 環境構築

まずはRailsから作っていきます。
ディレクトリgraphql_railsを作り、

```bash:
$ mkdir graphql_rails ; cd $_
$ bundle init
```

を叩いてGemfileを生成されました。
少し編集します。

```ruby:Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.2'
```

次に`bundle install`を叩きます。

```bash:
$ bundle install --path vendor/bundler
```

インストールが終わりましたら、`rails new`を実行します。
フロントにはVue.jsを使うので、optionでVueを指定しておきましょう。

```bash:
$ bundle exec rails new . --webpack=vue
```

出来たら次にGraphQL用のgemなどを追記しておきます。

```ruby:Gemfile
gem 'mysql2'

gem 'graphql'

gem 'graphiql-rails'
```
追記したらもう一度`bundle install`。

```bash:
$ bundle install
```

graphql-rubyが入り、Railsのgenerateでgraphql環境を簡単に作れるようになっているので、下記コマンドを実行。

```bash:
$ bundle exec rails g graphql:install
```

すると、/app配下に新たなディレクトリが生成されました。

```bash
.
├── graphql_rails_schema.rb
├── mutations
└── types
    ├── base_enum.rb
    ├── base_input_object.rb
    ├── base_interface.rb
    ├── base_object.rb
    ├── base_scalar.rb
    ├── base_union.rb
    ├── mutation_type.rb
    └── query_type.rb
```

データベースはmysqlを使うので、
立ち上げ&database.ymlも編集しておきましょう。

```bash:
$ mysql.server start
```

```ruby:config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  database: rails_development
  username: root
  password: 
```

今回はGraphQLを使って単純な記事データの受け渡しをする想定なので、
PostのModelを生成しておきます。
タイトルとコンテンツのカラムのみで大丈夫です。
フロントとのエンドポイントとなるHomeContollorを生成しておきます。

```bash:
$ bundle exec rails g controller Home index
$ bundle exec rails g model Post title:string content:text
$ bundle exec rails db:create
$ bundle exec rails db:migrate
```

GraphQL用のobjectも生成しておきます。

```bash:
$ bundle exec rails g graphql:object Post id:ID! title:String! content:String!
```

これで準備は出来ました!
実際にサーバーを立ち上げてブラウザで確認してみましょう。

```bash:
$ bundle exec rails s -p 5000
```

graphql-rubyではgenarateするだけで実行できるテストクライアントを用意してくれています。
routes.rbを確認するとテストクライアントにアクセスできるrouteが追加されているのが確認できます。

```bash:config/routes.rb
Rails.application.routes.draw do
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  end
  post "/graphql", to: "graphql#execute"
...
end
```

http://localhost:5000/graphiql
にアクセスしてみましょう。

<img width="1370" alt="スクリーンショット 2018-12-21 10.54.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/205207/adbe34d4-a542-dcc7-a72d-4e171e92104e.png">

では実際にGraphQLを実装していきましょう。

# queryとmutation

実際にテストクライアントでリクエストを送ってみましょう。

そのまえにGraphQLでは、
データの「読み」の場合は、query。
データの「作成」「更新」「削除」などの場合は、mutationを使います。

## queryで記事全件取得

まずはGraphQLのPostオブジェクトにアクセスするためfieldを作成します。

```ruby:app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :content, String, null: false
  end
end
```

上記のようにPostTypeオブジェクトのアクセスしたいカラム名を書く必要があります。

次にquery_type.rbに記事データをリードできるfieldを追加しましょう。

```ruby:app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :all_posts, [PostType], null: false
    def all_posts()
      Post.all
    end
  end
end
```
引数も何も渡さず、ただPost.allしているだけですね。

そして、このままだとCSRFトークンが無いと怒られちゃうので、今回は特別にトークン判定をSkipするようにしておきましょう。

```ruby:app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  skip_before_action :verify_authenticity_token
...
```

次に確認のために事前にrailsコンソールなどからダミーデータを作っておきましょう。
ダミーデータを作ったら下記のqueryをテストクライアントで渡してみます。

```
{
  allPosts {
    title
  }
}
```

<img width="1101" alt="スクリーンショット 2018-12-21 11.51.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/205207/b84ab099-2428-85fb-eef5-ccef86418a66.png">


ちゃんと全件取れていますね！
指定した`title`のみ返ってきていることも確認出来ました！

## mutationで記事作成

続いて記事作成をできるようにしていきましょう。
まずMutationTypeにも記事作成用のfieldを追加します。

```ruby:app/graphql/type/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :create_post, mutation: Mutations::CreatePost
  end
end
```

次にCreatePostクラスを実装しましょう。

```ruby:app/graphql/mutations/create_post.rb
module Mutations
  class CreatePost < GraphQL::Schema::Mutation
    null false

    argument :title, String, required: true
    argument :content, String, required: true

    field :post, Types::PostType, null: false

    def resolve(**arg)
      post = Post.new(
        title: arg[:title],
        content: arg[:content]
      )

      if post.save
        { 
          post: post
        }
      else
        raise GraphQL::ExecutionError, post.errors.full_messages.join(", ")
      end
    end
  end
end
```

こちらもTypes、Queryと同じくまずfieldを指定する必要があります。
そして実際に処理はresolveメソッドの中で行っていますね。
引数で渡されたtitleとcontentをsaveするだけの単純なものです。

ではこちらもテストクライアントで実行してみましょう。
下記queryを渡してみます。

```ruby
mutation {
  createPost(title: "テスト2", content: "あああ") 
  {
    post {
      id
      title
    }
  }
}
```

<img width="1004" alt="スクリーンショット 2018-12-21 12.19.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/205207/de089af9-94fb-a1fc-acdd-772abb8984eb.png">

こちらも記事作成が確認できました！
しっかりdataも返ってきていますね。
では実際にフロントエンドとの連携部分を作ってきましょう。

# フロントエンド実装

最後にフロントの実装をしていきます。
先程作った記事全件取得queryを使って記事一覧ページを作ります。

VueでGraphQLとの連携部分はApollo Clientというモジュールを使います。
必要なものインストールしましょう。
下記ではyarnを使っておりますが、npmでも大丈夫なはずです。

```bash:
$ yarn add vue-apollo vue-router apollo-client graphql graphql-tag apollo-boost
```

インストールが出来たらviewを修正します。

```erb:app/view/home/index.html.erb
<div id="app">
  <div>
    <router-view></router-view>
  </div>
</div>

<%= javascript_pack_tag 'app' %>
```

erbではこれだけでOKです。
後はVueを書いていきましょう。
まずVueのオブジェクトを作成するapp.jsを作成します。

```js:app/javascript/packs/app.js
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import VueApollo from 'vue-apollo'
import ApolloClient from "apollo-boost"

import Index from './components/index.vue'

const apolloClient = new ApolloClient({
  uri: "http://localhost:5000/graphql"
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})

Vue.use(VueApollo)

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
  ],
})

const app = new Vue({
  router,
  apolloProvider,
  el: '#app',
})
```

次に実際に処理を行うcomponentsを作ります。

```vue:app/javascript/packs/components/index.vue
<template>
  <div>
    <ul v-for="post in allPosts">
      <li>タイトル：　{{post.title}}</li>
      <li>内容：　{{post.content}}</li>
    </ul>
  </div>
</template>

<script>
import gql from 'graphql-tag'

const POSTS_QUERY = gql`
query allPosts{
  allPosts {
    title
    content
  }
}`

export default {
  name: 'index',
  data () {
    return {
      allPosts: []
    }
  },
  apollo: {
    allPosts: {
      query:POSTS_QUERY
    }
  }
}

</script>
```

graphql-tagを使うことでクエリをパースし、GraphQLに渡してくれます。
表示側ではただ記事をforで回して表示しているだけですね。


最後にhome#indexをrootに設定しておきます。

```ruby:config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
```

では表示されているか確認してみましょう。

<img width="258" alt="スクリーンショット 2018-12-23 4.51.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/205207/a01c63d8-50c9-752c-931c-5179d5445f74.png">


ここまで出来たら完成です。
mutationで記事作成機能も作ってあるので、そちらも同じくmutationにqueryを投げる実装をフロント側で作成すれば実行できますね。

# まとめ
・GraphQLでRailsを使うことで型システムが使える
・RESTと比べて必要なデータだけ取得できるため、クライアントのスイッチがしやすい
などの強力なメリットを知ることが出来ました。
ですが、GraphQLではeager loadingなどの仕組みが無いようなのでN+1問題が起こる等のデメリットもあります。
サーバー側でどの言語と連携するのが使いやすいのかなどをもう少し調べてみようと思います。

# おわりに
@ihsiek を始め、Ateam Lifestyle x cymaアドベントカレンダー運営メンバーのみなさん、
本当にありがとうございました！

優秀なメンバーが他にも記事を書いてくれているので、
<a href="https://qiita.com/advent-calendar/2018/ateam-lifestyle-cyma" target="_blank">Ateam Lifestyle x cyma Advent Calendar 2018</a>から読んでいただけると幸いです。

エイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/

# 参考
https://qiita.com/vsanna/items/031aa5a17a2f284eb65d
https://qiita.com/naoki85/items/51a8b0f2cbf949d08b11
https://qiita.com/193/items/bf9842af0e1df8fb7226
https://marketing-web.hatenablog.com/entry/vue-apollo_github