7
チャプター7
データの取得
データベースを作成し、シードを投入しました。次に、アプリケーションでデータを取得するさまざまな方法について説明し、ダッシュボードの概要ページを構築しましょう。
このチャプターでは...
取り上げるトピックは以下の通りです。
API、ORM、SQLなどのデータ取得方法について学習します。
サーバーコンポーネントがバックエンドリソースに安全にアクセスする方法について説明します。
ネットワークウォーターフォールとは何か。
JavaScriptパターンを使用して、並列データ取得を実装する方法。
データ取得方法の選択
APIレイヤー
APIは、アプリケーションコードとデータベース間の仲介レイヤーです。APIを使用するいくつかのケースがあります。
- サードパーティのサービスがAPIを提供している場合。
- クライアントからデータを取得する場合、サーバー上で実行されるAPIレイヤーを用意して、データベースのシークレットがクライアントに公開されるのを防ぎたいでしょう。
Next.jsでは、Route Handlersを使用してAPIエンドポイントを作成できます。
データベースクエリ
フルスタックアプリケーションを作成する際には、データベースとやり取りするためのロジックも記述する必要があります。Postgresのようなリレーショナルデータベースでは、SQLまたはORMを使用してこれを行うことができます。
データベースクエリを記述する必要があるケースがいくつかあります。
- APIエンドポイントを作成する際に、データベースとやり取りするためのロジックを記述する必要があります。
- React Server Components(サーバーでデータを取得する)を使用している場合、APIレイヤーをスキップして、データベースのシークレットをクライアントに公開するリスクなしに、データベースに直接クエリを実行できます。
React Server Componentsについてさらに学習しましょう。
サーバーコンポーネントを使用したデータ取得
デフォルトでは、Next.jsアプリケーションはReact Server Componentsを使用します。Server Componentsを使用したデータ取得は比較的新しいアプローチであり、それを使用することにはいくつかの利点があります。
- Server ComponentsはJavaScript Promisesをサポートしており、データ取得のような非同期タスクのネイティブなソリューションを提供します。
useEffect、useState、またはその他のデータ取得ライブラリを必要とせずにasync/await構文を使用できます。 - Server Componentsはサーバーで実行されるため、コストのかかるデータ取得とロジックをサーバーに保持し、結果のみをクライアントに送信できます。
- Server Componentsはサーバーで実行されるため、追加のAPIレイヤーなしにデータベースに直接クエリを実行できます。これにより、追加のコードの記述と保守の手間が省けます。
SQLの使用
ダッシュボードアプリケーションでは、postgres.jsライブラリとSQLを使用してデータベースクエリを記述します。SQLを使用する理由はいくつかあります。
- SQLはリレーショナルデータベースをクエリするための業界標準です(例:ORMは内部でSQLを生成します)。
- SQLの基本的な理解があれば、リレーショナルデータベースの基礎を理解でき、他のツールにも知識を応用できます。
- SQLは汎用的であり、特定のデータを取得および操作できます。
postgres.jsライブラリは、SQLインジェクションから保護します。
SQLを使ったことがなくても心配しないでください。クエリはすでに用意されています。
/app/lib/data.tsに移動します。ここで、postgresを使用していることがわかります。sql関数を使用すると、データベースにクエリを実行できます。
import postgres from 'postgres';
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
// ...sqlは、Server Componentのように、サーバー上のどこでも呼び出すことができます。ただし、コンポーネント間でのナビゲーションを容易にするために、すべてのデータクエリをdata.tsファイルに保持し、コンポーネントにインポートして使用できるようにしました。
注意: 第6章で独自のデータベースプロバイダーを使用した場合は、データベースクエリをプロバイダーに合わせて更新する必要があります。クエリは
/app/lib/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は非同期サーバーコンポーネントです。これにより、awaitを使用してデータを取得できます。- データを受け取る3つのコンポーネント、
<Card>、<RevenueChart>、<LatestInvoices>もあります。これらは現在コメントアウトされており、まだ実装されていません。
<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)に移動し、その中のコードのコメントを解除します。 localhost:3000を確認すると、revenueデータを使用するチャートが表示されるはずです。

