
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();
}
});
データベース操作のベストプラクティス
- SQLインジェクション対策: プレースホルダーを使用する(
?
) - 接続プールの使用: 効率的な接続管理
- エラーハンドリング: 適切なエラー処理とロギング
- トランザクション: 複数操作の原子性確保
- インデックスの活用: 検索性能の向上
次のステップ
今回は生のSQLを直接実行する方法を学びましたが、実際の開発ではORM(Object-Relational Mapping)を使用することが一般的です。次回はPrisma ORMを導入し、より安全かつ効率的なデータベース操作を学びます。Prismaを使用すると、型安全なクエリを書くことができ、データベーススキーマの変更管理も容易になります。
まとめ
MySQLの基本とCRUD操作について学びました:
- MySQLの基本概念とインストール方法
- SQLの基本構文(CREATE, SELECT, INSERT, UPDATE, DELETE)
- Node.jsからのMySQL接続方法
- Expressアプリケーションとの連携実装
- トランザクション処理とエラーハンドリング
データベース連携により、TODOアプリケーションは真に实用的なアプリケーションへと進化しました。次回は、これらのSQL操作をより現代的な方法で実装するためのPrisma ORMを学びましょう。