【Learn Next.js チュートリアル解説】第7章:データ取得(Data Fetching)Next.jsとNeonでDBデータを直接・並列取得する方法

Next.jsのServer ComponentsからNeonのPostgreSQLへ直接接続し、売上グラフ・最新請求書・集計データを並列取得する仕組みの図解 プログラミング

前回の第6章でNeon(PostgreSQL)のセットアップと初期データの投入が完了しました。

今回は、いよいよアプリケーション側からデータベースに接続し、ダッシュボードの概要ページに実際のデータを表示させる実装を進めていきます。

この記事では、Next.jsにおけるデータ取得の選択肢、Server Componentsのメリット、そして実務で気をつけるべき「ネットワークウォーターフォール」の回避方法までを詳しく解説します。

1. データの取得方法を選択する

フルスタックアプリケーションを作成する際、データベースとやり取りするためのアプローチには主に以下の選択肢があります。

APIレイヤー

  • 概要: アプリケーションコードとデータベースの間にある中間層(Next.jsではルートハンドラーなどで作成)。
  • 利用シーン:
    • サードパーティのAPIサービスを利用する場合。
    • クライアント側(ブラウザ)からデータを取得する際、データベースの機密情報が露出するのを防ぐため、サーバー側で動作するAPIを介する場合。

データベースクエリ(直接クエリ)

  • 概要: リレーショナルデータベース(PostgreSQLなど)に対し、SQLやORMを使って直接データを取得する手法。
  • メリット: React Server Components(RSC)を使用すれば、追加のAPIレイヤーをスキップしてサーバーから直接データベースにアクセスできます。機密情報(接続情報など)をクライアントに漏らすリスクをゼロにしつつ、余分なコードを削減できます。

今回はすでに用意されているデータベースクエリを使用して、直接データを取得していきます。

2. サーバーコンポーネントでデータを取得する

Next.jsのApp Routerでは、コンポーネントはデフォルトでReact Server Components (RSC)として動作します。RSCでのデータ取得には大きなメリットがあります。

  • 非同期処理のサポート: async/await構文がネイティブで使え、従来の useEffect や useState を使った複雑な記述が不要になります。
  • サーバー側での高負荷処理: 重いデータ取得やロジック処理をサーバー側だけで完結させ、結果(HTMLやJSON)だけをクライアントに送信できます。
  • 安全な直接アクセス: APIエンドポイントを別途用意しなくても、安全にデータベースにクエリを記述できます。

3. SQLの活用とダッシュボードのデータ取得

このチュートリアルでは、postgres.js ライブラリを使用してデータベースクエリを実行します。SQLを使うことで、必要なデータだけをピンポイントで効率よく取得・操作できます。

  • SQLは、リレーショナルデータベースを照会するための業界標準です(例えば、ORMは内部的にSQLを生成します)。
  • SQLの基本的な理解があれば、リレーショナルデータベースの構造を深く理解するのに役立ち、その知識を他のツールにも応用できるようになります。
  • SQLは汎用性が高く、特定のデータを自由自在に取得したり操作したりすることができます。
  • このライブラリ(postgres.js)は、SQLインジェクションに対する保護機能を提供しています。

/app/lib/data.ts にはあらかじめクエリが用意されています。この sql関数を使用すると、データベースに対して安全にクエリを実行できます。

import postgres from 'postgres';
 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
 
// ...

この 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>
  );
}

上記のコードは一部がまだコメントアウトされています。それでは、各部分の役割を見ていきましょう。

  • export default async function Page() とあるように、これは非同期の Page サーバーコンポーネントです。これにより await を使ってデータを取得できます。
  • <Card> や <RevenueChart> など、データを受信するコンポーネントが配置されていますが、現在はコメントアウトされており未実装の状態です。

【コラム】通信のタイムラグと「await」の忘れがち:非同期処理でハマった話

JavaScriptのawaitとPromiseを使ったデータベース通信の仕組みを示す図解

Next.js(App Router)と Neon(PostgreSQL)を使ってデータベース連携を進めている最中、ふとこんな疑問が湧きました。

「なんで外部APIやデータベースと通信する時って、いちいち await を挟まないといけないんだっけ?」

というわけで、サーバーサイドでのデータ取得(Data Fetching)を実装する中で改めて痛感した、JavaScriptの非同期処理と await の本質について、実体験を交えながら整理してみたいと思います。

1. 「返事をするまで待て」が通じない世界

プログラムを書いていると、まるで手元の変数を読み込むのと同じ感覚で、データベースからデータを引っ張ってきたくなります。

例えば、こんなコードを書いたとします。

// ❌ うっかり await を書き忘れた例
const revenue = fetchRevenue(); 
console.log(revenue); // 画面がバグる、または undefined になる

