Web APIにはRESTやGraghQL、gRPCがあるけどどう使い分ければ良いだろう?
前回の記事でRESTについて解説しました
今回はGraphQLの特徴と実装方法について解説します
- Visual Studio Code:1.76.0
- Docker Desktop
DjangoとReactのプロジェクトは前回と同じものを使用しますので詳細はそちらを参照してください
GraphQLとは?
GraphQLとはMeta(旧:Facebook)が2012年に作成し、2015年にオープンソース化されたAPI向けに作られたクエリ言語およびクエリを実行するサーバーサイドの実装(ランタイム)のことです
従来のRESTにはオーバーフェッチとアンダーフェッチという問題がありました
- オーバーフェッチとは?
-
オーバーフェッチとは実際に必要としているよりも多くのデータを取得してしまうことによってネットワークリソースや計算リソースを無駄にしてしまうこと
- アンダーフェッチとは?
-
アンダーフェッチとは必要なデータを取得するために何度もサーバーにリクエストを投げることにより、サーバーとの間に不必要なラウンドトリップが発生し、特にモバイルや低速ネットワーク回線においてレイテンシが増加してしまうこと
スタートアップ時にフロントエンドエンジニアとバックエンドエンジニアが同じチームであればREST APIでもさほど困りませんが、時間が経過するにつれてこのような問題が発生するケースというのはRESTを利用したことのあるエンジニアであればあるあるではないでしょうか
このような問題を解決できることから、GraphQLの採用は増加しており、今後も普及が期待されています
GraphQLの特徴としては次の通りです
- アプリケーションが呼び出すエンドポイントが1つ
- 柔軟なリクエストで最小限のレスポンス
- 1回のリクエストで複数のリソースにアクセス可能
- スキーマによる型付けにより、堅牢な開発ができる
Graphene-Django
まずはDjangoのプロジェクト側で作業します
次のコマンドでGraphene-Djangoをインストールします
pip install graphene-django
settings.pyにGraphene-Djangokのアプリケーション登録を行います
INSTALLED_APPS = [
...
"graphene_django",
]
スキーマ(API仕様)を定義するためにapi配下にschema.pyを作成します
テーブルは前回作成したTodoを使用しますので今回は作成しません
import graphene
from graphene_django import DjangoObjectType, DjangoListField
from .models import Todo
class TodoType(DjangoObjectType):
class Meta:
model = Todo
fields = ("task", "timestamp", "completed")
class Query(graphene.ObjectType):
todo_list = graphene.List(TodoType)
def resolve_todo_list(self, info, **kwargs):
return Todo.objects.all()
スキーマを簡単に説明しますとDjangoObjectTypeで使用するModelと利用可能なフィールドの定義を行います
QueryクラスでGraphQLクエリの実装を行います
graphene.Listは一覧形式で返却することを意味しており、resolve_xxxにてTodoテーブルの全件を返却します
特定の項目でフィルタしたい場合はgraphene.Fieldを利用するのですが、これで定義すると単一のレコードしか返却できません
ListとFieldの両方の特徴を利用したい場合はDjangoListFieldを使用します
todo_field = graphene.Field(
TodoType, completed=graphene.Boolean(required=True))
todo_list_field = DjangoListField(
TodoType, completed=graphene.Boolean(required=True))
def resolve_todo_field(self, info, **kwargs):
value = kwargs.get('completed')
return Todo.objects.filter(completed=value).first()
def resolve_todo_list_field(self, info, **kwargs):
value = kwargs.get('completed')
return Todo.objects.filter(completed=value)
completedの項目でフィルタできるようにしたクエリを2つを作成しました
これらのうち、React側からはtodo_list_fieldを使用してタスクが完了していないTodoを取得するようにします
prj配下にもschema.pyを作成します
import graphene
import api.schema
class Query(api.schema.Query,
graphene.ObjectType):
pass
schema = graphene.Schema(query=Query)
schemaを使えるようにsettings.pyに定義します
GRAPHENE = {
'SCHEMA': 'project.schema.schema'
}
projectの個所はdjangoのproject名になります
urls.pyにGraphQL用のURLを定義します
GraphQLではエンドポイントが1つだけのため、他のクエリが必要になった場合でも変更は不要になります
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
urlpatterns = [
...
path("graphql/", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]
csrf_exemptはCSRF対策を無効にするために使用します
これを使用しないとフロントエンドのReactからリクエストができません(方法はあるかもしれませんが…)
GraphQLの実装は完了です
サーバを起動して/graphqlでアクセスするとGraphQLクエリをリクエストすることできます
Todoテーブルには3件登録されており、そのうち2件が未完了状態です
次のクエリをReact側から実行することにします
query {
todoListField(completed:false) {
task,
timestamp,
completed
}
}
デフォルトだとフィールド名は自動的にキャメルケースに変換されます
利用する定義とフィルタの条件、取得する項目名を指定してクエリを作成します
ReactのApollo
続いてReactのプロジェクト側で作業します
次のコマンドでGraphQLを利用できるようにApolloをインストールします
npm install @apollo/client graphql
App.tsxにApolloクライアントを作成します
ApolloProviderに作成したclientを渡すことでApolloProvider内でGraphQLを利用できるようになります
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://127.0.0.1:8000/graphql/',
}),
});
return (
...
<ApolloProvider client={client}>
...
</ApolloProvider>
...
);
GraphQLから取得した結果を表示するコンポーネントを作成します
import { gql, useQuery } from '@apollo/client';
const GET_TODOS = gql`
query {
todoListField(completed: false) {
task
timestamp
completed
}
}
`;
const { loading, error, data } = useQuery(GET_TODOS);
if (loading) return <p>...loading</p>;
if (error) return <p>{error.message}</p>;
API通信部分のみ抜粋しています。全ソースはまとめを参照してください
作成したGraphQLクエリをuseQueryに渡すことでReactフックが自動的にデータを取得します
DJangoが起動している状態でReactを起動すると結果が表示されます
まとめ
今回作成したReact側のソースです
App.tsx
import {
ApolloClient,
ApolloProvider,
HttpLink,
InMemoryCache,
} from '@apollo/client';
import { Card, CardContent, Container } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import * as React from 'react';
import CustomList from './components/CustomList';
function App() {
const theme = createTheme();
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://127.0.0.1:8000/graphql/',
}),
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container fixed>
<Card variant="elevation" elevation={0}>
<CardContent>
<ApolloProvider client={client}>
<CustomList />
</ApolloProvider>
</CardContent>
</Card>
</Container>
</ThemeProvider>
);
}
export default App;
CustomList.tsx
import { gql, useQuery } from '@apollo/client';
import AddTaskIcon from '@mui/icons-material/AddTask';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import React from 'react';
import { Todo } from '../global';
function CustomList() {
const GET_TODOS = gql`
query {
todoListField(completed: false) {
task
timestamp
completed
}
}
`;
const { loading, error, data } = useQuery(GET_TODOS);
if (loading) return <p>...loading</p>;
if (error) return <p>{error.message}</p>;
return (
<List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
<ListSubheader
component="div"
sx={{ backgroundColor: 'black', color: 'white' }}
>
Task List
</ListSubheader>
{data.todoListField.map((todo: Todo) => (
<ListItem
key={todo.task}
secondaryAction={
<IconButton edge="end" aria-label="comments">
<AddTaskIcon />
</IconButton>
}
>
<ListItemText primary={todo.task} secondary={todo.timestamp} />
</ListItem>
))}
</List>
);
}
export default CustomList;