Next.jsでMySQLと接続ガイド

2025-08-25

PostgreSQL に続いて、Next.js アプリケーションで MySQL データベースに接続する方法を詳しく説明します。Vercel デプロイも考慮した手順です。

前提条件

  • Node.js がインストールされている
  • MySQL がインストールされているか、クラウドサービス(PlanetScale、AWS RDS など)のアカウントがある
  • 基本的な SQL の知識がある

ステップ 1: MySQL の準備

ローカル MySQL をセットアップ

  1. MySQL をインストール(まだの場合):
  1. データベースとユーザーを作成:
   mysql -u root -p
   CREATE DATABASE nextjs_mysql_demo;
   CREATE USER 'nextjs_user'@'localhost' IDENTIFIED BY 'securepassword';
   GRANT ALL PRIVILEGES ON nextjs_mysql_demo.* TO 'nextjs_user'@'localhost';
   FLUSH PRIVILEGES;
   EXIT;

クラウドサービスを使用する場合

PlanetScale や AWS RDS などのサービスを使用する場合:

  1. アカウントを作成
  2. 新しいデータベースを作成
  3. 接続文字列をメモ(後で使用します)

ステップ 2: Next.js プロジェクトのセットアップ

  1. 新しい Next.js プロジェクトを作成(まだの場合):
   npx create-next-app@latest nextjs-mysql-demo
   cd nextjs-mysql-demo
  1. 必要な依存関係をインストール:
   npm install mysql2
   # または
   yarn add mysql2

ステップ 3: 環境変数の設定

  1. プロジェクトルートに .env.local ファイルを作成:
   MYSQL_HOST="localhost"
   MYSQL_PORT="3306"
   MYSQL_DATABASE="nextjs_mysql_demo"
   MYSQL_USER="nextjs_user"
   MYSQL_PASSWORD="securepassword"

   # クラウドサービスの場合は次のような形式になります:
   # DATABASE_URL="mysql://username:password@host:port/database?options"
  1. .gitignore.env.local が含まれていることを確認

ステップ 4: データベース接続の設定

  1. lib/db.ts ファイルを作成:
   import { createPool, Pool } from 'mysql2/promise';
   import dotenv from 'dotenv';

   dotenv.config();

   let pool: Pool;

   export async function initializePool() {
     pool = createPool({
       host: process.env.MYSQL_HOST,
       port: parseInt(process.env.MYSQL_PORT || '3306'),
       database: process.env.MYSQL_DATABASE,
       user: process.env.MYSQL_USER,
       password: process.env.MYSQL_PASSWORD,
       waitForConnections: true,
       connectionLimit: 10,
       queueLimit: 0,
     });
   }

   export async function query(sql: string, values?: any[]) {
     if (!pool) await initializePool();
     const [rows] = await pool.query(sql, values);
     return rows;
   }

ステップ 5: データベース操作の実装

例: ユーザーテーブルの作成と操作

  1. 初期セットアップスクリプトを作成 scripts/initDB.ts:
import { query } from '../lib/db';

async function initDatabase() {
  try {
    await query(`
      CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(100) UNIQUE NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      );
    `);
    console.log('MySQL database initialized successfully');
  } catch (error) {
    console.error('Error initializing MySQL database:', error);
  }
}

initDatabase();
  1. スクリプトを実行:
   npx ts-node scripts/initDB.ts

ステップ 6: API ルートでのデータベース使用

  1. pages/api/users.ts を作成:
import { NextApiRequest, NextApiResponse } from 'next';
import { query } from '../../lib/db';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    switch (req.method) {
      case 'GET':
        const users = await query('SELECT * FROM users ORDER BY created_at DESC');
        res.status(200).json(users);
        break;
      
      case 'POST':
        const { name, email } = req.body;
        if (!name || !email) {
          return res.status(400).json({ message: 'Name and email are required' });
        }
        
        const result = await query(
          'INSERT INTO users (name, email) VALUES (?, ?)',
          [name, email]
        );
        // MySQLの場合は挿入されたIDを別途取得
        const [newUser] = await query('SELECT * FROM users WHERE id = LAST_INSERT_ID()');
        res.status(201).json(newUser);
        break;
      
      default:
        res.setHeader('Allow', ['GET', 'POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
    }
  } catch (error) {
    console.error('API Error:', error);
    res.status(500).json({ message: 'Internal server error' });
  }
}

ステップ 7: ページコンポーネントでのデータ取得

SSG (静的生成) でのデータ取得

pages/users/index.tsx:

import { GetStaticProps } from 'next';
import { query } from '../../lib/db';

interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

export default function UsersPage({ users }: { users: User[] }) {
  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const users = await query('SELECT * FROM users ORDER BY created_at DESC');
  return {
    props: {
      users: JSON.parse(JSON.stringify(users)), // Dateオブジェクトをシリアライズ
    },
    revalidate: 60, // ISR: 60秒ごとに再生成
  };
};

SSR (サーバーサイドレンダリング) でのデータ取得

pages/users/ssr.tsx:

import { GetServerSideProps } from 'next';
import { query } from '../../lib/db';

interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

