Open44

GraphQL

high-ghigh-g

モチベーション

  • GraphQLの概要
  • ざっくりとした利用方法
    • Nodeのみで利用
    • Prismaとの連携
    • Reactとの連携
      • ApolloClient
      • ApolloProvider
      • useQuery
  • 案件利用の場合
    • GraphQLサーバを立てるには
    • Next.js + GraphQLを利用するには
high-ghigh-g

Overfetching: レスポンスが不要なデータも多く含んでしまうケース
Underfetching: 一度のリクエストで全ての必要なデータを取得することができないため、追加でリクエストを送信する必要があるケース

これらの問題をGraphQLで解決可能

high-ghigh-g

これを読んでる
https://reffect.co.jp/html/graphql

GraphQLという名前はGraphとQLの2つに分けることができる
Graph→nodeとedge

オブジェクトをnode, リレーションシップをedgeで表現

QLはQuery Language
データベースとは関係がなくSQLのようにデータベースをクエリーで操作するための言語ではなくAPIのためのクエリー言語

GraphとQLを合わせたGraphQL自体はREST APIの代替

REST APIと同様にGraphQLもクライアントからサーバに対してCRUDする際に利用可能

REST API

  • データを取得する際に複数のエンドポイント
    (例:ユーザ一覧から/users, ブログの記事一覧なら/posts,…)を利用

GraphQL

  • 1つのエンドポイントのみ持ち
  • クエリーを設定(問い合わせなのか更新または削除なのか、どのデータが欲しいかを指定)でサーバからデータを取得
  • クエリーの設定によって一度のHTTPリクエストで一括でユーザ情報、ブログ記事情報を取得することも可能
  • ユーザ情報の中からはemailのみ選択して取得するといったことも可能

REST APIにプロダクトが存在しないようにGraphQLという名前のプロダクトが存在するわけではない
GraphQLサーバを構築する方法はいくつか存在

Applo ServerでGraphQLサーバを構築

high-ghigh-g

モジュールインストール

npm i apollo-server graphql
npm i -D nodemon
touch index.js

package.json

scriptsに以下を追加

    "server": "nodemon index.js"

index.js

const { ApolloServer, gql } = require('apollo-server')
high-ghigh-g

GraphQLでは、独自のスキーマ定義言語 SDL(Schema Definition Language)を持つ
SDLで型定義を行う
クライアントがアクセスするデータの定義を行うだけでなく問い合わせ、更新、削除も型定義する

スキーマ例

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

スキーマタイプにはオブジェクトタイプの他にQueryタイプ、Mutationタイプ、Subscriptionタイプがある

QueryタイプはRead Operation(問い合わせ)を行うクエリーの定義に利用
MutationタイプはWrite Operations(作成、更新、削除)に利用
SubscriptionタイプはChatアプリなどのようなリアルタイムのRead Operationに利用

スキーマ定義

helloworldがてら、helloクエリをQueryタイプに追加

index.jsに記述

const typeDefs = gql`
  type Query {
    hello: String
  }
`

resolvers設定

Queryタイプで定義した戻り値の型通りにどのような処理を行うかを記述するのがresolver(リゾルバ)

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
  },
}

サーバ起動

スキーマとリゾルバをGraphQLサーバ(ApolloServer)に設定


const server = new ApolloServer({ typeDefs, resolvers })

server.listen().then(({ url }) => {
  console.log(`server is running on ${url}`)
})

サーバに表示されているurlをもとにブラウザを開く

Query your serverをクリックすると、Apollo Studioが立ち上がる

high-ghigh-g

Apollo Studioの初期画面

以下のクエリを実行する

query Query {
  hello
}

結果が返ってくる

high-ghigh-g

オブジェクトタイプには特別なタイプであるQuery、Mutation、Subscriptionという名前はつけることができませんがそれ以外の任意の名前をつけることができる

Userタイプがidとnameとemailの3つのフィールドで構成されている場合

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

フィールドタイプにはスカラータイプとオブジェクトタイプがある

スカラータイプ→String, Int, ID, Booleanなど
オブジェクトタイプは定義済みのオブジェクトタイプを型として利用する場合に使用

