👋

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