メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
Next.js App Router アプリケーションは、Server Components、Server Actions、Route Handlers という 3 つの異なる場所から MySQL と通信します。このガイドでは、生の mysql2 ドライバーと 1 つの共有コネクションプールを使い、間に ORM を挟まずに、その 3 つすべてから MySQL に接続します。VillageSQL は MySQL のドロップイン代替なので、ここにあるすべてのステートメントは変更なしで VillageSQL に対して実行できます。

データベースの準備

root として接続するのではなく、専用のデータベースとアプリケーションユーザーを作成します。
作業用のテーブルを作成します。
アプリをスキャフォールドし、ドライバーをインストールします。
接続情報はソースではなく .env.local に保存します。

コネクションプール

アプリ内で MySQL と通信するすべての場所は、1 つのモジュール src/lib/db.ts から同じプールをインポートします。
これは、Prisma や Drizzle のクライアントをホットリロードをまたいでキャッシュするのに使われるのと同じパターンを、mysql2 のプールに直接適用したものです。connectionLimit、waitForConnections、queueLimit は MySQL のコネクションプーリングで説明しています。このガイドは、プールが Next.js プロセスの内部にあるときに何が変わるかに焦点を当てます。 globalThis のキャッシュは、開発モードでは任意ではありません。このアプリでのテストがその理由を証明しています。キャッシュを有効にした状態で npm run dev を実行し、src/lib/db.ts を続けて 4 回保存しても、開いている nextjs_app の接続は終始ちょうど 1 つのままでした。次のクエリで確認しました。
globalThis のキャッシュを取り除き、新しい next dev プロセスに対して同じ 4 回の保存を繰り返すと、その数は 5 に増えました。リロードのたびにモジュールのトップレベルが再実行され、そのたびに前のプールを閉じることなく mysql.createPool() が再び呼ばれました。これを確認するには、プールモジュールそのものを編集してください。それを単にインポートしているだけのページを保存しても再実行されません。Turbopack は再コンパイルをまたいで、変更されていないモジュールを生かしたままにするからです。本番環境ではこれは起きません。next build がアプリを一度だけコンパイルし、next start がモジュールをリロードせずに実行するからです。しかしこのガードは本番環境では何のコストもかからず、開発中の現実に再現可能な接続リークを防ぎます。

Server Component からのクエリ

src/app/page.tsx は、Server Component の中で、パラメータ化されたクエリを使って notes から直接読み取ります。
ユーザー入力を連結したり補間したりしてクエリ文字列を組み立ててはいけません。上の LIMIT ? プレースホルダーは文字列の連結ではなく mysql2 によって埋められます。これが、20; DROP TABLE notes のような値が、数値として解析に失敗する以外に何もできない理由です。 mysql2 は MySQL の TIMESTAMP 列を、文字列ではなく JavaScript の Date オブジェクトとして返します。上の Note インターフェイスが created_at を Date として型付けしているため、それを JSX として直接レンダリングする ({note.created_at}) と、アプリが実行される前に npm run build の型チェックで失敗します。
その型注釈がなければ、同じ間違いは後になって、より悪い形で、ビルド時エラーではなくランタイムのクラッシュとして現れます。Error: Objects are not valid as a React child (found: [object Date])。どちらにしても修正は同じです。値を文字列として扱うのではなく、レンダリングする場所でその値に .toISOString() (またはフォーマット用ライブラリ) を呼び出してください。 npm run build でアプリをビルドすると、このページはリクエスト時データへの依存を持たない (cookies() も headers() もなく、dynamic とマークされたものもない) ため、Next.js はビルド時にそれを静的コンテンツとしてプリレンダリングすることが分かります。
つまり getNotes() のクエリは next build 中に一度だけ実行され、データベースはビルド時に到達可能でなければなりません。後述の Server Action は、リクエストごとにクエリを再実行するのではなく、書き込みのたびにページを再検証することで、ページの内容を最新に保ちます。

Server Action による書き込み

src/app/actions.ts は同じプールを通じて挿入を実行します。
ホームページのフォームは、この関数を action として直接渡します。
実際のブラウザでこのフォームに “Server Action verified end to end” と入力して Add をクリックすると、ページ全体の遷移なしに新しい行が挿入され、revalidatePath("/") のおかげで、その行はレンダリングされた一覧にすぐに現れました。その後にデータベースへ直接クエリして書き込みを確認しました。
revalidatePath("/") は前のセクションでキャッシュされた静的レンダリングを無効化するため、次のリクエストはビルド時のスナップショットを配信する代わりに getNotes() を再実行します。この呼び出しがなければ、静的ページのデータキャッシュは新しい行に決して気づきません。

JSON Route Handler

Server Components と Server Actions は、Next.js 自身がレンダリングするページをカバーします。Route Handler は、ORM に焦点を当てたガイドがたいてい飛ばす部分です。ページではなく、素の HTTP API サーフェスだからです。src/app/api/notes/route.ts は、モバイルクライアント、Webhook、その他 Next.js のフロントエンドではない呼び出し元のために、GET リクエストに JSON で応答します。
ビルド済みアプリを npm run start (デフォルトではポート 3000) で実行してこのルートをリクエストすると、プレースホルダーのデータではなく、上で Server Action が挿入した行を含む notes の実際の行が返ります。
ホームページとは異なり、このルートは間接的にリクエスト時データを読む (ビルド時ではなく呼び出しのたびにデータベースにクエリする) ため、上の同じビルド出力は、static の ○ ではなく dynamic の ƒ でマークしています。