!は必須

Queryタイプにオブジェクトタイプのusersを追加

リゾルバに実際の値を追加

[User]は配列型

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    hello(name: String!): String
    users: [User]
  }
`

const users = [
  { id: '1', name: 'John Doe', email: 'john@test.com' },
  { id: '2', name: 'Taro Suzuki', email: 'suzuki@example.com' },
]

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
    users: () => users,
  },
}

Apollo Studioでクエリをいじると、レスポンス取得できた

query Query {
  users {
    id,
    email
  }
}

引数ありの場合は、クエリを以下のように書き

query Query($name: String!) {
  hello(name: $name)
}

valiablesを以下の様にする

{
  "name": "aaaaaa"
}
high-ghigh-g

外部のデータソースを取得して利用

yarn add axios

https://jsonplaceholder.typicode.com/usersから取得したデータを利用

const axios = require('axios')

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users_fetch: [User]
  }
`

const resolvers = {
  Query: {
    users_fetch: async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/users')
      return response.data
    },
  },
}

Apollo Studioで以下を試せる

query Query($userId: ID!) {  
  users_fetch {
    id
    name
    email
  }
}

high-ghigh-g

外部データソース読み込みを行う場合もGraphQLのエンドポイントは変わらない為、
フロントエンド側の変更の必要はなく、常にGraphQLを変更するだけで良い

high-ghigh-g

POSTしたい場合、Postタイプを追加する

type Post {
  id: ID!
  title: String!
  body: String!
  userId: ID!
}

Queryタイプにpost専用のスキーマを作る

type Query {
  posts: [Post]
}

リゾルバでの記述

const resolvers = {
  Query: {
    posts: async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
      return response.data
    },
  },
}

Apollo Studioでの確認

query ExampleQuery {
  posts {
    title
  }
}
high-ghigh-g

UserタイプにPost型を追加した時

  type User {
    id: ID!
    name: String!
    email: String!
    myPosts: [Post]
  }

UserにPostタイプを追加すると、下記のようなアクセスができるが、

query ExampleQuery {
  users {
    id
    myPosts {
      title
    }
  }
}

この時点でmyPostsの結果はnull

{
  "data": {
    "users": [
      {
        "id": "1",
        "myPosts": null
      },
      {
        "id": "2",
        "myPosts": null
      }
    ]
  }
}

myPostsをnullとしない為には、
リゾルバ側でmyPostsを取得する為の処理を記述する必要がある

    user: async (parent, args) => {
      const userResponse = await axios.get(`https://jsonplaceholder.typicode.com/users/${args.id}`)
      const postsResponse = await axios.get(`https://jsonplaceholder.typicode.com/posts`)

      const myPosts = postsResponse.data.filter((post) => post.userId == args.id)
      const user = Object.assign({}, userResponse.data, {
        myPosts,
      })
      return user
    },
    posts: async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts')
      return response.data
    },
  },

クエリ

query ExampleQuery($userId: ID!) {
  user(id: $userId) {
    id
    myPosts {
      title
    }
  }
}

これで確認ok

high-ghigh-g

リゾルバの引数のparentに関して
親構造へアクセスする為の引数

resolversは、typeDefsに定義したそれぞれのtypeにアクセスできそう。
UserにmyPostsを作成し、parentに対して

const resolvers = {
  Query: {
  },
  User: {
    myPosts: (parent) => {
      console.log(parent)
    },
  },
}

リゾルバのQueryのコードを以下にする

    user: async (parent, args) => {
      const userResponse = await axios.get(`https://jsonplaceholder.typicode.com/users/${args.id}`)
      return userResponse.data
    },

Apollo Studioでクエリを実行すると、ユーザ情報のみがターミナルに表示される

前回入れ子となっていた構造の場合でもQueryリゾルバ内でコードを書いていたが、
構造化が可能。

↓子スキーマは、別途分けることが出来る

    myPosts: async (parent) => {
      const response = await axios.get(`https://jsonplaceholder.typicode.com/posts`)
      const myPosts = response.data.filter((post) => post.id === parent.id)
      return myPosts
    },
high-ghigh-g

