LaravelでGraphQLを使うために学んだメモ

2022/07/02に公開

GraphQLとは

概要

  • GraphQLは仕様
  • GraphQLは言語
  • GraphQLはサービス

情報のグラフ構造に着目してクエリ言語を用いて情報を操作しようという考え方。
クライアント/サーバー通信のための言語仕様でもある。

GraphQLはRESTの代替品ではない

GraphQLは翻訳者である。
GrapQLはWebが発達するにつれてRESTが適合しない状況が出てきたため、誕生した。

GraphQLとRESTを同等と誤解する図

GraphQLはインターフェースであり、RESTの代替ではない
RESTからデータを取ってくるようにできるし、DBからデータを取ってくるようにもできる。

GraphQLの実体図

RESTとの比較

エンドポイント数が増えない

クライアント側から見ると/graphql だけである

バージョン管理が必要ない

常にクライアント側からは同じバージョンに見える

パフォーマンス面で無駄なケースを解消できる

RESTの過剰なレスポンスを必要最低限に抑えることでスピードをあげたり、多数アクセスによる情報取得を1回のクエリで済ませる。

また、フロントエンド〜バックエンド間のすり合わせで開発速度が低下するのを避ける

GraphQLの問題

  • リソース枯渇攻撃
    • 深いネストや同じフィールドを何度も要求するよくない実装が引き起こす
  • クライアントによるデータのキャッシュが難しい
  • N+1問題がある

GraphQLのスキーマ設計

APIはRESTのエンドポイントの集合ではなく、型の集合と捉えることができる。
そのデータ型の集合をスキーマと呼ぶ。

スキーマファーストで設計していくのであればGraphQLは輝く。
スキーマファーストで考えるということは、「フロントエンドのためのバックエンド(BFF)」である。
「クラアント側から何ができるか」という観点から設計を始めていく必要がある。UI駆動スキーマ設計のことである。

GraphQLスキーマガイド

命名規則

Query
  • リソース名(名詞)とその複数形
  • repository, repositories
Mutation
  • 動詞+名詞の形 addStar, createIssue
    • Mutationの名前+Input
    • Mutationの名前+Payload

Global Object Identification

  • 全データをIDにより一意に特定できるようにする仕様
  • users tableのidを一意にしたければ User:1など
type Query {
	node(id: ID!): Node
}

interface Node {
	id: ID!
}

type User implements Node {
	id: ID!
	name: String
}
  • Nodeに型指定をするとデータが取得する
{
  node(id: "MDEyOk9yZ2FuaXphdGlvbjEzNDIwMDQ=") {
    __typename
    id
    ... on Organization {
      databaseId
      login
    }
  }
}
  • nodes(ids: [ID!]!): [Node]!もついでに定義するとよい

Cursor Connections

  • カーソルを使ってリストを順にたどるための仕様
    • Repositoryのリストを返す場合、[Repository]を返す代わりにRepositoryConnectionを返す
  1. リスト型のフィールドの代わりに、Connectionサフィックスの型を作る
  2. first: Int!とafter: Stringを引数に設ける
  3. Connectionはcursorを持てるEdgeとページング情報をもつPageInfoをもつ

type Query {
	repositories(
		first: Int
		after: String
		
		last: Int
		before: String
	): RepositoryConnection
}

type RepositoryConnection {
	pageInfo: PageInfo
	edges: [RepositoryEdge]
}

type PageInfo {
	hasPreviousPage: Boolean!
	startCursol: String
	hasNextPage: Boolean!
	endCursol: String
}

type RepositoryEdge {
	cursor: String!
	node: Repository
}

type Repository {
	id: ID!
	name: String!
}

Queryでinputはアンチパターン

  • フロント側的にはinputでまとめなくてもvariablesでまとまる
  • 引数を明示的に指定するのは2 ~ 3がほとんどであまり多くならない
  • デフォルト値の指定が活用できずつらい

GraphQLサーバー

GraphQLサーバーにはリゾルバを実装する。
リゾルバはREST API, データベース, その他のサービスからデータを取得したり更新したりできる。

GraphQLの操作

RESTに対応付けて表現するとGraphQLにはルートリゾルバと呼ばれる操作が3つある。

  • Query (GET)
  • Mutation (POST, PUT, DELETE)
  • Subscription(データのSubscribe)

