はじめに
技術調査として前から気になっていた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
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 (ユーザー情報更新)
・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
}
}
FastAPIで実際に試してみる
インストールからコーディングまでの手順は省略させて頂きます
上記に関してはこちらの記事を参考にして頂ければと思います。
安全なGraphQL API開発が出来るって本当? FastAPI+GraphQL+テストで安全なAPI開発
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)
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()
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', )
試してみて
- 管理画面において複数グラフを表示する場合にRestだと複数エンドポイントにリクエストをしないといけないのがGraphQLだと一回で取得できそう
- Restだとレスポンスのフィールドが固定されている為、Reactなどで汎用的なテーブルコンポーネントを作成した場合にデータの間引きをする処理が必要になるが、GraphQLだと必要なフィールドのみを取得する事ができるのでフロントの実装が楽になりそう。
query {
getUser(id: 1) {
id
userName
firstName
lastName
}
}
// idを含めたくない場合のQuery
query {
getUser(id: 1) {
userName
firstName
lastName
}
}
- 汎用的なテーブルコンポーネント
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などがGraphQLをサポートしてるのもあり、Rest APIと同時運用で特定ページのみGraphQLを採用し、徐々に移行しながら知見を深めていくのも良いかと感じました。
適用するサービスによりますが、社内システム等なら比較的採用しやすいのではと。
今回は、比較的ライトに書かせて頂きましたが。今後また知見が深まった段階で記事を更新していければと思います。