Dev Study
← 解説「トランザクション」に戻る

サンプルコードで身につける: トランザクション

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1All or Nothing の最小イメージ

送金で「引く」と「足す」の片方だけが反映されると残高が壊れる、というトランザクションの動機をコメントで整理した例です。両方成功か、両方なかったことにするか、の二択しかないという原則を最小単位で示します。

// トランザクション = 複数の更新を「全部成功」か「全部失敗」にまとめる
//
// 送金: 口座A から 300 引いて、口座B に 300 足す
//
//   ① UPDATE accounts SET balance = balance - 300 WHERE id = 'A'
//   ② UPDATE accounts SET balance = balance + 300 WHERE id = 'B'
//
// もし ① の後で落ちると…
//   A だけ減って B は増えない → 300 円が消失(不整合)
//
// トランザクションで囲むと:
//   ① と ② が両方成功 → COMMIT(確定)
//   途中で失敗       → ROLLBACK(①も含めて巻き戻す)

2transaction() による自動コミット/ロールバック

DataSource.transaction() のコールバック内が全部成功すれば自動でコミット、例外が投げられれば自動でロールバックされる手軽な書き方の例です。manager を通した更新だけが同じトランザクションに含まれる点がポイントです。

import { Injectable } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { Account } from "./account.entity";

@Injectable()
export class TransferService {
  constructor(@InjectDataSource() private readonly dataSource: DataSource) {}

  async transfer(fromId: number, toId: number, amount: number) {
    // コールバックが成功すれば commit、throw されれば rollback
    await this.dataSource.transaction(async (manager) => {
      const from = await manager.findOneByOrFail(Account, { id: fromId });
      const to = await manager.findOneByOrFail(Account, { id: toId });
      if (from.balance < amount) throw new Error("残高不足"); // → rollback
      from.balance -= amount;
      to.balance += amount;
      await manager.save([from, to]); // manager 経由が同一 Tx に入る
    });
  }
}

3commit / rollback の挙動を再現

確定済みデータの作業用コピー上で更新し、成功時だけ反映・失敗時は破棄する、というトランザクションの本質をプレーン TypeScript で再現した例です。ロールバック後に元データが無傷で残ることを実行結果で確認できます。

TypeScript

4QueryRunner で手動制御する

begin・commit・rollback を自分で呼ぶ QueryRunner を使った、より細かく制御する書き方の例です。finally で必ず release() して接続をプールに返す、というリソース解放の定型までがセットになります。

import { Injectable } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { Account } from "./account.entity";

@Injectable()
export class TransferService {
  constructor(@InjectDataSource() private readonly dataSource: DataSource) {}

  async transfer(fromId: number, toId: number, amount: number) {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    await runner.startTransaction(); // BEGIN
    try {
      const from = await runner.manager.findOneByOrFail(Account, { id: fromId });
      from.balance -= amount;
      await runner.manager.save(from);
      await runner.commitTransaction(); // COMMIT
    } catch (e) {
      await runner.rollbackTransaction(); // ROLLBACK
      throw e;
    } finally {
      await runner.release(); // 接続をプールに返す(必須)
    }
  }
}

5在庫減算と注文作成をまとめる

「在庫を減らす」と「注文を作る」を1つのトランザクションに束ねる実務の典型例です。在庫が足りなければ例外を投げて注文ごと巻き戻すことで、在庫だけ減って注文がない、といった中途半端な状態を防ぎます。

import { Injectable, BadRequestException } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { Product } from "./product.entity";
import { Order } from "./order.entity";

@Injectable()
export class OrdersService {
  constructor(@InjectDataSource() private readonly dataSource: DataSource) {}

  // 在庫減算と注文作成は「両方成功」でなければならない
  async placeOrder(productId: number, quantity: number) {
    return this.dataSource.transaction(async (manager) => {
      const product = await manager.findOneByOrFail(Product, { id: productId });
      if (product.stock < quantity) {
        throw new BadRequestException("在庫不足"); // → 在庫も注文も巻き戻る
      }
      product.stock -= quantity; // ① 在庫を減らす
      await manager.save(product);
      // ② 注文を作る(①②が同一 Tx。片方だけ反映されることはない)
      return manager.save(manager.create(Order, { productId, quantity }));
    });
  }
}