これらの操作でデータの集合を扱っていく

詳解の説明はこちら

まとめ

  • GraphQLは翻訳者としての立ち位置なので応用が効きそうだが、銀の弾丸ではないことを理解する
  • LaravelでGraphQLを扱う際にもスキーマ設計が必要になるので、設計する必要がある

参考リンク

https://www.youtube.com/watch?v=_U6Ft8op9Ik
https://docs.github.com/ja/graphql
https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/
https://zenn.dev/saboyutaka/articles/07f1351a6b0049
https://relay.dev/docs/guides/graphql-server-specification/
https://zenn.dev/icchy/articles/2d64a9ff6aa800
https://docs.github.com/ja/graphql/guides/introduction-to-graphql
https://graphql.org/learn/best-practices/
https://docs.github.com/ja/graphql/guides/introduction-to-graphql
https://speakerdeck.com/nagano/phptographql

Laravel x GraphQL

LaravelでGraphQLを扱うためにLighthouseという便利なライブラリを使う。
Eloquentと連動していて、スキーマ定義にディレクティブを付けることで補完してくれる、というライブラリである。

Install

  1. composer require nuwave/lighthouse
  2. php artisan vendor:publish --tag=lighthouse-config
    1. CORS設定
  3. スキーマ定義する

Dev Tools

PlayGround

composer require mll-lab/laravel-graphql-playground

IDE Plugin

https://plugins.jetbrains.com/plugin/8097-graphql/

Lighthouse UseCase

https://lighthouse-php.com/

Schema

Type

type User {
  id: ID!
  name: String!
  email: String!
  created_at: String!
  updated_at: String
}

Object Type

Eloquentモデルと密接に関連していて、一意の名前を持ち、一連のフィールドを含んでいる必要がある。

type User {
  id: ID!
  name: String!
  email: String!
  created_at: String!
  updated_at: String
}

type Query {
  users: [User!]!
  user(id: ID!): User
}

Scalar

Enum

@enumディレクティブを使用して値を定義できる

enum EmploymentStatus {
  INTERN @enum(value: 0)
  EMPLOYEE @enum(value: 1)
  TERMINATED @enum(value: 2)
}

https://lighthouse-php.com/5/the-basics/types.html#enum

Root Type

Query

type Query {
  me: User
  users: [User!]!
  userById(id: ID): User
}

Mutation

type Mutation {
  createUser(name: String!, email: String!, password: String!): User
  updateUser(id: ID, email: String, password: String): User
  deleteUser(id: ID): User
}

Subscription

type Subscription {
  newUser: User
}

Fields

  • デフォルトでは、Lighthouseは App\GraphQL\QueriesまたApp\GraphQL\Mutationsの名前空間下でフィールドの大文字の名前を持つクラスを検索し、__invokeを使い、 通常のリゾルバ引数を使用して呼び出す。
  • php artisan lighthouse:query <QueryName> で生成できる
  • $argsはクエリに渡される引数の連想配列

ディレクティブ

Eloquent

  • READ
    • @allでモデルの名前が解決しているフィールドの戻りタイプと同じであると想定し、Eloquentで自動解決する
    • @paginateでページネーション
    • @eqで追加制約をクエリに追加できる
    • @orderByでソート
  • CREATE
    • @createでデータ作成
    • @updateでモデル更新
    • @upsert
    • @delete

スキーマ構成

  • schema.graphqlからスキーマを読み取る
  • インポートにワイルドカードも使える
type Query {
  user: User
}

#import user.graphql

Settings

Security

'security' => [  
	'max_query_complexity' => \GraphQL\Validator\Rules\QueryComplexity::DISABLED,  
    'max_query_depth' => \GraphQL\Validator\Rules\QueryDepth::DISABLED,  
    'disable_introspection' => \GraphQL\Validator\Rules\DisableIntrospection::DISABLED,  
],

  • クエリの深さを制限する場合は、max_query_depthを設定する
  • クエリの複雑性で制限する場合は、max_query_complexityを設定する
  • イントロスペクションを無効にする場合は、disable_introspectionを設定する

https://lighthouse-php.com/5/security/validation.html#single-arguments

GitHubで編集を提案

Discussion