概要
現在弊社で開発中のサービス要件を満たす技術選定の際に、データ取得の方法としてRESTFul APIの他に選択肢としてGraphQLが挙げられることがありました。
その時点でのGraphQLの理解がJSON(のようなもの?)を返すAPIみたいなものという
なんともふわっとしたものだったので、これはいけないと思い年末年始に少し触ってみました。
開発環境
- yarn 1.22.19
- Typescript 4.9.3
- Vue.js 3.2.45
- vite 4.0.0
- graphql 16.6.0
etc...
GraphQLとは
GraphQLとはAPI向けのクエリ言語です。
CircleCIブログGraphQLとは?メリットや概要を入門ガイドで学ぶでは以下のようにまとめられています。
- GraphQLは、クライアントアプリケーションが必要なデータをAPIからフェッチするために設計された言語です。
- GraphQLを使用すると、クライアントアプリケーションはバックエンドAPIから必要なデータの型とシェイプを取得できます。
- GraphQLでは、どんなタイプのリクエストでも、クライアント アプリケーションが呼び出すエンドポイントは1つだけです。
- GraphQLはSQLとよく似ていますが、フロントエンドで機能します。
なんとなく分かってきましたが、まだ使い所がよく分かりませんね。
GraphQLの比較対象としてRESTFul APIが挙げられますが、それぞれどのように違うのか、以下で説明します。
RESTFul APIとの違い
RestFul API
詳細はRESTful API とはに記載がありますが、特徴の1つとして取得するリソースを特定するために、URIやGET・POSTなどリクエストメソッド等の情報が必要です。
従って、複数のリソースを取得するには複数のリクエスト(URIなど)が必要となります。
以下にRESTFul APIのリソース取得フローを示します。
図鑑No.25のポケモンの名前と種族値を取得しています。
{
"id": 25,
"name": "Pikachu"
}
{
"hitPoints": 35,
"attack": 55,
"defense": 40,
"specialAttack": 50,
"specialDefense": 50,
"speed": 90
}
GraphQL
次に、GraphQLで同じデータを取得するフローを示します。
エンドポイントが1つのみであり、取得するリソースはクエリによって決定します。
POSTするクエリは以下の通りです。
query{
pokemon(id: 25) {
id
name
basestats {
hitPoints
attack
defense
specialAttack
specialDefense
speed
}
}
}
"pokemon" {
"id": 25,
"name": "Pikachu",
"basestats": {
"hitPoints": 35,
"attack": 55,
"defense": 40,
"specialAttack": 50,
"specialDefense": 50,
"speed": 90
}
}
このようにGraphQL APIでは1回のリクエストでアプリが必要とするすべてのデータを取得することができます。
GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それをPOSTします。
上記の例では、API呼び出しを2つ使うのではなく、APIへのリクエスト1つでポケモンと種族値をネストされたJSONオブジェクトとして取得しています。
どのような時に使用するか
上記で示した通り、GraphQLの大きな利点は
- クエリによって、欲しいデータのみを全て取得することができる
- その際のAPIへのリクエストは一度だけでよい
- エンドポイントが単一である
ということです。
例えば、関連するデータが多くRESTFul APIでは何回も呼び出しが必要な場合は、GraphQLの利用を検討するとよいでしょう。
呼び出し回数が削減され、サービスのパフォーマンス向上が期待できます。
Vue.jsからGraphqlを利用してデータ取得・絞り込み検索を実装
それでは、実際に使用してみます。
このあたりの記事が参考になりました。
GraphQL APIをVueJSフロントエンドに接続する方法
今回は練習として、ポケモンの名前、タイプ、画像を取得し、表示します。
GraphQLのエンドポイントとして、Pokemon APIのものを使用します。
https://graphql-pokemon2.vercel.app
ちなみに、練習用に使用できるGraphQL APIはこちらにあります。
8 Free to Use GraphQL APIs for Your Projects and Demos
GraphQL Playgroundに上記のエンドポイントを入力してみましょう。
すると、以下のようにSchema定義などが確認できます。
type Attack {
name: String
type: String
damage: Int
}
type Pokemon {
id: ID!
number: String
name: String
weight: PokemonDimension
height: PokemonDimension
classification: String
types: [String]
resistant: [String]
attacks: PokemonAttack
weaknesses: [String]
fleeRate: Float
maxCP: Int
evolutions: [Pokemon]
evolutionRequirements: PokemonEvolutionRequirement
maxHP: Int
image: String
}
type PokemonAttack {
fast: [Attack]
special: [Attack]
}
type PokemonDimension {
minimum: String
maximum: String
}
type PokemonEvolutionRequirement {
amount: Int
name: String
}
type Query {
query: Query
pokemons(first: Int!): [Pokemon]
pokemon(id: String, name: String): Pokemon
}
これを参考に実装を進めます。
まずは、以下のようにapollo関連のパッケージをインストールします。
Apolloは、アプリでGraphQLを使うためのツール群であり、コミュニティの取り組みでもあります。
Vue Apollo
yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag @vue/apollo-composable
最終的なpackage.jsonは以下の通りです。
{
"name": "vue-graphql",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/express": "^4.17.15",
"@vue/apollo-composable": "^4.0.0-beta.1",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
"apollo-link-http": "^1.5.17",
"express": "^4.18.2",
"express-graphql": "^0.12.0",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"vue": "^3.2.45",
"vue-apollo": "^3.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vue-tsc": "^1.0.11"
}
}
次に、GraphQLを使用する準備をします。
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';
// GraphQL APIへの接続を確立
const httpLink = new HttpLink({
uri: 'https://graphql-pokemon2.vercel.app'
});
// ApolloClientインスタンス生成
export const apolloClient = new ApolloClient({
// データが正しいAPIからポーリングされるように設定
link: httpLink,
// キャッシュを渡す。InMemoryCacheはApolloClientのデフォルトのキャッシュ実装であるのでこれを使用
cache: new InMemoryCache(),
// Apollo Client Devtoolsを、Webブラウザのインスペクタに「Apollo」タブとして表示する
connectToDevTools: true
});
const apolloProvider = new VueApollo({
defaultClient: apolloClient
});
const app = createApp(App);
// VueApolloをvueで使用する
app.use(apolloProvider);
app.mount('#app');
今回取得するポケモンのデータの型定義をしておきます。
export type Pokemon = {
name: string;
types: string[];
image: string;
}
検索フォームのコンポーネントをApp.vueから読み込みます。
<script setup lang="ts">
import PokemonForm from './components/PokemonForm.vue';
</script>
<template>
<div>ポケモン検索したい!</div>
<pokemon-form />
</template>
検索フォームを、ポケモンの名前(英語表記)での絞り込みができるよう実装します。
コンポーネントのsetup()外でuseQuery()を使用したい場合、
provideApolloClient(apolloClient);
が必要です。
ドキュメントによるとこれは、Vueのprovide/injectメカニズムでクライアントをインジェクトできないからのようです。
Usage outside of setup
使用しない場合は以下のエラーが発生します。
Uncaught Error: Apollo client with id default not found. Use provideApolloClient() if you are outside of a component setup.
<template>
<form>
<input type="text" placeholder="Name" v-model="name">
<input type="button" value="検索" @click="(e) => {pokemonQuery(name, e)}">
</form>
<pokemon-list v-if="fetchdata" :pokemons="fetchdata.pokemons" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useQuery, provideApolloClient } from '@vue/apollo-composable';
import gql from "graphql-tag"
import { apolloClient } from '../main';
import PokemonList from "./PokemonList.vue"
import { Pokemon } from '../model/pokemon';
const name = ref<string>();
type Query = {
pokemons: Pokemon[]
} | undefined;
const fetchdata = ref<Query>();
// https://v4.apollo.vuejs.org/guide-composable/setup.html#usage-outside-of-setup
//
provideApolloClient(apolloClient);
const getPokemonByName = (name: string) => {
console.log("getpPokemonByName");
const query = gql`
query($name: String) {
pokemon(name: $name) {
name
types
image
}
}
`;
const {onResult} = useQuery<Query>(query, {
name
});
onResult(result => {
fetchdata.value = {pokemons: [result.data.pokemon]};
});
};
const getAllPokemons = () => {
const query = gql`
query {
pokemons(first: 150){
name
types
image
}
}
`;
const {onResult} = useQuery<Query>(query);
onResult(result => {
fetchdata.value = result.data;
});
};
const pokemonQuery = (name: string | undefined, event: MouseEvent) => {
event.preventDefault();
if (!name) {
getAllPokemons();
return;
};
getPokemonByName(name);
};
getAllPokemons();
</script>
このとき、結果の取得とfetchdataの更新は以下のように行なっています。
const {onResult} = useQuery<Query>(query, {
name
});
onResult(result => {
fetchdata.value = {pokemons: [result.data.pokemon]};
});
注意点として、以下のようにresultプロパティを取得することも可能ですが、fetchdataの更新ができずundefinedとなってしまいます。
これは、useQueryによるデータ取得は非同期処理であるため、データ取得が完了する前に代入処理を実行してしまうからです。
hogehoge() => {
const {result} = useQuery<Query>(query, {
name
});
fetchdata.value = {pokemons: [result.data.pokemon]};
}
次に、ポケモンのリスト表示用コンポーネントを実装します。
初回表示時には、150匹のポケモンを表示するためポケモン1種類ごとにループさせています。
<script setup lang="ts">
import pokemonItem from './pokemonItem.vue';
import { Pokemon } from '../model/pokemon';
const {pokemons} = defineProps<{pokemons: Pokemon[]}>();
</script>
<template>
<div v-for="pokemon in pokemons">
<pokemon-item :pokemon="pokemon" />
</div>
</template>
最後にポケモン1種類を表示するコンポーネントを実装します。
タイプはStringの配列であるため、join()で連結して表示しています。
<script setup lang="ts">
import { Pokemon } from '../model/pokemon';
const {pokemon} = defineProps<{pokemon: Pokemon}>();
</script>
<template>
<div>
<h3>{{ pokemon.name }}</h3>
<p>{{ pokemon.types.join("・") }}</p>
<img :src="pokemon.image" width="100">
</div>
</template>
完成!
GraphQLを使用する上での注意点
詳細はGraphQLを導入する時に考えておいたほうが良いことに、GraphQLの恩恵を受けるために考慮すべき点とその対策が記載されています。
上記の記事で紹介されている課題のうちいくつか例をあげると、
- データを取得する際、N+1問題が起こる場合がある
- クエリが複雑になるとリクエストボディが肥大化する
- POSTを利用するためHTTPキャッシュに乗らない
などがあります。
まとめ
GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それをPOSTします。
RESTFul APIに比べて、API呼び出しの回数が削減され、サービスのパフォーマンスの向上が期待できます。
一方で、愚直な実装をすると十分にGraphQLの恩恵を受けられないことがあるため、今後も勉強していきたいですね。