Next.js でのコネクションプールのサイズ決定

Route Handler は受信リクエストごとに 1 回実行され、並行負荷の下では複数のリクエストが同時に処理中になり得ます。プールが存在するのは、各リクエストが自分の接続を開くのではなく、それらのリクエストが上限のある数の MySQL 接続を共有するためです。 実行中のアプリの /api/notes に 20 件の並行リクエストを送り、すぐにサーバーを確認すると、
connectionLimit: 10 で設定されたプールに対して、使用中の接続は 4 つだけでした。Threads_connected はこのアプリのものだけでなくサーバー上のすべての接続を数えるため、ユーザーごとの数が必要なときは、このガイドの前のほうにある information_schema.processlist のクエリを使ってください。正確な数は、バーストが到着する時点に対して各クエリがどれだけ速く完了するかに依存するため、遅いクエリや混雑したサーバーでは、設定された上限まで、より高い数が表示されます。いずれにせよこれが確認するのは、プールが 20 個の接続を開くのではなく、そのバースト全体で接続をキューイングし再利用していることです。connectionLimit を上げると、残りがプールの内部キュー (queueLimit で上限が決まり、queueLimit が 0 なら無制限) で待つ前に、並行してクエリを実行できるリクエストの数が増えます。アプリが受け付けられるリクエストの数は変わらず、MySQL に対して同時にクエリ実行中でいられる数だけが変わります。 Next.js に固有の 2 つのことが、実際にはこの数に影響します。
  • サーバープロセスごとに独自のプールを持つ。 複数のインスタンス (複数の next start プロセス、または複数のサーバーレス関数インスタンス) を実行する Next.js のデプロイは、実行中のインスタンス数だけ connectionLimit を掛け算します。connectionLimit だけでなく、connectionLimit と MySQL の max_connections を一緒にサイズ決定してください。
  • 開発モードは 1 つのプロセス内でプールを増やすことがある。 上のコネクションプールのセクションで示したとおり、プールが globalThis にキャッシュされていない場合です。本番ビルドでは起きませんが、症状 (MySQL がアプリに必要なはずの数をはるかに超える開いた接続を報告する) が本番環境のサイズ決定の問題と見分けがつかないため、開発中に確認する価値があります。

代わりに ORM を選ぶ

このガイドが mysql2 を直接使うのは、生のコネクションプールが、その上に何が乗っていようと、すべての Next.js と MySQL の統合が共有する部分だからです。アプリケーションが生成された型、マイグレーション、またはクエリビルダーの恩恵を受けるなら、MySQL 固有の設定については Next.js と Prisma で MySQL を使うまたは Next.js と Drizzle で MySQL を使うを、ORM 全般で重要な設定については ORM で MySQL を使うを参照してください。

よくある質問

なぜ note.created_at のレンダリングは、テキストとして表示されずに失敗するのですか?

mysql2 は MySQL の TIMESTAMP または DATETIME 列を JavaScript の Date オブジェクトにデコードし、TypeScript の ReactNode 型も React 自身も、それを JSX として直接レンダリングできません。列が Date として型付けされていれば、npm run build は型チェック時に {note.created_at} を error TS2322: Type 'Date' is not assignable to type 'ReactNode' で捕まえます。その型がなければ、同じ間違いは代わりにランタイムに到達し、Error: Objects are not valid as a React child (found: [object Date]) を投げます。レンダリングの前に、値に .toISOString()、.toLocaleString()、またはフォーマット用ライブラリを呼び出してください。

少数のユーザーしか見込まない場合でもコネクションプールは必要ですか?

はい。トラフィックが軽くても、Next.js は複数のリクエストを並行して実行することがあり (Route Handler も呼び出すページの読み込み、2 つのブラウザタブ、サイトをクロールするボット)、各 MySQL 接続はサーバー上の実際のスレッドです。小さな connectionLimit を持つプール (小さなアプリには 10 が妥当なデフォルトです) は、アイドル時には何のコストもかからず、並行リクエストのバーストがそれぞれ 1 つずつ接続を開くのを防ぎます。

Route Handler から Server Action を呼ぶ、またはその逆はできますか?

Route Handler は Server Action が呼ぶのと同じ関数を呼べます。どちらも、プールを使う単なる非同期関数だからです。異なるのは呼び出し規約です。Server Action は <form action> または React の useActionState によって起動され、Route Handler は HTTP リクエストによって起動されます。一方がもう一方を直接呼ぶのではなく、データベースのロジックを素の関数に切り出して共有し、両方からそれを呼んでください。

なぜホームページは static なのに API ルートはそうではないのですか?

Next.js はこれをルートごとに、そのルートがリクエスト時データを読むかどうかに基づいて決定します。このガイドの src/app/page.tsx は cookies() や headers() のようなランタイム API を呼ばないため、next build はそれを一度だけプリレンダリングし、revalidatePath が無効化するまでそのスナップショットを配信します。このガイドの Route Handler にはそのようなキャッシュが適用されていないため、リクエストのたびにデータベースにクエリします。

トラブルシューティング

関連項目