Skip to content
Code Blue プログラミング学習ノート
Javascript/React

MySQLの基本とCRUD操作

はじめに

前回は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();
  }
});

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

データベース操作を安全かつ効率的に行うためには、いくつかの重要なベストプラクティスを遵守する必要があります。まず、セキュリティの観点から最も重要なのがSQLインジェクション対策であり、クエリ実行時には直接値を埋め込むのではなく、必ずプレースホルダー(?など)を使用したプリペアドステートメントを利用しなければなりません。

また、パフォーマンスとリソース管理の面では、リクエストのたびに接続を確立するのではなく、データベース接続プールを活用することで、限られた接続リソースを効率的に再利用することが推奨されます。アプリケーションの堅牢性を高めるためには、適切なエラーハンドリングが不可欠であり、例外発生時にはそれを適切に捕捉してユーザーへ配慮した処理を行うとともに、詳細なログを記録して後々の調査に役立てる必要があります。

さらに、複数のデータ更新操作を行う際には、データの整合性を保つためにトランザクションを利用し、一連の操作が「すべて成功する」か「すべて失敗する(ロールバックされる)」という原子性を確保することが必須です。最後に、データベースの検索性能を最適化し、アプリケーションの応答速度を向上させるために、検索条件や結合キーとして頻繁に使用されるカラムに対しては、適切にインデックスを活用することが重要です。

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

まとめ

今回の学習では、MySQLの基本概念から実践的なCRUD操作までの一連の流れを習得しました。まず、MySQLの基本的な仕組みとシステムへのインストール方法を理解し、データベース操作の基礎となるCREATE、SELECT、INSERT、UPDATE、DELETEといったSQLの基本構文について学びました。

次に、Node.js環境からMySQLデータベースへ接続するための具体的な手法を実装しました。この接続を基盤として、WebアプリケーションフレームワークであるExpressとMySQLを連携させ、アプリケーション上でデータの永続化を行う実装力を養いました。さらに、データの整合性を保つためのトランザクション処理や、予期せぬエラーに対処するためのエラーハンドリングについても学習し、実務で必要とされる堅牢なデータベース操作の技術を包括的に学びました。

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