ここまでSupabaseによるアプリ開発方法をいくつかご紹介してきましたが、ぜひ使ってほしい機能が一つあります。
それが『GraphQL』です。
そもそも『GraphQL』とは何か、というところからご説明すると、『GraphQL』はMeta社(旧Facebook)が開発したAPI用のクエリ言語であり、データの取得時に優れたパフォーマンスを発揮します。
通常のREST API等からユーザーの”名前”と”年齢”を取得しようとした場合、ユーザーの情報すべてがAPIから返ってきて、その中から”名前”と”年齢”を抜き出す必要が出てくるでしょう。
しかしこれでは非効率ですよね。
そこで『GraphQL』を利用するならば、ユーザーの”名前”と”年齢”だけを要求し、それだけを受け取ることができるのです。
そのため処理・送信・応答すべてを軽くすることが可能となり、サービス全体のパフォーマンスを上げることができます。
今回はGraphQLでデータを取得して、添付画像のような集計アプリを作ってみましょう。
Supabaseの準備
①:Supabaseプロジェクトの作成
Supabaseにログイン(登録していないければアカウント登録から)して、
トップページの「New Project」を押します。
すると、下記の様な画面が表示されます。
適当なプロジェクト名とデータベースのパスワードを入れて新しいプロジェクトを作成しましょう。
※Regionはできれば自分の住んでいる地域の近くがいいです。私は日本にしました。
②:Supabaseのテーブル作成
今回集計アプリを作成するにあたってユーザ情報のテーブルと売買情報のテーブルを準備します。
まずはユーザ情報のテーブルを作成します。
Supabaseのダッシュボードから`Table Editor`を選択し、`New Table`をクリックします。
添付画像のような設定で作成しましょう。
次に売買情報のテーブルを作成します。
同じように`New Table`まで進み、添付画像のような設定で作成してください。
またユーザIDをユーザテーブルの情報から参照するため、外部キー設定を添付画像のように行ってください。
③:ポリシー作成
不必要かもしれませんが、念のため各テーブルにポリシーを設定します。
両テーブル共に誰でもselect(データ取得)だけできる状態にしたいので、
下記のような設定でポリシーを作成してください。(両方のテーブルに適用する必要があります)
④:テーブルデータのインポート
各テーブルにそれぞれ添付のCSVをインポートしてください。
(自分で適当にデータを作成してもOKです)
users_rows.csv (6.1 kB)
sales_rows.csv (6.3 kB)
⑤:GraphQLのクエリ作成
Supabase側で行う一番重要な作業になります。
GraphQLのクエリにはあくまでCRUD(作成、読み出し、更新、削除)のそれぞれに該当した基本的な機能しかないため、詳細な実装はNext.jsに任せてここではデータの取得のみ行います。
うれしいことにGraphQLは外部キーを参照していると参照先のデータを自動で結び付けてくれるので、今回は`sales`テーブルに対する取得クエリのみ作成します。
まずダッシュボードの`API Docs`を開き、`GraphiQL`にアクセスしてください。
この画面でクエリを試すことができるため、下記のようにクエリを作成し、実行ボタンを押して確認してみましょう。
query SalesQuery($orderBy: [salesOrderBy!], $after: Cursor) {
salesCollection(orderBy: $orderBy, after: $after) {
edges {
cursor
node {
id
created_at
user_id
purchase_date
item_name
price
users {
id
created_at
name
birthday
sex
}
}
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
※Variablesに引数に渡す変数の指定が必要なので、下記のように設定しましょう。
{
"orderBy": {
"id" : "AscNullsLast"
},
"after": null
}
これを実行すると、画面右に実行結果が表示されます。
データを見ればわかる通り、結果のデータは一度に30個までしか表示されないため、次のページのデータを取得するために`PageInfo`を利用して再帰的に実装する必要があります。
詳細は後程Next.jsの実装で行います。
Next.js側の準備
①:Next.jsプロジェクトの作成
任意のディレクトリで、
npx create-next-app -e with-supabase
と打ち込み、
対話的にアプリケーションの名前の設定をして、プロジェクトの作成を行いましょう。
次に環境変数の設定を行います。
作成されたNext.jsプロジェクトの直下に`.env.local.example`ファイルがあるので、
このファイルを`.env.local`にリネームし、
Supabaseのサイトの`Project Settings→API`にある、Project URLとAPI Keyをそれぞれ`.env.local`にコピペしてください。
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=Project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=APIキー
その後、
npm run dev
を実行して添付のような画面が表示されれば、プロジェクトの作成成功です。
②:必要なファイルの作成
今回作りたい画面に合わせて、プロジェクトのファイル構成を変更しましょう。
現状のファイル構成が下記のようになっているかと思いますが、まずは不要なファイルを削除し、必要なファイルと入れ替えて行きましょう。
不要なファイル削除&入れ替え後
削除するファイルは、
- app/auth/callback/route.ts
- app/auth/login/page.tsx
- componentsフォルダの直下すべて
- util/supabaseフォルダのclient.ts以外
- middleware.ts
になります。
新たに作成するファイルは、
- components/GqlGetData.tsx
- components/chart/ChartApp.tsx
- components/chart/ChartUtils.tsx
- gql/constants.ts
- codegen.ts
になります。(一旦ファイルを新規作成するのみで大丈夫です。中身は後で作ります)
多いので一個ずつ画像を見ながら作成すると良いと思います。
では、各ファイルの中身を作成する前に下準備から行いましょう。
③:下準備
必要なファイルをいくつか作成・変更していきます。
utils/supabase/client.ts
Supabaseに共通でアクセスするためのクライアントを作成します。
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
.env.localに入力したURLとAPIキーがここで利用されます。
app/globals.css
tailwindcssのインポート以外は全て消しておきます。
@tailwind base;
@tailwind components;
@tailwind utilities;
④:GraphQLの利用準備
GraphQLのクエリをNext.js側から呼び出すために必要な設定を行います。
https://www.apollographql.com/docs/react/
こちらのApollo Clientを利用するとシンプルにGraphQLを利用できます。
まずは必要なライブラリのインストールを行います。
npm i @apollo/client graphql
npm i -D @graphql-codegen/cli @graphql-codegen/client-preset
型の生成を行うためのコマンドを`package.json`に追加します。
"scripts": {
"compile": "graphql-codegen --require dotenv/config --config codegen.ts dotenv_config_path=.env.local"
}
codegen.ts
次にGraphQLのコード生成用に利用する`codegen.ts`を作成します。
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: {
[process.env.NEXT_PUBLIC_SCHEMA_URL!]: {
headers: {
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
},
},
},
documents: ["**/*.tsx", "**/*.ts"],
generates: {
"./gql/__generated__/": {
preset: "client",
plugins: [],
presetConfig: {
gqlTagName: "gql",
},
},
},
ignoreNoDocuments: true,
};
export default config;
※4月17日追記
schemaでURLパラメータを指定できなくなってしまったため、
schemaの記述を変更し、headerを設定するようにしました。
github: https://github.com/TodoONada/nextjs-supabase-graphql/commit/0e0bce70bb291138fafca4dae8bae473cd8256c6
// before
schema: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/graphql/v1?apikey=${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
// after
schema: {
[process.env.NEXT_PUBLIC_SCHEMA_URL!]: {
headers: {
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
},
},
},
gql/constants.ts
最後に先ほど作成したGraphQLのクエリを定義する記述を作成します。
import { gql } from '@apollo/client'
// コンパイル後はこちらを利用
// import { gql } from "./__generated__";
/** セールステーブル一覧取得クエリー */
export const salesQuery = gql(`
query SalesQuery($orderBy: [salesOrderBy!], $after: Cursor) {
salesCollection(orderBy: $orderBy, after: $after) {
edges {
cursor
node {
id
created_at
user_id
purchase_date
item_name
price
users {
id
created_at
name
birthday
sex
}
}
}
pageInfo {
startCursor
endCursor
hasPreviousPage
hasNextPage
}
}
}
`);
これでコードは作成できたのでコンパイルしましょう。
npm run compile
これを行うとgqlフォルダの直下に`generated`が作成され、その中にファイルが生成されることが確認できると思います。
(生成されたファイルは`.gitignore`に登録しておくとよいです)
⑤:基本レイアウト実装
アプリ全体を覆う`layout.tsx`や`page.tsx`を編集します。
app/layout.tsx
ヘッダーをなくしてシンプルにした形です。
import "./globals.css";
const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
export const metadata = {
metadataBase: new URL(defaultUrl),
title: "Next.js and Supabase Starter Kit",
description: "The fastest way to build apps with Next.js and Supabase",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className="bg-background text-foreground">
<main className="min-h-screen flex flex-col items-center">
{children}
</main>
</body>
</html>
);
}
app/page.tsx
Apollo Clientを利用するための設定を追加しています。
"use client";
import GqlGetData from "@/components/GqlGetData";
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { supabase } from "@/utils/supabase/client";
const httpLink = createHttpLink({
uri: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/graphql/v1`,
});
const authLink = setContext(async (_, { headers }) => {
const session = (await supabase.auth.getSession()).data.session;
return {
headers: {
...headers,
authorization: `Bearer ${
session
? session.access_token
: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
}`,
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
},
};
});
const apolloClient = new ApolloClient({
uri: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/graphql/v1`,
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
export default async function Index() {
return (
<div className="flex-1 w-full flex flex-col gap-4 items-center">
<h1 className="pt-10 text-xl">集計APIのサンプル</h1>
<ApolloProvider client={apolloClient}>
<GqlGetData></GqlGetData>
</ApolloProvider>
</div>
);
}
⑥:主要部分の実装
GraphQLを実行し、チャートで表現する仕組みを作成していきます。
components/chart/ChartUtils.ts
まずはチャートアプリ側で利用する関数群を定義します。
export const getAgeGroup = (birthday: any) => {
const age = Math.floor(
(new Date().getTime() - new Date(birthday).getTime()) / 3.15576e10
);
return Math.floor(age / 10) * 10;
};
export const sortDataMap = (dataMap: Map<number, number>) => {
return new Map(
Array.from(dataMap).sort((a, b) => {
if (a[0] > b[0]) {
return 1;
} else if (a[0] < b[0]) {
return -1;
}
return 0;
})
);
};
export const initAgeDataMap = () => {
const map = new Map<number, number>();
map.set(10, 0);
map.set(20, 0);
map.set(30, 0);
map.set(40, 0);
map.set(50, 0);
return map;
};
export const initMaleOrFemaleMap = () => {
const map = new Map<String, number>();
map.set("りんご", 0);
map.set("みかん", 0);
map.set("バナナ", 0);
return map;
};
誕生日から年代を計算したり、データをソートするなどを行っています。
components/chart/ChartApp.tsx
チャート部分の実装です。
今回チャートをChart.jsを使って作成するため、
下記のライブラリをインストールしてください。
npm install --save chart.js react-chartjs-2
コードはこちらになります。
"use client";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Bar } from "react-chartjs-2";
export default function ChartApp(props: { title: string; chartData: any }) {
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
const options = {
responsive: true,
plugins: {
legend: {
position: "top" as const,
},
title: {
display: true,
text: props.title,
},
},
};
console.log(props.chartData);
if (props.chartData != undefined) {
return (
<div>
<Bar options={options} data={props.chartData} />
</div>
);
} else {
return <div>Loading...</div>;
}
}
components/GqlGetData.tsx
GqlGetDataではGraphQLのクエリ実行→データ整形→チャート表示まで対応しています。
import { useState, useEffect } from "react";
import { gql, useApolloClient } from "@apollo/client";
import { OrderByDirection } from "@/gql/__generated__/graphql";
import { salesQuery } from "@/gql/constants";
import ChartApp from "./chart/ChartApp";
import {
getAgeGroup,
initAgeDataMap,
initMaleOrFemaleMap,
sortDataMap,
} from "./chart/ChartUtils";
// GraphQLからのデータ取得を担う関数
export default function GqlGetData() {
const client = useApolloClient();
const [dataOfAgeLayer, setDataOfAgeLayer] = useState<any>();
const [dataOfMaleOrFemale, setDataOfMaleOrFemale] = useState<any>();
useEffect(() => {
let allSales: any = [];
const fetchSales = async (cursor: any = null) => {
const { data: queryData } = await client.query({
query: salesQuery,
variables: {
orderBy: [
{
id: OrderByDirection.AscNullsLast,
},
],
after: cursor,
},
});
const salesCollection = queryData?.salesCollection;
if (salesCollection != undefined) {
allSales = allSales.concat(
salesCollection.edges.map((edge) => edge.node)
);
if (salesCollection.pageInfo.hasNextPage) {
await fetchSales(salesCollection.pageInfo.endCursor);
}
}
};
// 何歳の人がどの果物を何回買っているか?
const getDataOfAgeLayer = async () => {
const appleMap = initAgeDataMap();
const orangeMap = initAgeDataMap();
const bananaMap = initAgeDataMap();
for (let i = 0; i < allSales.length; i++) {
const saleElement = allSales[i];
const ageGroup = getAgeGroup(saleElement["users"]["birthday"]);
switch (saleElement["item_name"]) {
case "りんご":
const currentAppleValue = appleMap.get(ageGroup);
appleMap.set(ageGroup, currentAppleValue! + 1);
break;
case "みかん":
const currentOrangeValue = orangeMap.get(ageGroup);
orangeMap.set(ageGroup, currentOrangeValue! + 1);
break;
case "バナナ":
const currentBananaValue = bananaMap.get(ageGroup);
bananaMap.set(ageGroup, currentBananaValue! + 1);
break;
default:
break;
}
}
const labels = ["10", "20", "30", "40", "50"];
const appleData: any[] = [];
const orangeData: any[] = [];
const bananaData: any[] = [];
appleMap.forEach((value, key) => {
appleData.push(value);
});
orangeMap.forEach((value, key) => {
orangeData.push(value);
});
bananaMap.forEach((value, key) => {
bananaData.push(value);
});
const chartData = {
labels,
datasets: [
{
label: "リンゴ",
data: appleData,
backgroundColor: "rgba(255, 0, 0, 0.5)",
},
{
label: "みかん",
data: orangeData,
backgroundColor: "rgba(0, 255, 0, 0.5)",
},
{
label: "バナナ",
data: bananaData,
backgroundColor: "rgba(0, 0, 255, 0.5)",
},
],
};
setDataOfAgeLayer(chartData);
};
// 各果物につき男性女性どちらが何回買っているか
const getDataOfMaleOrFemale = async () => {
const maleMap = initMaleOrFemaleMap();
const femaleMap = initMaleOrFemaleMap();
for (let i = 0; i < allSales.length; i++) {
const saleElement = allSales[i];
switch (saleElement["users"]["sex"]) {
case 0:
const currentMaleValue = maleMap.get(saleElement["item_name"]);
maleMap.set(saleElement["item_name"], currentMaleValue! + 1);
break;
case 1:
const currentFemaleValue = femaleMap.get(saleElement["item_name"]);
femaleMap.set(saleElement["item_name"], currentFemaleValue! + 1);
break;
default:
break;
}
}
const labels = ["りんご", "みかん", "バナナ"];
const maleData: any[] = [];
const femaleData: any[] = [];
maleMap.forEach((value, key) => {
maleData.push(value);
});
femaleMap.forEach((value, key) => {
femaleData.push(value);
});
const chartData = {
labels,
datasets: [
{
label: "男性",
data: maleData,
backgroundColor: "rgba(0, 0, 255, 0.5)",
},
{
label: "女性",
data: femaleData,
backgroundColor: "rgba(255, 0, 0, 0.5)",
},
],
};
setDataOfMaleOrFemale(chartData);
};
const init = async () => {
await fetchSales();
await getDataOfAgeLayer();
await getDataOfMaleOrFemale();
};
init();
}, [client]);
return (
<div>
<ChartApp
title="何歳代の人が累計何回購入したか?"
chartData={dataOfAgeLayer}
></ChartApp>
<ChartApp
title="各果物を男性、女性がそれぞれ何個買ったか?"
chartData={dataOfMaleOrFemale}
></ChartApp>
</div>
);
}
`fetchSales`関数でsalesテーブルのデータをGraphQLのクエリを実行して取得します。
次のページがあったら再帰的に実行する仕組みです。
const { data: queryData } = await client.query({
query: salesQuery,
variables: {
orderBy: [
{
id: OrderByDirection.AscNullsLast,
},
],
after: cursor,
},
});
詳細な実装の説明は割愛しますが、`getDataOfAgeLayer`と`getDataOfMaleOrFemale`でチャート表示に必要な処理を行っています。
実装確認
では`npm run dev`で実際に実行してみましょう。
ローディングが終わると下記のようにチャートが二つ表示されていることがわかるかと思います。
また、コンソールを見るとデータがどのような形でチャートに追加されているかが表示されているため、確認してみてください。
これで今回の実装は完了です。GraphQLでデータを扱う方法が理解できたかと思います。
更新や削除などもGraphQLから実行できるため、是非試してみてください。
その他参考資料
またTodoONada株式会社では、この記事で紹介した以外にも、各種チャットシステムやストレージを利用した画像アプリの作成方法についてご紹介しています。
下記記事にて一覧を見ることができますので、ぜひこちらもご覧ください!https://note.com/embed/notes/n522396165049
お問合せ&各種リンク
- お問合せ:GoogleForm
- ホームページ:https://libproc.com
- 運営会社:TodoONada株式会社
- Twitter:https://twitter.com/Todoonada_corp
- Instagram:https://www.instagram.com/todoonada_corp/
- Youtube:https://www.youtube.com/@todoonada_corp/
- Tiktok:https://www.tiktok.com/@todoonada_corp
presented by