GraphQL
モチベーション
- GraphQLの概要
- ざっくりとした利用方法
- Nodeのみで利用
- Prismaとの連携
- Reactとの連携
- ApolloClient
- ApolloProvider
- useQuery
- 案件利用の場合
- GraphQLサーバを立てるには
- Next.js + GraphQLを利用するには
GraphQLの入門情報
GraphQL + React
GraphQLサーバを立てる方法
その他
Overfetching: レスポンスが不要なデータも多く含んでしまうケース
Underfetching: 一度のリクエストで全ての必要なデータを取得することができないため、追加でリクエストを送信する必要があるケース
これらの問題を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サーバを構築
モジュールインストール
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')
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が立ち上がる
Apollo Studioの初期画面
以下のクエリを実行する
query Query {
hello
}
結果が返ってくる
クエリに引数を渡す
const typeDefs = gql`
type Query {
hello(name: String!): String
}
`
この時点ではApollo Studioはエラー
Queryタイプで引数を設定した場合、リゾルバでも
↓から
オブジェクトタイプには特別なタイプである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"
}
query Query($userId: ID!) {
user(id: $userId) {
name
}
}
ここから
外部のデータソースを取得して利用
yarn add axios
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
}
}
外部データソース読み込みを行う場合もGraphQLのエンドポイントは変わらない為、
フロントエンド側の変更の必要はなく、常にGraphQLを変更するだけで良い
スキーマの追加 POST
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
}
}
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
次はここから
リゾルバの引数の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
},
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
},
次はここから
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
npmスクリプトに以下を記述
"studio": "prisma studio"
yarn studioでprisma studioが立ち上がる
npx prisma generate
prisma clientがインストールされる
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
}
}
次ここから
read operations → Queryタイプ
wite operations → Mutationタイプ
型
type Mutation {
createUser(name!: String!, email: String!): User
}
リゾルバ
Mutation: {
createUser: (_, args) => {
return prisma.user.create({
data: {
name: args.name,
email: args.email,
},
})
},
},
Apollo StudioでMutation実行
mutation Mutation($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
values は以下の形
{
name: "値"
email: "値"
}
メモ
- クエリの引数に対して型を書く
- クエリの引数は$をつけて書く
- リゾルバの引数で、プロップス: $引数の形で記述
- {} 内にリゾルバに渡す項目を書く
Prismaスタジオを立ち上げると追加されていることを確認
usersクエリの実行でも追加を確認
query Query {
users {
id
name
email
}
}
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のクエリで表現する。
更新のオペレーション
mutation Mutation($name: String!, $updateUserId: Int!) {
updateUser(name: $name, id: $updateUserId) {
id
name
}
}
varaibles
{
"name": "update_texttttttttttttt",
"updateUserId": 1
}
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
}
ここまででCRUD系操作が完了
フロントエンドからの接続
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 = ``
}
fetchBook()
</script>
ok
つぎここから
FetchでもGraphQLサーバとの通信は可能だが、
Apollo Clientを利用したほうが効率的。
Apollo Clientを利用することで、useQueryなどの実装をする際に便利なHooksを利用可能。
React, Vue, Svelteに対応しているライブラリ。
Apollo Client Devtoolsを利用すると便利
Query, Mutationの中身、キャッシュの中身を確認できる。
Reactプロジェクトを作る
npm create vite
TypeScript + SWCを選択する
cd vite-project
npm i
npm run dev
動作確認ok
ライブラリインストール
npm i @apollo/client graphql
ここから
コードとしては以下のようなシンプルな構造でいけた
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 />)
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>
)
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
改めて見てる
Subscription, Argument系
Subscription
- 通知系
Argument
- 構造整理系
- こっちはもう使ってた
メリデメ整理
メリット
- APIからの取得データを過不足無くできる
- 上記によりパフォーマンスが上がる
- スキーマ駆動
- API設計の明確化
- ドキュメンテーションが同時に満たせる
- クライアント側の取得の自由度が上がる
デメリット
- 学習コストが高い(?)
- セキュリティ
- クエリの柔軟性が高い為、不正クエリが実行される可能性がある
- クエリのバリエーション、認証、アクセス制御などは適切に設定
- キャッシュの利用が難しい
- ツールやライブラリが不足(?)
デメリットは対処すればよいだけの話ですね
現状で分からない、課題となりそうなところ
- Prismaを使ったMySQLとの連携
- スキーマで利用できる型の理解
- Query, Mutation, Subscription, Argumentなどを適切に扱えるか
- 実運用方法(Apollo Serverを外部に立ててフロントエンドと連携など)
単純に経験不足
↓
簡単な会員登録システムを作るで良さげ
- Express + Apollo Server + Prisma + MySQL辺りでGraphQLサーバを作成
- Next.jsでガワ + Apollo Clientの通信部分 をそれぞれ作成