GraphQLの採用についての考察

## はじめに

技術調査として前から気になっていたGraphQLを調査したので今後、新規プロジェクトにおいて採用できるかの考察をまとめておきます。
※ 今回は、REST APIと比較してGraphQLの利点と使いどろこをまとめています

## 検証環境

- OS: macOS
- python 3.9
- Web Framework: FastAPI
- Front: React.js & Material UI

## GraphQLとは
[GraphQL | A query language for your API](https://graphql.org/)
Facebookが開発元のオープンソースでREST APIと同じく特定エンドポイントにリクエストをしてレスポンスを受け取る仕組みにかわりはないのですが以下の点が違います


**・Rest API**
同一エンドポイントに対してデータ取得・登録・更新などの処理をGET・POST・PATCHなどのHTTPメソッドの使いわけによって行う。
また以下のユーザー情報に関してのリソースに対しては`/user`というパスになっているが、
これが、ECサイトの様にカートに対してのデータにアクセスする場合は `/cart`みたいにエンドポイントが増えていく。

```
curl -x GET http://localhost:3000/user/{id} （ユーザー情報取得）
curl -x POST -d '{ "uesr_name": "yamada", "first_anme": "Hanako", "last_name": "Yamada"} http://localhost:3000/user （ユーザー登録） 
curl -x PATCH -d '{ id:3, "last_name": "Tanaka"} http://localhost:3000/user （ユーザー情報更新） 
```
![Screen Shot 2021-02-25 at 10.15.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96623/0da8fbdf-d223-baa0-59e6-52d4b006cd34.png)
**・GraphQL**
全てのリソース（ユーザー情報・カートデータ）に対して同一エンドにリクエストを行う。
また、取得・登録・更新などの処理は**オペレーション型**と言われる、データ取得系のquery、データ更新系のmutation、サーバーサイドのイベントを受け取るsubscriptionをリクエストのボディに定義して行う

```
Endpoint: http://localhost:3000/graphql
# Full filed
query {
  getUser(id: 1) {
    id
    userName
    firstName
    lastName
  }
}

query {
  getUser(id: 1) {
    userName
    firstName
    lastName
  }
}


# Create & update
mutation createUser {
  createUser(newUser: {
    userName: "h-tanaka",
    firstName: "hanako",
    lastName: "Tanaka",
  })
  {
    id
    userName
		firstName
    lastName
  }
}
```

![Screen Shot 2021-02-25 at 10.58.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96623/6fe7e82e-80ab-22cb-79a4-3db131f7fec4.png)

## FastAPIで実際に試してみる
インストールからコーディングまでの手順は省略させて頂きます
上記に関してはこちらの記事を参考にして頂ければと思います。
[安全なGraphQL API開発が出来るって本当？ FastAPI+GraphQL+テストで安全なAPI開発](https://qiita.com/satto_sann/items/6f277c223d9acb250781)

```python:app.py

from fastapi import FastAPI
import graphene
from starlette.graphql import GraphQLApp
from .graph import schema
from .routers import user

app = FastAPI()

app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=schema.Query, mutation=schema.Mutation)))

app.include_router(user.router)
```

```python:schema.py

import graphene
from .serializers import UserInfoModel, UserModel, UserCreateModel

class Query(graphene.ObjectType):
    get_user = graphene.Field(graphene.List(UserInfoModel), id=graphene.NonNull(graphene.Int))

    @staticmethod
    def resolve_get_user(parent, info, id):
        return[
            UserModel(
                id=1,
                user_name='t-yamada',
                first_name='taro',
                last_name='yamada'
            )
        ]


class CreateUser(graphene.Mutation):
    class Arguments:
        new_user = UserCreateModel()

    Output = UserInfoModel

    @staticmethod
    def mutate(parent, info, new_user):
        print(new_user)

        return UserModel(
                id=1,
                user_name='k-yamada',
                first_name='kenji',
                last_name='yamada'
            )

class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()
```

```python:serializers.py
from typing import List, Optional
from graphene_pydantic import PydanticInputObjectType, PydanticObjectType
from pydantic import BaseModel

class UserModel(BaseModel):
    id: Optional[int]
    user_name: str
    first_name: str
    last_name: str

class UserInfoModel(PydanticObjectType):
    class Meta:
        model = UserModel


class UserCreateModel(PydanticInputObjectType):
    class Meta:
        model = UserModel
        exclude_fields = ('id', )
```


## 試してみて
1. 管理画面において複数グラフを表示する場合にRestだと複数エンドポイントにリクエストをしないといけないのがGraphQLだと一回で取得できそう
2. Restだとレスポンスのフィールドが固定されている為、Reactなどで汎用的なテーブルコンポーネントを作成した場合にデータの間引きをする処理が必要になるが、GraphQLだと必要なフィールドのみを取得する事ができるのでフロントの実装が楽になりそう。

```
query {
  getUser(id: 1) {
    id
    userName
    firstName
    lastName
  }
}
// idを含めたくない場合のQuery
query {
  getUser(id: 1) {
    userName
    firstName
    lastName
  }
}
```

- 汎用的なテーブルコンポーネント

```ts:table.tsx
import React from 'react'
import Table, { Size } from '@material-ui/core/Table'
import TableBody from '@material-ui/core/TableBody'
import TableCell from '@material-ui/core/TableCell'
import TableHead from '@material-ui/core/TableHead'
import TableRow from '@material-ui/core/TableRow'
import Button from '@material-ui/core/Button'
import EditIcon from '@material-ui/icons/Edit'
import styled from 'styled-components'
import { Rows, Row, RowValue, Headers } from '~/types'

type Props = {
  headers: Headers
  editable?: boolean
  size?: Size
  rows: Rows
  onClick?: (key?: number) => void
}

function mapRows(rows: Rows, headers: Headers): Rows {
  const columns = []
  for (const header of headers) {
    columns.push(header.accessor)
  }
  const newRows = rows.map((row) => {
    const newHash: Row = { key: null, value: null }
    const newValue: RowValue = {}
    for (const key of columns) {
      newValue[key] = row.value[key]
    }
    newHash['value'] = newValue
    newHash['key'] = row.key
    return newHash
  })
  return newRows
}

const DataGridView: React.FC<Props> = (props) => {
  const size = props.size || 'small'
  const isEditable = props.editable || false
  const newRows = mapRows(props.rows, props.headers)
  return (
    <React.Fragment>
      <DataTable stickyHeader aria-label="sticky table" size={size}>
        <TableHead>
          <TableRow>
            {props.headers.map((value) => (
              <TableCell key={value.accessor}>{value.header}</TableCell>
            ))}
            {isEditable && <TableCell />}
          </TableRow>
        </TableHead>
        <TableBody>
          {newRows.map((row) => (
            <TableRow key={row.key}>
              {Object.keys(row.value).map((key) => (
                <TableCell key={key}>{row.value[key]}</TableCell>
              ))}
              <TableCell>
                {isEditable && (
                  <Button
                    size="small"
                    color="primary"
                    onClick={() => props.onClick(row.key)}
                  >
                    <EditIcon fontSize="small" />
                  </Button>
                )}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </DataTable>
    </React.Fragment>
  )
}

const DataTable = styled(Table)`
  min-width: 750px;
  .MuiTableCell-stickyHeader {
    background: #f3f3f3;
  }
`

export default DataGridView
```


## 考察
まだまだQuery（取得）系以外のものに関しては調査等が必要だが、リアクトなどでフロントを作成する場合は[SWR](https://swr.vercel.app/)などがGraphQLをサポートしてるのもあり、Rest APIと同時運用で特定ページのみGraphQLを採用し、徐々に移行しながら知見を深めていくのも良いかと感じました。
適用するサービスによりますが、社内システム等なら比較的採用しやすいのではと。

今回は、比較的ライトに書かせて頂きましたが。今後また知見が深まった段階で記事を更新していければと思います。