2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

トレタAdvent Calendar 2020

Day 8

RailsエンジニアがGraphQLを触って比較してみた

Last updated at Posted at 2020-12-08

はじめに

こんにちは!榊原です。
トレタに入社して1年が経ちました。
この1年はとても変化が多くあった年だったと感じています。

特に今年の1月から会社全体でフルリモート化や、
DXを意識した飲食店での自分のスマホでオーダーできるシステムや予約時に座席を指定して予約できるシステムなど、
時代の変化に対応した様々な新規サービスを発表した年でもあります。

発表したサービスの中にはまだ市場に受け入れられるか仮設段階のものも数多くありました。
エンジニアの人数も限りがある中で、最速かつ柔軟な変化を求められる開発を進めるに当たって、
最近弊社で使われ始めているのがGraphQLです。

今回はそんなGraphQLを弊社でも創業当初から運用されてきたRuby on Railsとの比較を中心とした説明とさせていただきます。

GraphQLを説明をするに当たって、グラフ理論は欠かせないのですがそちらは他の記事が多数存在するため今回は割愛させていただきます。

対象となる読者

  • Railsは使用していてもGraphQLは触ったことがないけど触ってみたい
  • サーバーサイドを担当していたけど、業務で使用しなければならなくなった

対象アプリケーション

GraphQLって何者なの?

GraphQLはFacebookによって開発されました。
元々モバイルアプリはWeb用のラッパーアプリでした。
しかしRESTfulAPIサーバーとFQL(Facebook用のSQL)のデータテーブルで運用されてましたが、
パフォーマンスの低下や度々クラッシュするなど改善が求められました。
そこでFacebookのクライアント、サーバーアプリケーションの性能上のかだいと、データ構造の要件を満たす解決策として誕生したのがGraphQLです。
そして現在ではFacebookのほぼ全てのデータ取得にGraphQLが用いられています。

GraphQLのクライアントアプリケーションとしては、 Relay, Apollo, そして最近ではHasura等があります。

初めの一歩 (GraphQLでできること)

シンプルなGraphQLの記述例ですが、
例えばRailsでPersonsテーブルの情報全て取得するAPIがあるとします

Rails

def PersonsController
  def index
    data = Person.all
    render json: data
  end
end

この場合GraphQLでは下記の様なクエリを記述するだけで取得できます

GraphQL

query {
  persons {
    id
    name
    birthday
    created_at
    updated_at
  }
}

出力結果はGraphQLでは data のハッシュ内に梱包される形になりますが、下記の様な形式で出力されます。

{
  "data": {
    "persons": [
      {
        "id": 1,
        "name": "佐藤太郎",
        "birthday": "2000-01-01",
        "created_at": "2020-12-05T06:31:43.250885+00:00",
        "updated_at": "2020-12-05T06:31:43.250885+00:00"
      },

      〜 略 〜

      {
        "id": 4,
        "name": "伊藤花子",
        "birthday": "2010-03-03",
        "created_at": "2020-12-05T06:32:37.952758+00:00",
        "updated_at": "2020-12-05T06:32:37.952758+00:00"
      }
    ]
  }
}

この様にGraphQLはクエリ言語なため、RailsではあったModelやController、Render等を省き、
簡単なコードだけででRESTFullなAPIを作成することができます。

この後のRailsのサンプルコードとしては、Controllerや render は極力記述せず、ActiveRecordを用いたデータを中心とした記述とします。

使用するSchemaの説明

schema.png

今回使用するのは下記の3つのテーブルで、単純なSNSを想定して作成しました。

  • persons: ユーザー
  • photos: ユーザーが投稿した写真
  • photo_person_tags: 写真にタグ付けされた人

シンプルなQuery

それでは先ほどのクエリを見ていきましょう。

query {       // Select句
  persons {   // テーブル名
    id        // カラム名 (以下同一)
    name
    birthday
    created_at
    updated_at
  }
}

GraphQLではCQRSを採用しているため、最初にQuery(Select句)は query を、Command(Create、Update、Delete句)は mutation を記述します。
今回はSelectなので query を使用しました。

次に、取得したいテーブル名とカラム名を記述すれば、先程の様な値が取得できます。

条件のあるQuery

全てを取得することはできても、条件に合ったもののみ取得したいケースは勿論あるでしょう。
その場合は下記の様に記述します。
名前に佐藤という文字が入っている人を検索します。

Rails

persons.where('name like ?','佐藤%').limit(2)

GraphQL

query {
  persons(where: {name: {_like: "佐藤%"}}, limit: 2) {
    id
    name
    birthday
    created_at
    updated_at
  }
}

persons の中にwhere句、Limit句が追加されただけですね。
基本的な表現は同じです。
条件式に使用できるものとしては下記で確認してください。

https://hasura.io/docs/1.0/graphql/core/queries/query-filters.html

複数のテーブルに対するQuery

実際にサービスを作成していると複数のテーブル情報を一度に取得したいというケースは多くの場面で遭遇すると思います。

ここでは、シンプルに personsphotos のデータを1つのJSON形式で出力してみます。

