コンテンツにスキップ

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をサポートしており、データ取得のような非同期タスクのネイティブなソリューションを提供します。useEffectuseState、またはその他のデータ取得ライブラリを必要とせずに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つのコンポーネント、<Card><RevenueChart><LatestInvoices>もあります。これらは現在コメントアウトされており、まだ実装されていません。

<RevenueChart/>用のデータ取得

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

より多くのデータをインポートしてダッシュボードに表示し続けましょう。

<LatestInvoices/>用のデータ取得

<LatestInvoices />コンポーネントでは、最新の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();
  // ...
}

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

localhostにアクセスすると、データベースから最新の5件のみが返されていることがわかります。データベースに直接クエリを実行することの利点が、なんとなくわかってきたのではないでしょうか!

Latest invoices component alongside the revenue chart

練習: <Card>コンポーネント用のデータ取得

今度は、<Card>コンポーネント用のデータを取得してみましょう。カードは次のデータを表示します。

  • 回収された請求書の合計金額。
  • 保留中の請求書の合計金額。
  • 請求書の合計数。
  • 顧客の合計数。

ここでも、すべての請求書と顧客を取得し、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のさまざまなレンダリングモードについて学習します。

App Router: データ取得 | Next.js