axiosを利用しなくてもApollo ServerはREST API用のデータソースライブラリが提供されている

apollo-datasource-rest

インストール

yarn add apollo-datasource-rest

利用

const { RESTDataSource } = require('apollo-datasource-rest');

以下のclassを作成

class jsonPlaceAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = 'https://jsonplaceholder.typicode.com/'
  }

  async getUsers() {
    const data = await this.get('/users')
    return data
  }
  async getUser(id) {
    const data = await this.get(`/users/${id}`)
    return data
  }
  async getPost() {
    const data = await this.get('/posts')
    return data
  }
}

ApolloServerにdataSourcesを追加

const server = new ApolloServer({
  typeDefs,
  resolvers,
  dataSources: () => ({ jsonPlaceAPI: new jsonPlaceAPI() }),
})

dataSourcesを追加することで、リゾルバの第3引数にdataSourceを追加して利用可能

    user: async (parent, args, { dataSources }) => {
      const userResponse = await axios.get(`https://jsonplaceholder.typicode.com/users/${args.id}`)
      return userResponse.data
    },
high-ghigh-g

DB作成の為にprismaを使う

yarn add prisma -D

npx prisma init

prisma/にdev.dbファイルを作成

.envファイル内に
DATABASE_URL="file:./dev.db" を記述

scheme.prismaファイルを設定

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}


model User {
  id Int @id @default(autoincrement())
  email String @unique
  name String?
  posts Post[]
}


model Post {
  id Int @id @default(autoincrement())
  title String
  body String?
  auther User? @relation(fields: [autherId], references: [id])
  autherId Int?
}

以下のコマンドでDBにテーブルの作成を行います。
npx prisma migrate dev

high-ghigh-g

npmスクリプトに以下を記述
"studio": "prisma studio"

yarn studioでprisma studioが立ち上がる

npx prisma generate

prisma clientがインストールされる

high-ghigh-g

ApolloServerとの接続

PrismaClientインスタンスの作成

const { PrismaClient } = require('@prisma/client')

const prisma = new PrismaClient()

リゾルバにて、usersの指定をprismaに変えることでprismaとの接続ができる

  Query: {
    users: () => {
      return prisma.user.findMany()
    },

prisma studioを利用してGUI上でデータ登録を行い、

apollo studioでクエリを実行することで、prisma側で定義した情報を取得できる

query ExampleQuery {
  users {
    id
    name
    email
  }
}

high-ghigh-g

read operations → Queryタイプ
wite operations → Mutationタイプ

high-ghigh-g

  type Mutation {
    createUser(name!: String!, email: String!): User
  }

リゾルバ

  Mutation: {
    createUser: (_, args) => {
      return prisma.user.create({
        data: {
          name: args.name,
          email: args.email,
        },
      })
    },
  },
high-ghigh-g

Apollo StudioでMutation実行

mutation Mutation($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
} 

values は以下の形

{
  name: "値"
  email: "値"
}

メモ

  1. クエリの引数に対して型を書く
  2. クエリの引数は$をつけて書く
  3. リゾルバの引数で、プロップス: $引数の形で記述
  4. {} 内にリゾルバに渡す項目を書く
high-ghigh-g

Prismaスタジオを立ち上げると追加されていることを確認

usersクエリの実行でも追加を確認

query Query {
  users {
    id
    name
    email
  }
}
high-ghigh-g

mutationによる更新

型追加

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

リゾルバ

  Mutation: {
    createUser: (_, args) => {
      return prisma.user.create({
        data: {
          name: args.name,
          email: args.email,
        },
      })
    },
    updateUser: (_, args) => {
      return prisma.user.update({
        where: {
          id: args.id
        },
        data: {
          name: args.name
        }
      })
    }
  },

型追加→リゾルバ追加→リゾルバ内でprismaと接続
型で新規、更新、削除を分けるのではなく、
read operations, write operationsという区分でQueryタイプかMutationタイプかを分ける
実際のDB処理はprisma.◯◯.createやprisma.◯.update等、ORMのクエリで表現する。

high-ghigh-g

更新のオペレーション

mutation Mutation($name: String!, $updateUserId: Int!) {
  updateUser(name: $name, id: $updateUserId) {
    id
    name
  }
}

varaibles

{
  "name": "update_texttttttttttttt",
  "updateUserId": 1
}
high-ghigh-g

user削除

型定義

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

リゾルバ


    deleteUser: (_, args) => {
      return prisma.user.delete({
        where: {
          id: args.id,
        },
      })
    },

オペレーション

mutation Mutation($deleteUserId: Int!) {
  deleteUser(id: $deleteUserId) {
    id
  }
}

valiables

{
  "deleteUserId": 2
}
high-ghigh-g

vanilla JS

fetchメソッドでGraphQLサーバに対し、
post + クエリを投げる

index.htmlに下記を記述し、ファイルをブラウザで開く

<div id="app"></div>

<script>
    async function fetchBook() {
      const response = await fetch('http://localhost:4000', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: `
            query {
              users {
                id
                name
                email
              }
            }
          `,
        }),
      })

      const res = await response.json()

      document.getElementById('app').innerHTML = `<h1>${res.data.users[0].name}</h1>`
    }

    fetchBook()
