データ取得

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

データ取得方法の選択

APIレイヤー

APIはアプリケーションコードとデータベースの中間層です。APIを使用するケースは以下の通りです:

  • サードパーティサービスがAPIを提供している場合
  • クライアントからデータを取得する場合、データベースの秘密情報をクライアントに公開しないようにサーバー上で実行されるAPIレイヤーが必要

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

データベースクエリ

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

データベースクエリを記述する必要があるケース:

  • APIエンドポイントを作成する際、データベースとやり取りするロジックを記述する必要がある
  • Reactサーバーコンポーネント(サーバー上でデータを取得)を使用している場合、APIレイヤーをスキップし、データベースの秘密情報をクライアントに公開するリスクなしに直接データベースをクエリできる

Reactサーバーコンポーネントについてさらに学びましょう。

サーバーコンポーネントを使用したデータ取得

デフォルトでNext.jsアプリケーションはReactサーバーコンポーネントを使用します。サーバーコンポーネントでのデータ取得は比較的新しいアプローチで、以下の利点があります:

  • サーバーコンポーネントはJavaScriptのPromiseをサポートし、データ取得のような非同期タスクに対してネイティブな解決策を提供。useEffectuseStateや他のデータ取得ライブラリなしでasync/await構文を使用可能
  • サーバーコンポーネントはサーバー上で実行されるため、負荷の高いデータ取得やロジックをサーバー上に保ち、結果のみをクライアントに送信できる
  • サーバーコンポーネントはサーバー上で実行されるため、追加の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はサーバーコンポーネントのようにサーバー上のどこでも呼び出せます。ただし、コンポーネント間の移動を容易にするため、すべてのデータクエリを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>
  );
}

上記のコードは意図的にコメントアウトされています。各部分を順番に確認していきます。

  • pageasyncサーバーコンポーネントです。これによりデータ取得に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データを使用したチャートが表示されます
過去12ヶ月の総収益を示す収益チャート

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

**<LatestInvoices/>**のデータ取得

<LatestInvoices />コンポーネントでは、日付でソートされた最新の5つの請求書を取得する必要があります。

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

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

/app/lib/data.ts
// 日付でソートされた最新5件の請求書を取得
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件のみが表示されます。直接データベースをクエリする利点がわかってきたのではないでしょうか!

収益チャートと並んだ最新請求書コンポーネント

練習:<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ファイルを確認し、関数が返す内容を確認

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

素晴らしい!ダッシュボード概要ページのすべてのデータを取得できました。ページは次のようになっているはずです:

すべてのデータが取得されたダッシュボードページ

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

  1. データリクエストが意図せず互いをブロックし、リクエストの滝が発生している
  2. デフォルトでNext.jsはパフォーマンス向上のためルートを事前レンダリング(静的レンダリング)するため、データが変更されてもダッシュボードに反映されない

この章では1番目について、次の章で2番目について詳しく説明します。

リクエストの滝とは?

「滝」とは、前のリクエストの完了に依存する一連のネットワークリクエストを指します。データ取得の場合、各リクエストは前のリクエストがデータを返した後にのみ開始できます。

逐次的なデータ取得と並列的なデータ取得を示す時間図

例えば、fetchLatestInvoices()の実行を開始する前にfetchRevenue()の実行を待つ必要があり、以下同様です。

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // fetchRevenue()の完了を待つ
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // fetchLatestInvoices()の完了を待つ

このパターンが必ずしも悪いわけではありません。次のリクエストを行う前に条件が満たされるのを待ちたい場合もあります。例えば、ユーザーの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つあります:1つのデータリクエストが他のすべてよりも遅い場合どうなるでしょうか?次の章でさらに詳しく見ていきましょう。