👋
Expressでのトランザクション
概要
ExpressでPostgreへのトランザクション処理を書いた際、途中で気づいたことがあるので記録しておく。
コードの問題点
もともとは以下のような記述をしていた。
const db = require('../db/database');
/**
* 温泉の名前を編集するコントローラー
* @route PUT /api/onsen/:id/nameedit
* @param {number} req.params.id - 温泉ID
* @param {string} req.body.newName - 新しい温泉の名前
* @param {object} req.user - 認証済みユーザー情報 (JWTから取得)
*/
exports.editOnsenName = async (req, res) => {
const { id } = req.params; // URLパスから温泉IDを取得
const { newName } = req.body;
const userId = req.user.id;
const userResult = await db.query(`SELECT role FROM users WHERE id = $1`, [userId]);
if (userResult.rows[0].role == '温泉家' || userResult.rows[0].role == '探湯者') {
return res.status(401).json({ message: '権限が確認できませんでした'})
}
if (!newName || newName.trim() === ""){
return res.status(400).json({ message: '新しい名前は必須です。' });
}
const client = await db.connect(); // トランザクション用にクライアントを取得
try {
// 1. 温泉が存在するか確認
const onsenResult = await client.query('SELECT * FROM hot_springs WHERE id = $1', [id]);
if (onsenResult.rows.length === 0) {
return res.status(404).json({ message: '指定された温泉が見つかりませんでした。' });
}
await client.query('BEGIN'); // トランザクション開始
// 2. 名前の重複チェック
const nameCheck = await client.query('SELECT * FROM hot_springs WHERE name = $1', [newName]);
if (nameCheck.rows.length > 0) {
await client.query('ROLLBACK');
return res.status(409).json({ message: 'その名前は既に使用されています。' });
}
// 4. 名前の更新
await client.query(`
UPDATE hot_springs
SET name = $1, name_changer_user_id = $2, WHERE id = $3
`, [newName, userId, id]);
await client.query('COMMIT'); // トランザクションをコミット
return res.status(200).json({ message: '名前が正常に更新されました。' });
} catch (e) {
await client.query('ROLLBACK');
console.error('温泉名前編集エラー:', e);
return res.status(500).json({ message: '名前の更新中にエラーが発生しました。' });
} finally {
client.release(); // クライアントを解放
}
}
このコードではトランザクション処理を開始しているawait client.query('BEGIN');よりも後ろでreturn res.status(200).json({ message: '名前が正常に更新されました。' });などによって処理を終了してしまっているため、client.release();の処理まで行われず、データベースとの接続がそのままになってしまう。
これを回避するためには条件分岐ごとに個別にclient.release();か、クエリがUPDATEなどの処理一つの場合は、条件分岐をトランザクションの外で行う必要がある。
修正店
以下は修正後の処理
const db = require('../db/database');
/**
* 温泉の名前を編集するコントローラー
* @route PUT /api/onsen/:id/nameedit
* @param {number} req.params.id - 温泉ID
* @param {string} req.body.newName - 新しい温泉の名前
* @param {object} req.user - 認証済みユーザー情報 (JWTから取得)
*/
exports.editOnsenName = async (req, res) => {
const { id } = req.params; // URLパスから温泉IDを取得
const { newName } = req.body;
const userId = req.user.id;
const userResult = await db.query(`SELECT role FROM users WHERE id = $1`, [userId]);
if (userResult.rows[0].role == '温泉家' || userResult.rows[0].role == '探湯者') {
return res.status(401).json({ message: '権限が確認できませんでした'})
}
if (!newName || newName.trim() === ""){
return res.status(400).json({ message: '新しい名前は必須です。' });
}
const client = await db.connect(); // トランザクション用にクライアントを取得
try {
// 1. 温泉が存在するか確認
const onsenResult = await client.query('SELECT * FROM hot_springs WHERE id = $1', [id]);
if (onsenResult.rows.length === 0) {
client.release();
return res.status(404).json({ message: '指定された温泉が見つかりませんでした。' });
}
await client.query('BEGIN'); // トランザクション開始
// 2. 名前の重複チェック
const nameCheck = await client.query('SELECT * FROM hot_springs WHERE name = $1', [newName]);
if (nameCheck.rows.length > 0) {
await client.query('ROLLBACK');
client.release();
return res.status(409).json({ message: 'その名前は既に使用されています。' });
}
// 4. 名前の更新
await client.query(`
UPDATE hot_springs
SET name = $1, name_changer_user_id = $2, WHERE id = $3
`, [newName, userId, id]);
await client.query('COMMIT'); // トランザクションをコミット
res.status(200).json({ message: '名前が正常に更新されました。' });
} catch (e) {
await client.query('ROLLBACK');
console.error('温泉名前編集エラー:', e);
res.status(500).json({ message: '名前の更新中にエラーが発生しました。' });
} finally {
client.release(); // クライアントを解放
}
}
修正点は
- クエリ成功時・例外処理を補足時に
finaly{...以降が実行されるようreturnを削除 - 各条件分岐での
client.release();での追加
Discussion