頭の中では「売上データが revenue に入るはず!」と思っているのですが、実際に返ってくるのはデータそのものではなく、中身が空っぽの「Promise(プロミス=引換券)」という名のオブジェクトです。

2. なぜネットワーク通信には await が絶対に必要なのか?

JavaScript(Node.js)は、基本的にコードを上から下へ一瞬で駆け抜けるように実行します。

しかし、アプリケーションが動いているサーバー(あるいはローカルのPC)から、クラウド上にあるデータベース(今回の場合はシンガポールにある Neon のサーバーなど)までは、物理的な距離があります。

これには、どれだけ高速な回線であっても、数ミリ秒〜数秒の「物理的なタイムラグ(ネットワーク遅延)」が発生します。

JavaScriptはこのタイムラグを無視して次の行に行こうとします。だからこそ、ここで一回立ち止まって、相手から返事が来るまで進むなよ!と命令するためのブレーキ、それが await なのです。

3. Server Components(RSC)における await のありがたみ

昔のフロントエンド開発や、従来の useEffect を使ったデータ取得では、この非同期の待ち受け(Promiseの解決)を管理するのがなかなか面倒でした。ローディング中のフラグ(isLoading)を用意したり、データがない時のガード構文を入れたり……。

しかし、Next.js の React Server Components(RSC) では、コンポーネント自体を async にして、必要な場所でストレートに await を書くことができます。

export default async function Page() {
  // ちゃんと相手の返事をその場で「待って」から次の処理へ進める
  const revenue = await fetchRevenue(); 

  return <RevenueChart revenue={revenue} />;
}

「返事が来るまでサーバー側でしっかり待機し、データが揃った完璧な状態のHTML(またはJSON)をビルドしてブラウザに返す」——この一連の流れを極めてシンプルに記述できるのは、現代のフルスタックフレームワークの大きな強みだと感じます。

まとめ:通信相手は「別の世界の住民」と思おう

コードを書いていると、あたかも自分のパソコンの中だけで全てが完結しているような錯覚に陥りがちです。

しかし、一歩ネットワークの外(外部APIやクラウドデータベース)に出たら、そこはもう「いつ返事が来るか分からない、別世界の住民」です。

  • メモリ内の計算や変数操作: 一瞬で終わるので await は不要。
  • ネットワークを伴う外部通信: 相手の返事を待つ必要があるので await は必要

この基本原則を頭に置いておくだけで、非同期処理のエラー(undefined や Promise が返ってくるバグ)の大部分は綺麗に防げるようになります。

データベース連携やAPI通信で「あれ、データが取れないな?」と思ったときは、まずコードのどこかに await の付け忘れがないか、あるいは非同期のバトンリレーが途切れていないかを確認してみましょう!

① 売上データの取得 <RevenueChart/>

コンポーネントのデータを取得するには /app/dashboard/page.tsx に data.ts から fetchRevenue 関数をインポートして関数を追加します。

