MySQLの基本とCRUD操作

2025-08-28

はじめに

前回はExpress.jsを使用してREST APIを構築する方法を学びました。しかし、データはメモリ上に保存していたため、サーバーを再起動するとすべて失われてしまいます。今回は、データを永続的に保存するためのリレーショナルデータベースとしてMySQLを導入し、基本的なCRUD操作(Create, Read, Update, Delete)を学びます。

MySQLとは

MySQLは世界で最も人気のあるオープンソースのリレーショナルデータベース管理システム(RDBMS)の一つです。以下の特徴があります:

  • オープンソース: 無料で利用可能
  • 高性能: 大規模なデータセットでも高速に動作
  • 信頼性: 多くの企業で採用されている実績あり
  • コミュニティ: 豊富な資料と活発なコミュニティ

MySQLのインストール

1. 公式サイトからのインストール

MySQL Community Serverを公式サイトからダウンロードしてインストールします。

2. Dockerを使用する方法(推奨)

Dockerを使うと環境構築が簡単になります:

# MySQLコンテナの実行
docker run --name mysql-container -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=todo_app -e MYSQL_USER=app_user -e MYSQL_PASSWORD=password -p 3306:3306 -d mysql:8.0

# コンテナへの接続
docker exec -it mysql-container mysql -u root -p

3. クラウドサービスを利用する方法

MySQL互換のクラウドサービス(PlanetScale, Amazon RDS, Google Cloud SQLなど)を利用することもできます。

データベース設計の基本

テーブル設計

TODOアプリケーションでは、少なくとも以下のようなテーブルが必要です:

todosテーブル:

  • id: 一意の識別子(主キー)
  • title: TODOのタイトル
  • completed: 完了状態
  • created_at: 作成日時
  • updated_at: 更新日時

SQLによるテーブル作成

CREATE DATABASE IF NOT EXISTS todo_app;
USE todo_app;

CREATE TABLE todos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

SQLの基本構文

データの操作(CRUD操作)

1. Create(作成) – INSERT文

-- 単一レコードの挿入
INSERT INTO todos (title, completed) VALUES ('Reactを学ぶ', FALSE);

-- 複数レコードの挿入
INSERT INTO todos (title, completed) VALUES 
('Expressを学ぶ', FALSE),
('MySQLを学ぶ', TRUE);

2. Read(読み取り) – SELECT文

-- すべてのカラムを取得
SELECT * FROM todos;

-- 特定のカラムのみ取得
SELECT id, title, completed FROM todos;

-- 条件指定(WHERE句)
SELECT * FROM todos WHERE completed = TRUE;

-- 並び替え(ORDER BY句)
SELECT * FROM todos ORDER BY created_at DESC;

-- 件数制限(LIMIT句)
SELECT * FROM todos LIMIT 5;

-- あいまい検索(LIKE句)
SELECT * FROM todos WHERE title LIKE '%学ぶ%';

-- 複合条件
SELECT * FROM todos 
WHERE completed = FALSE 
AND created_at > '2023-01-01' 
ORDER BY created_at DESC 
LIMIT 10;

3. Update(更新) – UPDATE文

-- 単一レコードの更新
UPDATE todos SET completed = TRUE WHERE id = 1;

-- 複数レコードの更新
UPDATE todos SET completed = TRUE WHERE id IN (1, 2, 3);

-- 複数カラムの更新
UPDATE todos SET title = 'MySQLをマスターする', completed = TRUE WHERE id = 3;

4. Delete(削除) – DELETE文

-- 単一レコードの削除
DELETE FROM todos WHERE id = 1;

-- 複数レコードの削除
DELETE FROM todos WHERE completed = TRUE;

-- 全レコードの削除(注意して使用すること)
DELETE FROM todos;

Node.jsからMySQLに接続

mysql2パッケージのインストール

npm install mysql2

データベース接続の設定

// db.js
const mysql = require('mysql2/promise');

const dbConfig = {
  host: 'localhost',
  user: 'app_user',
  password: 'password',
  database: 'todo_app',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
};

// 接続プールの作成
const pool = mysql.createPool(dbConfig);

module.exports = pool;

Expressアプリケーションでの使用

前回作成したTODO APIをMySQL連携版に改造しましょう:

// server.js
const express = require('express');
const pool = require('./db'); // データベース接続モジュール

const app = express();
const port = 3000;

app.use(express.json());

// GET /todos - すべてのTODOを取得
app.get('/todos', async (req, res) => {
  try {
    const [rows] = await pool.execute('SELECT * FROM todos ORDER BY created_at DESC');
    res.json(rows);
  } catch (error) {
    console.error('データ取得エラー:', error);
    res.status(500).json({ error: 'データの取得に失敗しました' });
  }
});