Rails

persons = persons.all
photos = photos.all

data = {
  persons: persons, 
  photos: photos
}

GraphQL

query {
  persons {
    id
    name
    birthday
    created_at
    updated_at
  }
  photos {
    id
    person_id
    url
    created_at
    updated_at
  }
}

出力結果

{
  "data": {
    "persons": [
      {
        "id": 1,
        "name": "佐藤太郎",
        "birthday": "2000-01-01",
        "created_at": "2020-12-05T06:31:43.250885+00:00",
        "updated_at": "2020-12-05T06:31:43.250885+00:00"
      },

      〜 略 〜

      {
        "id": 3,
        "name": "佐藤花子",
        "birthday": "2010-03-03",
        "created_at": "2020-12-05T06:32:37.249905+00:00",
        "updated_at": "2020-12-05T08:21:50.992633+00:00"
      }
    ],
    "photos": [
      {
        "id": 1,
        "person_id": 1,
        "url": "https://example.com/photos/1",
        "created_at": "2020-12-05T06:33:19.847425+00:00",
        "updated_at": "2020-12-05T06:33:19.847425+00:00"
      },

      〜 略 〜

      {
        "id": 5,
        "person_id": 2,
        "url": "https://example.com/photos/4",
        "created_at": "2020-12-05T06:33:43.201778+00:00",
        "updated_at": "2020-12-05T06:33:43.201778+00:00"
      }
    ]
  }
}

この様にGraphQLでは、ただ取得したいテーブル、カラムを追加するだけで取得できます。
 

リレーションを使用したQuery

personsphotos をそれぞれ別で取得を行いました。
ですが実際にはリレーションに紐づく値のみを取得したいというケースの方が多いかと思います。

次は person_id が既知の場合に、それに紐づく photo を取得する例です。

Rails

person = Person.find(1)

