前回
Next.jsのチュートリアルをやってみる【データベースの設定編】はじめに
前回はデータベースの作成と初期データの追加をいたしました。今回は、アプリケーションのデータを取得する方法について学習していきます。
データの取得方法
APIレイヤー
APIはアプリケーションコードとデータベースの仲介レイヤーです。APIを利用するケースは以下の通りです。
- APIを提供するサードパーティーのサービスを利用する場合
- クライアントからデータを取得する場合にデータベースの秘密がクライアントに公開されるのを避けるために、サーバー上で動作するAPIレイヤーを用意したい場合
Next.jsではルートハンドラーを使用してAPIエンドポイントを作成することができます。
データベースクエリ
フルスタックのアプリケーションを作成するときは、データベースとやり取りをするロジックを書く必要があります。Postgresのようなリレーショナルデータベースの場合は、SQLやORMを使用することもできます。
以下の場合はクエリを書かなければなりません。
- APIエンドポイントを作成する場合は、データベースとやり取りするためのロジックを必要とする場合
- React Server Components(サーバー上でデータを取得する)を使用している場合は、APIレイヤーをスキップして、データベースの秘密をクライアントに公開するリスクを冒すことなく、データベースに直接問い合わせることができるという場合
サーバーコンポーネントを使用してデータを取得
基本的には、Next.jsアプリケーションはReact Server Componentsを使用します。Server Componentsを使用したデータ取得を利用するとこのようなメリットがあります。
- サーバーコンポーネントはpromiseをサポートしており、データ取得などの非同期タスクをシンプルに解決することができます。
useEffect
やuseState
、データ取得ライブラリを使用することなく、async/await
構文を使用することができます。 - サーバーコンポーネントはサーバー上で実行されるため、データ取得やロジックをサーバー上に保持し、その結果のみをクライアントに送信することができます。
- サーバーコンポーネントはサーバー上で実行されるため、APIレイヤーを追加することなくデータベースに直接問い合わせることができます
SQLを使用
今回のチュートリアルで作成しているダッシュボードのプロジェクトではVercel Postgres SDKとSQLを使用してデータベースクエリを記述しています。
- SQLはリレーショナルデータベースをクエリするための業界標準です(ORMはフードの下でSQLを生成しています)。
- SQLの基本的な理解を持つことで、リレーショナルデータベースの基礎を理解することができ、ほかのツールに知識を適用することができます。
- SQLは汎用性があり、特定のデータを取得したり操作したりすることができます
- Vercel Postgres SDKはSQLインジェクションからの保護を提供してくれます
/app/lib/data.ts
を確認すると@vercel/postgres
からsql
関数インポートしていることが分かります。この関数を使用することでデータベースに問い合わせることができます。
どのサーバーコンポーネント内でもsql
を呼び出すことができます。しかし、より簡単にコンポーネントをナビゲートできるように、すべてのデータクエリをdata.ts
に保持し、コンポーネントにインポートできるようにしています。
ダッシュボードプロジェクトの概要ページデータをフェッチする
データフェッチの様々な方法を学んだところで、ダッシュボードの概要ページのデータをフェッチしてみます。
/app/dashboard/page.tsx
に移動して以下のコードを追加してみましょう。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
export default async function Page() {
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */}
{/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */}
{/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */}
{/* <Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/> */}
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
{/* <RevenueChart revenue={revenue} /> */}
{/* <LatestInvoices latestInvoices={latestInvoices} /> */}
</div>
</main>
);
}
Page
関数にはasync
が付いているため、非同期コンポーネントとなります。また、<Card>
、<RevenueChat>
、<LatestInvoices>
というデータを受け取るコンポーネントが3つあります(現在はエラー防止でコメントアウト)。
<RevenueChart/>
コンポーネントのデータを取得する
<RevenueChart/>
コンポーネントのデータを取得するには、data.ts
からfetchRevenue
関数をインポートしてコンポーネント内で呼び出します。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
// ...
}
コードを追加・変更したら<RevenueChart>
コンポーネントのコメントを解除して、コンポーネントファイルである/app/ui/dashboard/revenue-chart.tsx
のコメントも解除しましょう。
コメントの解除が終わったら、サーバーが起動されていることを確認し、http://localhost:3000/dashboardにアクセスします。
以下のように収益データを使用したチャート画面が表示されたら、データ取得の成功です。
<LatestInvoices/>
コンポーネントのデータを取得する
<LatestInvoices/>
コンポーネントでは、日付順に並べ替えられた最新の5件の請求書を取得する必要があります。すべての請求書を取得して、JavaScriptを使って並べ替えることもできます。しかし、アプリケーションの規模が大きくなるにつれて、リクエスト毎に転送されるデータ量と、ソートするJavaScriptの量が大幅に増加する可能性があります。ソートして最新の請求書を使用するのではなく、SQLクエリを使用して直近の5件の請求書だけを取得しましょう。
/app/dashboard/page.tsx
ファイルを開き、以下の様にfetchLatestInvoices
関数をインポートして追加します。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices();
// ...
}
その後<LatestInvoices/>
コンポーネントのコメントを外しましょう。また、/app/dashboard/latest-invoices
にある<LatestInvoices/>
コンポーネントの関連コードもコメント解除する必要があります。
サーバーが起動されていることを確認したら再度、http://localhost:3000/dashboardにアクセスして5件の請求書データが表示されていることを確認しましょう。以下のようなが目になればデータ取得の成功です。
<Card/>
コンポーネントのデータを取得する
続いて<Card/>
コンポーネントのデータを取得します。
カードには以下のデータが表示されています。
- 収集された請求書の合計金額
- 未決済の請求書の合計金額
- 請求書の合計数
- 顧客の合計数
ここでもすべてのデータを取得してJavaScriptを使用しデータを操作したくなるかもしれません。ですがその方法では転送するデータが多くなってしまう等の問題が出てきます。
SQLを使用すると必要なデータを取り出すことができます。以下のようにコードを変更してみましょう。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import {
fetchRevenue,
fetchLatestInvoices,
fetchCardData
} from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices();
const {
numberOfCustomers,
numberOfInvoices,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
// ...
}
以下のような画面が表示されたらカードの追加成功です。
注意点
これまででダッシュボードにコンポーネントを追加してきました。しかし注意すべき点が以下の二点あります。
- データリクエストは意図せず互いにブロックし合い、リクエストのウォーターフォールを生み出しています。
- デフォルトではNext.jsはパフォーマンスを向上させるためにルートをプリレンダリングします(スタティックレンダリング)。そのため、データが変更されてもダッシュボードには反映されません。
ウォーターフォールとは
注意点で説明された「ウォーターフォール」について学んでいきましょう。
ウォーターフォールというのは、前のリクエストの完了に依存する一連のネットワーク・リクエストのことです。データ・フェッチの場合、各リクエストは前のリクエストがデータを返して初めてフェッチの処理を開始できるようになります。
先ほど追加したフェッチ処理を確認してみましょう。ここではfetchRevenue
関数が完了してからfetchLatestInvoices
関数を実行し、fetchLatestInvoices
関数が完了してからfetchCardData
関数が実行されます。
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // fetchRevenue関数が完了するのを待機する
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // fetchLatestInvoices関数が完了するのを待機する
ウォーターフォールパターンが悪というわけではありません。「最初にIDとプロフィール情報を取得する必要がある」など、適切に使用されれば問題ないのです。
今回は前の処理を待つ必要はありません。この場合はパフォーマンスに影響が出るため並列に実行することが望ましいです。
並列にデータを取得する
JavaScriptではPromise.all()
やPromise.allSettled()
関数を使用してすべての処理を同時に開始することができます。
このパターンを使用することで以下のようなメリットが生まれます。
- すべてのデータ取得を同時に実行し始めることで、パフォーマンスを向上させることができる
- ネイティブのJavaScriptパターンを使用することで、どのようなライブラリやフレームワークにも適用できる
しかし、このJavaScriptのパターンだけに頼るとデメリットが生まれます。それは、複数のデータ取得のうち、一つのデータリクエストがほかのリクエストより遅い場合、遅いデータリクエストが完了するまで、UIが表示されなくなってしまいます。
次回はこの問題を解決していきます。
まとめ
- Next.jsはデフォルトでプリレンダリングされているため、データに変更があってもUIに反映されない。
- ウォーターフォールになってしまうのを避けてJavaScriptのpromiseを使用すると、すべてのデータ取得が終わるのを待つため、一つのデータ取得が遅れるとUIの表示も遅くなってしまう。
データ取得方法 | ポイント |
---|---|
APIレイヤー | ・APIを提供するサードパーティーのサービスを利用する場合。 ・クライアントからデータを取得する場合にデータベースの秘密がクライアントに公開されるのを避けるために、サーバー上で動作するAPIレイヤーを用意したい場合。 |
データベースクエリ | ・APIエンドポイントを作成する場合は、データベースとやり取りするためのロジックを必要とする場合。 ・React Server Components(サーバー上でデータを取得する)を使用している場合は、APIレイヤーをスキップして、データベースの秘密をクライアントに公開するリスクを冒すことなく、データベースに直接問い合わせることができるという場合。 |
サーバーコンポーネントを使用して取得 | ・サーバーコンポーネントはpromiseをサポートしており、データ取得などの非同期タスクをシンプルに解決することができます。useEffect やuseState 、データ取得ライブラリを使用することなく、async/await 構文を使用することができます。・サーバーコンポーネントはサーバー上で実行されるため、データ取得やロジックをサーバー上に保持し、その結果のみをクライアントに送信することができます。 ・サーバーコンポーネントはサーバー上で実行されるため、APIレイヤーを追加することなくデータベースに直接問い合わせることができます |
SQL | ・SQLはリレーショナルデータベースをクエリするための業界標準です(ORMはフードの下でSQLを生成しています)。 ・SQLの基本的な理解を持つことで、リレーショナルデータベースの基礎を理解することができ、ほかのツールに知識を適用することができます。 ・SQLは汎用性があり、特定のデータを取得したり操作したりすることができます ・Vercel Postgres SDKはSQLインジェクションからの保護を提供してくれます |