// GET /todos/:id - 特定のTODOを取得
app.get('/todos/:id', async (req, res) => {
  try {
    const id = req.params.id;
    const [rows] = await pool.execute('SELECT * FROM todos WHERE id = ?', [id]);
    
    if (rows.length === 0) {
      return res.status(404).json({ error: 'TODOが見つかりません' });
    }
    
    res.json(rows[0]);
  } catch (error) {
    console.error('データ取得エラー:', error);
    res.status(500).json({ error: 'データの取得に失敗しました' });
  }
});

// POST /todos - 新しいTODOを作成
app.post('/todos', async (req, res) => {
  try {
    const { title, completed = false } = req.body;
    
    if (!title || title.trim() === '') {
      return res.status(400).json({ error: 'タイトルは必須です' });
    }
    
    const [result] = await pool.execute(
      'INSERT INTO todos (title, completed) VALUES (?, ?)',
      [title.trim(), completed]
    );
    
    const [newTodo] = await pool.execute('SELECT * FROM todos WHERE id = ?', [result.insertId]);
    res.status(201).json(newTodo[0]);
  } catch (error) {
    console.error('データ作成エラー:', error);
    res.status(500).json({ error: 'TODOの作成に失敗しました' });
  }
});

// PUT /todos/:id - TODOを更新
app.put('/todos/:id', async (req, res) => {
  try {
    const id = req.params.id;
    const { title, completed } = req.body;
    
    // 現在のデータを取得
    const [currentRows] = await pool.execute('SELECT * FROM todos WHERE id = ?', [id]);
    
    if (currentRows.length === 0) {
      return res.status(404).json({ error: 'TODOが見つかりません' });
    }
    
    const currentTodo = currentRows[0];
    const newTitle = title !== undefined ? title : currentTodo.title;
    const newCompleted = completed !== undefined ? completed : currentTodo.completed;
    
    await pool.execute(
      'UPDATE todos SET title = ?, completed = ? WHERE id = ?',
      [newTitle, newCompleted, id]
    );
    
    const [updatedRows] = await pool.execute('SELECT * FROM todos WHERE id = ?', [id]);
    res.json(updatedRows[0]);
  } catch (error) {
    console.error('データ更新エラー:', error);
    res.status(500).json({ error: 'TODOの更新に失敗しました' });
  }
});

// DELETE /todos/:id - TODOを削除
app.delete('/todos/:id', async (req, res) => {
  try {
    const id = req.params.id;
    
    const [result] = await pool.execute('DELETE FROM todos WHERE id = ?', [id]);
    
    if (result.affectedRows === 0) {
      return res.status(404).json({ error: 'TODOが見つかりません' });
    }
    
    res.status(204).send();
  } catch (error) {
    console.error('データ削除エラー:', error);
    res.status(500).json({ error: 'TODOの削除に失敗しました' });
  }
});

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  console.error('予期せぬエラー:', error);
  res.status(500).json({ error: 'サーバー内部エラーが発生しました' });
});

app.listen(port, () => {
  console.log(`TODO APIサーバーが起動しました: http://localhost:${port}`);
});

トランザクションの処理

複数のデータベース操作を原子性(Atomicity)を持って実行するにはトランザクションを使用します:

// トランザクションの例
app.post('/todos/batch', async (req, res) => {
  let connection;
  try {
    connection = await pool.getConnection();
    await connection.beginTransaction();
    
    const { todos } = req.body;
    
    for (const todo of todos) {
      await connection.execute(
        'INSERT INTO todos (title, completed) VALUES (?, ?)',
        [todo.title, todo.completed || false]
      );
    }
    
    await connection.commit();
    res.status(201).json({ message: '一括登録が成功しました' });
  } catch (error) {
    if (connection) await connection.rollback();
    console.error('一括登録エラー:', error);
    res.status(500).json({ error: '一括登録に失敗しました' });
  } finally {
    if (connection) connection.release();
  }
});

データベース操作のベストプラクティス

  1. SQLインジェクション対策: プレースホルダーを使用する(?
  2. 接続プールの使用: 効率的な接続管理
  3. エラーハンドリング: 適切なエラー処理とロギング
  4. トランザクション: 複数操作の原子性確保
  5. インデックスの活用: 検索性能の向上

次のステップ

今回は生のSQLを直接実行する方法を学びましたが、実際の開発ではORM(Object-Relational Mapping)を使用することが一般的です。次回はPrisma ORMを導入し、より安全かつ効率的なデータベース操作を学びます。Prismaを使用すると、型安全なクエリを書くことができ、データベーススキーマの変更管理も容易になります。

まとめ

MySQLの基本とCRUD操作について学びました:

  1. MySQLの基本概念とインストール方法
  2. SQLの基本構文(CREATE, SELECT, INSERT, UPDATE, DELETE)
  3. Node.jsからのMySQL接続方法
  4. Expressアプリケーションとの連携実装
  5. トランザクション処理とエラーハンドリング

データベース連携により、TODOアプリケーションは真に实用的なアプリケーションへと進化しました。次回は、これらのSQL操作をより現代的な方法で実装するためのPrisma ORMを学びましょう。