さらにjsxのレンダリング部分のグラフ表示のコメントアウトを外します。

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();
  // ...
 return (
    <main>
    // ...
    <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
      // ここのコメントアウトを外す
      <RevenueChart revenue={revenue}  />

次に <RevenueChart/> のあるコンポーネントファイル /app/ui/dashboard/revenue-chart.tsx を修正します。

以下のように 合計2箇所(JavaScriptのロジック部分 と JSXのグラフ描画部分)のコメントアウトを外します。

import { generateYAxis } from '@/app/lib/utils';
// ...

export default async function RevenueChart({
  revenue,
}: {
  revenue: Revenue[];
}) {
  const chartHeight = 350;
  
  // ▼【1箇所目】ここを外す(Y軸のメモリ計算と空データチェック)
  const { yAxisLabels, topLabel } = generateYAxis(revenue);

  if (!revenue || revenue.length === 0) {
    return <p className="mt-4 text-gray-400">No data available.</p>;
  }

  return (
    <div className="w-full md:col-span-4">
      <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Recent Revenue
      </h2>
      // ...

      {/* ▼【2箇所目】ここから下のグラフ本体の JSX のコメントアウトを外す */}
      <div className="rounded-xl bg-gray-50 p-4">
        <div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
        // ...

        </div>
      </div>
      {/* ▲ ここまで外す */}
    </div>
  );
}

開発サーバーを起動して http://localhost:3000/dashboard にアクセスします。データに基づいたグラフが表示されるはずです。

revenue関数によってグラフが追加されたダッシュボード画面のスクリーンショット

最新請求書の取得 <LatestInvoices/>

続いて、日付順に並べた最新の請求書5件を取得します。

JavaScriptを使ってすべてのデータを取得した後にクライアント側でソートすることも可能ですが、データ量が増えるとパフォーマンスに悪影響を与えます。メモリ内で処理する代わりに、最初からSQLクエリを使って最新の5件のみを取得(LIMIT 5)するのが効率的です。

app/lib/data.ts にはすでに以下の fetchLatestInvoices 関数が用意されています。

// Fetch the last 5 invoices, sorted by date
export async function fetchLatestInvoices() {
  try {
    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`;

この関数を /app/dashboard/page.tsx にインポートして、レンダリング部分のjsxのコメントアウトを外します。

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();
  // ...
 return (
    <main>
    // ...
    <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
      <RevenueChart revenue={revenue}  />
      // ここのコメントアウトを外す
      <LatestInvoices latestInvoices={latestInvoices} />
    // ...
}

コンポーネント /app/ui/dashboard/latest-invoices.tsx 内の関連コードのコメントも解除します。

import { ArrowPathIcon } from '@heroicons/react/24/outline';
// ...
  return (
    <div className="flex w-full flex-col md:col-span-4">
      <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Latest Invoices
      </h2>
      <div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
        {/* ここからコメントアウトを外す */}
        <div className="bg-white px-6">
          {latestInvoices.map((invoice, i) => {
            return (
              // ...

                <p
                  className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
                >
                  {invoice.amount}
                </p>
              </div>
            );
          })}
        </div>
        {/* ▲ ここまで外す */}
// ...
http://localhost:3000/dashboard
にアクセスすると、グラフと表が正しく表示され、データベースから最新の5件のみが取得されていることが確認できます。

invoicsesが追加されたダッシュボードのスクリーンショット

これで、データベースに直接クエリを実行するメリットが実感できたのではないでしょうか。

③ カード情報の取得 <Card>

今度は <Card> コンポーネント用のデータを取得します。カードには以下の指標が表示されます。

  • 回収した請求書の合計金額
  • 未払い請求書の合計金額
  • 請求書の総数
  • 顧客総数

ここでもすべてのレコードを取得して Array.length で数えたくなるかもしれませんが、SQLの集計関数(COUNT や SUM)を使えば、リクエスト時に転送するデータ量を最小限に抑えられます。

app/lib/data.ts には以下のような関数がすでに用意されています。

const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

/app/dashboard/page.tsx に fetchCardData 関数をインポートし、返される値を分割代入して受け取ります。jsxの表示部分のコメントアウトも外します。

import { Card } from '@/app/ui/dashboard/cards';
// ...

import {
  fetchRevenue,
  fetchLatestInvoices,
  // このインポートを追加
  fetchCardData,
} from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // このデータを追加
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
 
  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"
        />
        // ...

変更した後、開発サーバーを確認してみると、カードが4つ追加されダッシュボードの概要ページに必要なすべてのデータが取得できました。

カード表示を追加したダッシュボードのスクリーンショット

しかし、ここで注意すべき点が2つあります。

  1. データの取得リクエストが続くと順番待ちが発生してしまい、ページの表示が遅くなるリクエストウォーターフォールが発生する。
  2. Next.jsはデフォルトでパフォーマンス向上のためルートをプリレンダリングします。これは静的レンダリングと呼ばれます。そのため、表示された後にデータが変更されてもダッシュボードには反映されません。

この章ではまず1番目の項目について解説し、2番目の項目については次の章で詳しく見ていきましょう。

4. リクエストウォーターフォール(Request Waterfalls)とは?

データを順番に取得していくと、「前のリクエストが完了するまで次のリクエストが開始されない」という現象が起きます。これをネットワークウォーターフォールと呼びます。

// ウォーターフォールの例(前の処理の完了を待つ)
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // fetchRevenueの完了を待つ
const cardData = await fetchCardData(); // latestInvoicesの完了を待つ
  • 必要なケース: 前のリクエスト結果に依存する場合(例:ユーザーIDを取得してから、そのユーザーのプロフィールを取得するなど)。
  • 避けるべきケース: 互いに依存しない独立したデータ取得なのに、順番に実行されてページの表示が遅くなってしまう場合。

5. 並列データ取得(Parallel Data Fetching)による最適化

ウォーターフォールを防ぐ標準的なJavaScriptの解決策が Promise.all()(または Promise.allSettled())です。これにより、複数の非同期リクエストを同時に(並列で)開始できます。

export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`...`;
 
    // すべてのクエリを同時に実行
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}
  • メリット: すべてのデータ取得が同時に走るため、個別に順番待ちをするよりも全体のロード時間を大幅に短縮できます。

まとめ・次のステップ

第7章では、Next.jsにおける効率的なデータ取得の基礎と、パフォーマンス低下の原因となるウォーターフォール、そしてそれを解決する Promise.all について学びました。

次は 第8章「静的レンダリングと動的レンダリング」 に進み、アプリケーションのレンダリングモードについてさらに理解を深めていきましょう!

コメント