例えば、ネットショップのアプリを作ってて、注文の支払いの時にやることがあるとするよ:
- お客さんのカードからお金を引き落とす。
- 倉庫の在庫数を減らす。
- 成功したトランザクションの記録を作る。
もしこの途中で何かトラブルが起きたらどうなる?例えば、お金は引き落としたけど、在庫がなくなって注文の記録が作れなかった場合。全部グチャグチャだよね:お金は「宙ぶらりん」、注文は未完了、サーバーには怒りのメール(もしかしたら訴訟も)大量に届く。
こういうのを防ぐためにトランザクションがあるんだ。複数の操作をまとめて「アトミック」な単位でDBに投げられる。テキストエディタの「元に戻す」ボタンみたいな感じ。何か失敗したら、最初に戻せる。
トランザクションはどうやってデータ整合性を守るの?
トランザクションはACIDっていう考え方に基づいてる:
- アトミック性 (Atomicity) — トランザクション内の操作は全部やるか、全くやらないか。「全部かゼロか」だよ。
- 一貫性 (Consistency) — トランザクションの前後でデータは一貫した状態になる。
- 分離性 (Isolation) — 他のトランザクションに邪魔されない。
- 永続性 (Durability) — トランザクションが終わったら、システム障害があっても結果は残る。
なんでまたこれを繰り返すのかって?だってこれが理想だから。でも…実際はなかなか全部は守れない。コースの後半でまたトランザクションやる時、ACIDのいくつかは妥協しなきゃいけないって分かるよ。
だから今は、トランザクションがシンプルでキレイな時代を楽しもう。じゃあ、例を見ていこう!
トランザクションの使い方の例
学生を追加してコースに登録するシナリオを見てみよう。
例えば、大学のDBを使ってるとする。コースに空きがあれば、外部の受講者を一時的に学生として登録してコースに追加する。流れはこんな感じ。
新しい学生をDBに追加してコースに登録する時は:
studentsテーブルにレコードを追加。- 学生とコースをつなぐ
enrollmentsテーブルにレコードを作る。
もし途中で何か失敗したら(例えばコースが満員)、操作をロールバックして、テーブル間でデータがズレないようにしなきゃ。やり方はこう:
-- トランザクション開始
BEGIN;
-- ステップ1: 学生を追加
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- 例えば、id = 10 が返ってきたとする
-- ステップ2: コースに登録
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- 全部OKなら変更を確定
COMMIT;
エラーが起きたらどうなる?
例えば、コース登録でエラーが出た場合:コースが存在しないとか。トランザクションを忘れると、studentsテーブルには学生の記録が残るけど、enrollmentsには何もない。これじゃデータの整合性が崩れる。これを防ぐにはROLLBACKを使う。
-- トランザクション開始
BEGIN;
-- ステップ1: 学生を追加
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Male')
RETURNING id;
-- ステップ2: コースに登録しようとする
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- エラー: id = 999 のコースは存在しない!
-- 全部ロールバック
ROLLBACK;
こうすると、どの操作も実行されず、DBはトランザクション前の状態に戻る。
SAVEPOINTでコントロールする
もっと複雑なシナリオを考えてみよう。いくつかの操作をやるけど、途中のあるポイントまでだけ戻したい、全部キャンセルじゃなくて。
じゃあ、ステップごとに学生を登録する例をやってみる
-- トランザクション開始
BEGIN;
-- 学生を追加
SAVEPOINT add_student; -- セーブポイント作成
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Female');
-- 1つ目のコースに登録
SAVEPOINT enroll_course_1; -- さらにセーブポイント
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- 2つ目のコースに登録(ここでエラー)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- エラー!
-- 最後のセーブポイントまでロールバック
ROLLBACK TO enroll_course_1;
-- 続きをやる
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- 変更を確定
COMMIT;
こんな感じで、プロセスの一部でエラーがあっても、他の部分のデータはちゃんと残せる。
変更があったかどうかのチェック
SQLクエリで何か変更があったかどうか、実際に確認できる方法があるよ。
例えばDELETEを実行したけどWHEREに引っかかる行がなかったとか、UPDATEしたけどデータがすでに同じで何も変わらなかったとか。
そういう時は、システム変数FOUNDが使える。直前のSQLで行が変更されたかどうかを教えてくれる:
FOUND = TRUE— クエリで何か更新/削除があった;FOUND = FALSE— 何も削除・変更されてない。
普通のSELECTには使えないけど、変更を追跡する時に便利。
実践例:支払い処理
トランザクションは特に金融系アプリで超重要。もう一回、口座から口座へお金を送るシステムを例にしよう。
-- トランザクション開始
BEGIN;
-- ステップ1: 1つ目の口座からお金を引く
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- ステップ2: 操作が成功したかチェック(行が変更されたか)
IF NOT FOUND THEN
ROLLBACK; -- 残高不足ならロールバック
RAISE EXCEPTION '残高が足りないよ!'; -- エラー!例外を投げる
END IF;
-- ステップ3: 2つ目の口座にお金を足す
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- トランザクションを確定
COMMIT;
ここで、もしクライアントが口座残高以上のお金を送ろうとしたら、トランザクションはロールバックされて、DBが「宙ぶらりん」にならない。
特徴とよくあるミス
COMMIT忘れ: トランザクションの最後にCOMMITし忘れると、DBは「待ち状態」になって、変更が保存されない。
WHERE忘れ: 条件なしでデータを更新・削除すると大惨事になることも。例えばDELETE FROM studentsをWHEREなしでやると、全学生が消える。
長いトランザクション: トランザクションを開きっぱなしにすると、データへのアクセスがブロックされてパフォーマンスが落ちる。必ずCOMMITかROLLBACKで早めに終わらせよう。
トランザクションは、データ整合性を守る時の唯一の味方。特にユーザー登録、支払い処理、関連テーブルの更新みたいな複雑なシナリオで、データの不整合を防いでくれる。BEGIN、COMMIT、ROLLBACK、SAVEPOINTを使いこなせば、もっと信頼できて安全なアプリが作れるよ。
GO TO FULL VERSION