photo_urls person&.photos&.each_with_object([) do |photo, hash|
  hash << {url: photo.url}
end

data = {
  name: person.name,
  birthday: person.birthday,
  photos: photo_urls
}

GraphQL

{
  persons_by_pk(id: 1) {
    photos{
      url
    }
    name
    birthday
  }
}

出力結果

{
  "data": {
    "persons_by_pk": {
      "photos": [
        {
          "url": "https://example.com/photos/1"
        },
        {
          "url": "https://example.com/photos/2"
        },
        {
          "url": "https://example.com/photos/3"
        },
        {
          "url": "https://example.com/photos/3"
        }
      ],
      "name": "佐藤太郎",
      "birthday": "2000-01-01"
    }
  }
}

この様にリレーションを貼っていれば、通常のカラムを取得するように取得することができます。
この辺りからGraphQLの方がnull回避やループ処理等を記述しなくて良くなるので、単純なります。
(実際にはRailsの場合はURLを詰め直す作業は行わないと思いますが)

件数を算出するQuery

開発する上で、合計値、最大値、平均値等の値を算出したくなる時があると思います。
今回はIDが1の person が投稿した写真の件数を取得してみましょう

Rails

photos = Person.find(1).photos

data = photos.each_with_object({}) do |photo, hash|
  hash << {
    id: photo.id,
    url: photo.url,
    photo_person_tag: {
      count: photo.photo_person_tags.count
    }
  }
end

aggregate の様なGraphQL側でしか使用しない名前は省いています。

GraphQL

query {
  persons_by_pk(id: 1){
    photos {
      url
      id
      photo_person_tags_aggregate {
        aggregate {
          count(columns: id)
        }
      }
    }
  }
}

出力結果

{
  "data": {
    "persons_by_pk": {
      "photos": [
        {
          "url": "https://example.com/photos/1",
          "id": 1,
          "photo_person_tags_aggregate": {
            "aggregate": {
              "count": 1
            }
          }
        },

          

        {
          "url": "https://example.com/photos/3",
          "id": 4,
          "photo_person_tags_aggregate": {
            "aggregate": {
              "count": 0
            }
          }
        }
      ]
    }
  }
}

この様にGraphQLでも単純な演算処理は備わっています。
ただ、単純な演算自体はRailsが圧倒的に記述しやすいですね。

フラグメントを使用したQuery

フラグメントは同じクエリを複数の場所で使い回すことができる選択セットです。
Railsで言う所の、処理をメソッドに切り出す事ですね。
それでは一つ前の例を元に見ていきましょう。

Rails

今回はRails側は既に count メソッドとして切り出されていたものを使用したので割愛します。

GraphQL

fragment personInThePhotoCount on photos {
      photo_person_tags_aggregate {
        aggregate {
          count(columns: id)
        }
      }
}

query {
  persons_by_pk(id: 1){
    photos {
      url
      id
      ...personInThePhotoCount
    }
  }
}

出力結果

{
  "data": {
    "persons_by_pk": {
      "photos": [
        {
          "url": "https://example.com/photos/1",
          "id": 1,
          "photo_person_tags_aggregate": {
            "aggregate": {
              "count": 1
            }
          }
        },

          

        {
          "url": "https://example.com/photos/3",
          "id": 4,
          "photo_person_tags_aggregate": {
            "aggregate": {
              "count": 0
            }
          }
        }
      ]
    }
  }
}

変数を使用したQuery

実際にアプリケーションとして運用する際にClientを操作するユーザによって入力されたものをWhere句などでクエリに使用する事があります。
その中で意識する事として入力値のバリデーションがあると思います。
そんな時に使用するものとしてクエリ変数 (Query Variables)があります。
クエリ中で使用される動的な変数を別で定義することにより、
型のチェックやnull回避等はもちろん、再利用性も高くなります。

次の例は名前が 佐藤 から始まり、 2005-01-01 より前に誕生日の人を取得するクエリです。

GraphQL

Query

query MyQuery($name: String!, $birthday: date) {
  persons(where: {name: {_like: $name}, birthday: {_lt: $birthday}}) {
    id
    name
    birthday
  }
}

Query Variables

{
    "name": "佐藤%", 
    "birthday": "2005-01-01"
}

出力結果

{
  "data": {
    "persons": [
      {
        "id": 1,
        "name": "佐藤太郎",
        "birthday": "2000-01-01"
      }
    ]
  }
}

query MyQuery($name: String!, $birthday: date)
このクエリの引数は2つで、
名前は String 型で入力必須
誕生日は date 型でnull許可されている例です。

Mutation

次はMutaionを説明します。Mutationは変更を加えたい時に使用します。
種類としては大きく分けて作成、更新、削除の3種類あります。

それではそれぞれ見ていきましょう。

レコード作成のMutation

Rails

Person.create(
    name: "佐藤三郎", 
    birthday: "2005-01-01"
)

GraphQL

Query

mutation MyMutation($name: String!, $birthday: date!) {
  insert_persons_one(object: {birthday: $birthday, name: $name}) {
    id
    name
    birthday
    created_at
    updated_at
  }
}

Query Variables

{
    "name": "佐藤三郎", 
    "birthday": "2005-01-01"
}

出力結果

{
  "data": {
    "insert_persons_one": {
      "id": 6,
      "name": "佐藤三郎",
      "birthday": "2005-01-01",
      "created_at": "2020-12-08T10:27:17.430325+00:00",
      "updated_at": "2020-12-08T10:27:17.430325+00:00"
    }
  }
}

この様にQueryとMutationは基本同一の形になります。
また、insert_persons_oneの {} の部分は返却値になります。必要なものだけを入力してください。
insert_persons_one(key: value) { 【ここの部分】 }

また、 insert_persons_one という名前からわかるように、これは単数のレコード作成になるので、
複数の場合は insert_persons で作成できます。

値変更のMutation

今回は値を変更しようと思います。
person のIDが5のユーザの誕生日を 2002-01-01 に変更します。

Rails

person = Person.find(5)
person.birthday = "2002-01-01"
person.save

GraphQL

Query

mutation MyMutation($birthday: date) {
  update_persons(where: {id: {_eq: 5}}, _set: {birthday: $birthday}) {
    affected_rows
  }
}

Query Variables

{
    "birthday": "2002-01-01"
}

出力結果

{
  "data": {
    "update_persons": {
      "affected_rows": 1
    }
  }
}

更新対象の特定は、今回はIDを用いましたが普通に他のものを使用して問題ありません。
また、更新内容は _set 内に更新情報を入力することで更新できます。

今回の返却値は "affected_rows": 1 となっていますが、これは返却値を更新した行数を設定したため
1行だけ変更があった事が確認できます。

削除を行うMutation

今回はIDが5の person を削除してみたいと思います。

Rails

person = Person.find(5)
person.destroy

GraphQL

mutation {
  delete_persons(where: {id: {_eq: 5}}) {
    affected_rows
  }
}

出力結果

{
  "data": {
    "delete_persons": {
      "affected_rows": 1
    }
  }
}

こちらも同様に簡単に削除を行う事ができました。

まとめ

最後まで読んでくださってありがとうございます。
GraphQLは表現には制限がありますし、スキーマ情報を外に公開しているので、
スキーマの変更に対して脆くなってしまう問題こそあれ、Rails以上に簡単にRESTFul APIを作成することができます。

今回記述させて頂いた内容はまだGraphQLの一部でしかありません。
より一層GraphQLを知りたい、使いたいという方はまずはGraphQLの公式ドキュメントを見てみると良いでしょう。
https://graphql.org/learn/

ただし、今回使用したGraphQLエンジンのHasuraはGraphQLとは大きく異なっている部分があるので、 HasuraのGraphQLを使用したい場合はHasuraのGraphQLのドキュメントを読んでみてください。

https://hasura.io/docs/1.0/graphql/core/index.html

最後に

弊社ではGraphQLを仮説検証段階の、速度感をもったサービス開発で少しづつ取り入れ始めています。

そんな技術もサービスも一緒に検証したいエンジニアを募集しています。

カジュアルな面談もありますので、一度下記のURLから確認してみてください!
https://corp.toreta.in/recruit/midcareer/

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?