コンテンツをスキップ

7

データフェッチ

データベースを作成し、データを投入したので、アプリケーションのデータをフェッチするさまざまな方法について説明し、ダッシュボードの概要ページを構築しましょう。

このチャプターでは...

取り上げるトピックは次のとおりです

データをフェッチするいくつかの方法(API、ORM、SQLなど)について学びます。

Server Componentsがバックエンドリソースへより安全にアクセスするのにどのように役立つか。

ネットワークウォーターフォールとは何か。

JavaScriptパターンを使用して並列データフェッチを実装する方法。

データのフェッチ方法を選択する

APIレイヤー

APIは、アプリケーションコードとデータベースの間の中間レイヤーです。APIを使用する可能性のあるケースがいくつかあります

  • APIを提供するサードパーティサービスを使用している場合。
  • クライアントからデータをフェッチする場合、データベースの秘密をクライアントに公開しないように、サーバーで実行されるAPIレイヤーを持つ必要があります。

Next.jsでは、Route Handlersを使用してAPIエンドポイントを作成できます。

データベースクエリ

フルスタックアプリケーションを作成する場合、データベースと対話するロジックも記述する必要があります。Postgresのようなリレーショナルデータベースでは、SQLまたはORMを使用してこれを行うことができます。

データベースクエリを記述する必要があるケースがいくつかあります

  • APIエンドポイントを作成する際、データベースと対話するロジックを記述する必要があります。
  • React Server Components(サーバーでデータをフェッチする)を使用している場合、APIレイヤーをスキップし、データベースの秘密をクライアントに公開するリスクなく、データベースを直接クエリできます。

React Server Componentsについて詳しく学びましょう。

Server Componentsを使用したデータフェッチ

デフォルトでは、Next.jsアプリケーションはReact Server Componentsを使用します。Server Componentsでのデータフェッチは比較的新しいアプローチであり、使用するいくつかの利点があります

  • Server ComponentsはJavaScriptのPromiseをサポートしており、データフェッチのような非同期タスクをネイティブで処理するソリューションを提供します。`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` 関数を使用すると、データベースをクエリできます

/app/lib/data.ts
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`に移動し、以下のコードを貼り付けて、しばらく内容を確認してください。

/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つあります: ``、``、そして``です。これらは現在コメントアウトされており、まだ実装されていません。

**``**のデータフェッチ

``コンポーネントのデータをフェッチするには、`data.ts`から`fetchRevenue`関数をインポートし、コンポーネント内で呼び出します

/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';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

次に、以下を実行しましょう

  1. ``コンポーネントのコメントを解除します。
  2. コンポーネントファイル(`/app/ui/dashboard/revenue-chart.tsx`)に移動し、その中のコードのコメントを解除します。
  3. `localhost:3000`を確認すると、`revenue`データを使用するチャートが表示されるはずです。
Revenue chart showing the total revenue for the last 12 months

さらにデータをインポートし、ダッシュボードに表示し続けましょう。

**``**のデータフェッチ

``コンポーネントには、最新の5つの請求書を日付順に取得する必要があります。

すべての請求書をフェッチし、JavaScriptを使用してソートすることもできます。データが少ないうちは問題ありませんが、アプリケーションが成長するにつれて、各リクエストで転送されるデータの量と、ソートに必要なJavaScriptの量が大幅に増加する可能性があります。

最新の請求書をメモリ内でソートする代わりに、SQLクエリを使用して最後の5つの請求書のみをフェッチできます。例えば、これは`data.ts`ファイルからのSQLクエリです

/app/lib/data.ts
// 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`関数をインポートします

/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';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

次に、``コンポーネントのコメントを解除します。また、`/app/ui/dashboard/latest-invoices`にある``コンポーネント自体の関連コードのコメントも解除する必要があります。

ローカルホストにアクセスすると、データベースから最後の5つだけが返されていることがわかるはずです。データベースを直接クエリする利点が見え始めていることを願っています!

Latest invoices component alongside the revenue chart

実践: ``コンポーネントのデータフェッチ

それでは、``コンポーネントのデータをフェッチしてみましょう。カードには以下のデータが表示されます

  • 回収済み請求書の合計金額。
  • 未回収請求書の合計金額。
  • 請求書の合計数。
  • 顧客の合計数。

ここでも、すべての請求書と顧客をフェッチし、JavaScriptでデータを操作したくなるかもしれません。例えば、`Array.length`を使用して請求書と顧客の合計数を取得することができます

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

しかしSQLを使用すると、必要なデータだけをフェッチできます。`Array.length`を使用するよりも少し長くなりますが、リクエスト中に転送されるデータ量が少なくて済みます。これがSQLの代替案です

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

インポートする必要がある関数は`fetchCardData`です。関数から返される値を分割代入する必要があります。

ヒント

  • カードコンポーネントをチェックして、必要なデータを確認してください。
  • `data.ts`ファイルで関数が何を返すか確認してください。

準備ができたら、以下のトグルを展開して最終コードを確認してください

素晴らしい!これでダッシュボードの概要ページのすべてのデータをフェッチしました。ページは次のようになるはずです

Dashboard page with all the data fetched

しかし...注意すべき点が2つあります

  1. データリクエストが意図せずに互いをブロックし、リクエストウォーターフォールを作成しています。
  2. デフォルトでは、Next.jsはパフォーマンス向上のためにルートをプリレンダリングします。これは静的レンダリングと呼ばれます。そのため、データが変更されても、ダッシュボードには反映されません。

この章では1番目の点について議論し、2番目の点については次の章で詳しく見ていきましょう。

リクエストウォーターフォールとは?

「ウォーターフォール」とは、前のリクエストの完了に依存する一連のネットワークリクエストを指します。データフェッチの場合、各リクエストは前のリクエストがデータを返した後でしか開始できません。

Diagram showing time with sequential data fetching and parallel data fetching

例えば、`fetchLatestInvoices()`が実行を開始するには、`fetchRevenue()`の実行を待つ必要があります。

/app/dashboard/page.tsx
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()`を使用しています

/app/lib/data.ts
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でのさまざまなデータフェッチ方法について学びました。

次へ

8: 静的レンダリングと動的レンダリング

Next.jsのさまざまなレンダリングモードについて学びましょう。