より多くのデータをインポートしてダッシュボードに表示し続けましょう。
<LatestInvoices/>用のデータ取得
<LatestInvoices />コンポーネントでは、最新の5件の請求書を日付順に並べ替えて取得する必要があります。
すべての請求書を取得し、JavaScriptでソートすることもできます。データが小さい場合は問題ありませんが、アプリケーションが大きくなるにつれて、各リクエストで転送されるデータ量と、それをソートするために必要なJavaScriptが大幅に増加する可能性があります。
メモリ内で最新の請求書をソートする代わりに、SQLクエリを使用して最新の5件の請求書のみを取得できます。たとえば、data.tsファイルからのSQLクエリは次のようになります。
// Fetch the last 5 invoices, sorted by date
const data = await sql<LatestInvoiceRaw[]>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;ページで、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/ui/dashboard/latest-invoicesにある<LatestInvoices />コンポーネント自体の関連コードのコメントも解除する必要があります。
localhostにアクセスすると、データベースから最新の5件のみが返されていることがわかります。データベースに直接クエリを実行することの利点が、なんとなくわかってきたのではないでしょうか!

練習: <Card>コンポーネント用のデータ取得
今度は、<Card>コンポーネント用のデータを取得してみましょう。カードは次のデータを表示します。
- 回収された請求書の合計金額。
- 保留中の請求書の合計金額。
- 請求書の合計数。
- 顧客の合計数。
ここでも、すべての請求書と顧客を取得し、JavaScriptでデータを操作したくなるかもしれません。たとえば、Array.lengthを使用して請求書と顧客の合計数を取得できます。
const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;しかし、SQLを使用すると、必要なデータのみを取得できます。Array.lengthを使用するよりも少し長くなりますが、リクエスト中に転送されるデータ量が少なくなります。これはSQLの代替手段です。
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;インポートする必要がある関数はfetchCardDataと呼ばれます。関数から返された値を分割代入する必要があります。
ヒント
- カードコンポーネントを確認して、どのようなデータが必要かを確認してください。
data.tsファイルを確認して、関数が何を返すかを確認してください。
準備ができたら、下のトグルを展開して最終コードを表示してください。
素晴らしい!これでダッシュボード概要ページに必要なすべてのデータを取得できました。ページは次のようになっているはずです。

しかし…注意すべきことが2つあります。
- データリクエストが意図せず互いにブロックし、リクエストウォーターフォールを発生させています。
- デフォルトでは、Next.jsはパフォーマンスを向上させるためにルートを事前レンダリングします。これは静的レンダリングと呼ばれます。したがって、データが変更されても、ダッシュボードには反映されません。
この章ではまず1について説明し、次の章で2について詳しく見ていきます。
リクエストウォーターフォールとは?
「ウォーターフォール」とは、以前のリクエストの完了に依存する一連のネットワークリクエストを指します。データ取得の場合、各リクエストは前のリクエストがデータを返してからしか開始できません。

たとえば、fetchLatestInvoices()が実行を開始する前にfetchRevenue()が実行されるのを待つ必要があります。そして、それに続いて…というようになります。
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finishこのパターンが必ずしも悪いわけではありません。ウォーターフォールを意図的に使用したい場合があります。たとえば、ユーザーIDとそのプロフィール情報を最初に取得したい場合。IDを取得したら、友達のリストを取得に進むことができます。この場合、各リクエストは前のリクエストから返されたデータに依存します。
しかし、この動作は意図しないものであり、パフォーマンスに影響を与える可能性もあります。
パラレルなデータ取得
ウォーターフォールを回避する一般的な方法は、すべてのデータリクエストを同時に開始すること、つまり並列に実行することです。
JavaScriptでは、Promise.all()またはPromise.allSettled()関数を使用して、すべてのPromiseを同時に開始できます。たとえば、data.tsでは、fetchCardData()関数でPromise.all()を使用しています。
export async function fetchCardData() {
try {
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
// ...
}
}このパターンを使用することで、次のことが可能になります。
- すべてのデータ取得を同時に実行開始でき、各リクエストの完了を待つウォーターフォールよりも高速です。
- あらゆるライブラリやフレームワークに適用できるネイティブJavaScriptパターンを使用できます。
しかし、このJavaScriptパターンだけに頼ることには1つの欠点があります。他のすべてのデータリクエストよりも遅いリクエストがあった場合、どうなるでしょうか?次の章で詳しく見ていきましょう。
チャプターを完了しました。7
Next.jsでのデータ取得のさまざまな方法について学習しました。
役に立ちましたか?



