はじめに
こんにちは!榊原です。
トレタに入社して1年が経ちました。
この1年はとても変化が多くあった年だったと感じています。
特に今年の1月から会社全体でフルリモート化や、
DXを意識した飲食店での自分のスマホでオーダーできるシステムや予約時に座席を指定して予約できるシステムなど、
時代の変化に対応した様々な新規サービスを発表した年でもあります。
発表したサービスの中にはまだ市場に受け入れられるか仮設段階のものも数多くありました。
エンジニアの人数も限りがある中で、最速かつ柔軟な変化を求められる開発を進めるに当たって、
最近弊社で使われ始めているのがGraphQLです。
今回はそんなGraphQLを弊社でも創業当初から運用されてきたRuby on Railsとの比較を中心とした説明とさせていただきます。
GraphQLを説明をするに当たって、グラフ理論は欠かせないのですがそちらは他の記事が多数存在するため今回は割愛させていただきます。
対象となる読者
- Railsは使用していてもGraphQLは触ったことがないけど触ってみたい
- サーバーサイドを担当していたけど、業務で使用しなければならなくなった
対象アプリケーション
- GraphQLエンジン v1.3.3
- DB: PostgreSQL v13.1
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の説明
今回使用するのは下記の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
実際にサービスを作成していると複数のテーブル情報を一度に取得したいというケースは多くの場面で遭遇すると思います。
ここでは、シンプルに persons
と photos
のデータを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
persons
と photos
をそれぞれ別で取得を行いました。
ですが実際にはリレーションに紐づく値のみを取得したいというケースの方が多いかと思います。
次は 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のドキュメントを読んでみてください。
最後に
弊社ではGraphQLを仮説検証段階の、速度感をもったサービス開発で少しづつ取り入れ始めています。
そんな技術もサービスも一緒に検証したいエンジニアを募集しています。
カジュアルな面談もありますので、一度下記のURLから確認してみてください!
https://corp.toreta.in/recruit/midcareer/