</script>

ok

high-ghigh-g

FetchでもGraphQLサーバとの通信は可能だが、
Apollo Clientを利用したほうが効率的。

Apollo Clientを利用することで、useQueryなどの実装をする際に便利なHooksを利用可能。
React, Vue, Svelteに対応しているライブラリ。

Apollo Client Devtoolsを利用すると便利
https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm

Query, Mutationの中身、キャッシュの中身を確認できる。

high-ghigh-g

Reactプロジェクトを作る

npm create vite

TypeScript + SWCを選択する

cd vite-project
npm i
npm run dev

動作確認ok

ライブラリインストール

npm i @apollo/client graphql
high-ghigh-g

コードとしては以下のようなシンプルな構造でいけた

import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

import { ApolloClient, InMemoryCache, gql } from '@apollo/client'

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
})

client
  .query({
    query: gql`
      query GetUsers {
        users {
          id
          name
          email
        }
      }
    `,
  })
  .then((result) => console.log(result))

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)

high-ghigh-g

ApolloClientをアプリ内全体で利用できるようにする為にApolloProviderを利用する
以下の様にRootコンポーネントをラップする

import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
})

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
)

high-ghigh-g

ApolloProvider以下にあるコンポーネントでApolloClientを利用する場合、useQueryを利用する。
useStateやuseSWRなどと同じ様な感覚で利用できる

import { gql, useQuery } from '@apollo/client'

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`

type User = {
  id: number
  name: string
  email: string
}

function App() {
  const { data, loading, error } = useQuery(GET_USERS)

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error</p>

  return (
    <div>
      <h1>Hello World</h1>
      {data.users.map((user: User) => (
        <div key={user.id}>
          <h2>ID: {user.id}</h2>
          <p>Name: {user.name}</p>
          <p>Email: {user.email}</p>
        </div>
      ))}
    </div>
  )
}

export default App

high-ghigh-g

メリデメ整理

メリット

  • APIからの取得データを過不足無くできる
  • 上記によりパフォーマンスが上がる
  • スキーマ駆動
    • API設計の明確化
    • ドキュメンテーションが同時に満たせる
  • クライアント側の取得の自由度が上がる

デメリット

  • 学習コストが高い(?)
  • セキュリティ
    • クエリの柔軟性が高い為、不正クエリが実行される可能性がある
    • クエリのバリエーション、認証、アクセス制御などは適切に設定
  • キャッシュの利用が難しい
  • ツールやライブラリが不足(?)

デメリットは対処すればよいだけの話ですね

high-ghigh-g

現状で分からない、課題となりそうなところ

  • Prismaを使ったMySQLとの連携
  • スキーマで利用できる型の理解
  • Query, Mutation, Subscription, Argumentなどを適切に扱えるか
  • 実運用方法(Apollo Serverを外部に立ててフロントエンドと連携など)

単純に経験不足

簡単な会員登録システムを作るで良さげ

  • Express + Apollo Server + Prisma + MySQL辺りでGraphQLサーバを作成
  • Next.jsでガワ + Apollo Clientの通信部分 をそれぞれ作成