export default function UsersSSRPage({ users }: { users: User[] }) {
  return (
    <div>
      <h1>Users (SSR)</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - {user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  const users = await query('SELECT * FROM users ORDER BY created_at DESC');
  return {
    props: {
      users: JSON.parse(JSON.stringify(users)),
    },
  };
};

ステップ 8: Vercel へのデプロイ

  1. Vercel でプロジェクトを作成
  2. 環境変数を設定:
  • Vercel ダッシュボードでプロジェクトを開く
  • “Settings” → “Environment Variables” に移動
  • MySQL接続情報を追加(本番用のデータベース接続情報)
  1. vercel.json をプロジェクトルートに作成(必要に応じて):
   {
     "version": 2,
     "builds": [
       {
         "src": "package.json",
         "use": "@vercel/next"
       }
     ],
     "env": {
       "MYSQL_HOST": "@mysql_host",
       "MYSQL_USER": "@mysql_user",
       "MYSQL_PASSWORD": "@mysql_password",
       "MYSQL_DATABASE": "@mysql_database"
     }
   }
  1. デプロイ:
   vercel
   # または GitHub と連携して自動デプロイ

MySQL 固有の注意点

  1. 接続プーリング:
  • MySQLでは接続プールの管理が重要
  • createPool を使用して接続を効率的に管理
  1. 日付処理:
  • MySQLの日付型はJavaScriptのDateオブジェクトに自動変換
  • SSG/SSRでpropsに渡す際は JSON.parse(JSON.stringify()) でシリアライズ
  1. LAST_INSERT_ID():
  • 挿入後のID取得には LAST_INSERT_ID() 関数を使用
  1. PlanetScale を使用する場合:
  • サーバーレス環境に最適化されたMySQL互換データベース
  • 接続に ssl: { rejectUnauthorized: true } が必要

ステップ 9: 接続プーリングの最適化(本番環境向け)

lib/db.ts を更新して本番環境用に最適化:

import { createPool, Pool } from 'mysql2/promise';
import dotenv from 'dotenv';

dotenv.config();

const isProduction = process.env.NODE_ENV === 'production';

let pool: Pool;

export async function initializePool() {
  pool = createPool({
    host: process.env.MYSQL_HOST,
    port: parseInt(process.env.MYSQL_PORT || '3306'),
    database: process.env.MYSQL_DATABASE,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    ssl: isProduction ? { rejectUnauthorized: true } : undefined,
    waitForConnections: true,
    connectionLimit: isProduction ? 20 : 10,
    queueLimit: 0,
    idleTimeout: 60000, // アイドル接続のタイムアウト(ms)
    enableKeepAlive: true, // 接続を維持
    keepAliveInitialDelay: 0,
  });
}

export async function query(sql: string, values?: any[]) {
  if (!pool) await initializePool();
  try {
    const [rows] = await pool.query(sql, values);
    return rows;
  } catch (error) {
    console.error('MySQL query error:', error);
    throw error;
  }
}

// アプリケーション終了時に接続プールをクリーンアップ
process.on('SIGINT', async () => {
  if (pool) {
    await pool.end();
    console.log('MySQL connection pool closed');
  }
  process.exit(0);
});

ステップ 10: セキュリティのベストプラクティス(MySQL版)

  1. SSL接続の強制:
  • 本番環境では必ずSSLを使用
   ssl: { rejectUnauthorized: true }
  1. プリペアドステートメント:
  • SQLインジェクション防止のために常にパラメータ化クエリを使用
   await query('SELECT * FROM users WHERE id = ?', [userId]);
  1. 最小権限の原則:
  • アプリケーションユーザーには必要な権限のみ付与
  1. 接続情報の保護:
  • 環境変数を使用
  • ソースコードに直接記述しない
  1. 定期的なパスワードローテーション:
  • データベース認証情報を定期的に更新

代替ORMオプション(MySQL)

MySQLでORMを使用する場合:

  1. Prisma:
   npm install prisma @prisma/client
   npx prisma init

schema.prisma でMySQLを指定:

   datasource db {
     provider = "mysql"
     url      = env("DATABASE_URL")
   }
  1. Sequelize:
   npm install sequelize mysql2
  1. TypeORM:
   npm install typeorm reflect-metadata mysql2

トラブルシューティング(MySQL版)

  1. 接続エラー:
  • エラー: ER_NOT_SUPPORTED_AUTH_MODE
   ALTER USER 'username'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
  1. タイムゾーン問題:
   // 接続設定に追加
   timezone: '+09:00' // 日本時間の場合
  1. Vercelで接続できない:
  • VercelのIPをデータベースの許可リストに追加
  • PlanetScaleなどVercelと相性の良いサービスを検討
  1. 接続タイムアウト:
   // プール設定に追加
   connectTimeout: 10000, // 10秒

パフォーマンスチューニング

  1. インデックスの追加:
   CREATE INDEX idx_users_email ON users(email);
  1. EXPLAIN の使用:
   const explain = await query('EXPLAIN SELECT * FROM users WHERE email = ?', [email]);
   console.log(explain);
  1. 適切なデータ型の選択:
  • VARCHAR ではなく TEXT が必要な場合
  • INT ではなく TINYINT で十分な場合

このガイドで、Next.js アプリケーションから MySQL データベースに接続する方法を理解できたはずです。実際のプロジェクトでは、要件に応じてさらに最適化を加えてください。