【GraphQL編】React+Djangoで開発するWeb API

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件が未完了状態です

管理画面よりGraphQLでリクエスト

次のクエリを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を起動すると結果が表示されます

GraphQLから取得した結果をリストに表示

まとめ

今回作成した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;
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次