Python | DB・SQL:トランザクション

Python
スポンサーリンク

概要(トランザクションは「一連の処理を、成功か失敗かで丸ごと扱う仕組み」)

トランザクションは、データベースに対して行う「一連の処理」を、
「全部まとめて成功」か「全部なかったことにする(失敗)」かのどちらかで扱うための仕組みです。

お金の振り込み、在庫の更新、複数テーブルへの書き込みなど、
「途中まで成功して途中で失敗したら困る」処理を安全に行うために、
トランザクションという考え方が存在します。

ざっくり言うと、

この一連の SQL は、全部通ったら反映していい
途中で一つでも失敗したら、最初からなかったことにしてほしい

という「約束」をデータベースにしているイメージです。

ここから、まずはイメージを固めてから、
ACID 特性、COMMIT / ROLLBACK、Python からの使い方まで、
初心者向けにかみ砕いて説明していきます。


トランザクションのイメージを具体例でつかむ

送金処理の例(途中で止まると困る典型パターン)

一番イメージしやすいのは「銀行の振り込み」です。

A さんの口座から 1000 円引き落とし、
B さんの口座に 1000 円入金する。

これをデータベースでやると、だいたい次のような処理になります。

UPDATE accounts
SET balance = balance - 1000
WHERE id = 'A';

UPDATE accounts
SET balance = balance + 1000
WHERE id = 'B';
SQL

ここで問題になるのは、「1 行目だけ成功して、2 行目でエラーになった」ようなケースです。

A さんからは 1000 円引かれたのに、
B さんには 1000 円入っていない、という「お金が消える」状態になります。

これを防ぐために、

この 2 つの UPDATE は「運命共同体」にしたい
両方成功したら反映していい
どちらか一つでも失敗したら、両方ともなかったことにしてほしい

という約束をするのがトランザクションです。

トランザクションを使ったときの流れ

トランザクションを使うと、この処理は次のような流れになります。

トランザクション開始
A さんから 1000 円引く UPDATE
B さんに 1000 円足す UPDATE
両方成功したら COMMIT(確定)
途中でエラーが起きたら ROLLBACK(全部取り消し)

つまり、「一連の処理をひとまとめにして、成功か失敗かで丸ごと扱う」ことができるようになります。


トランザクションの 4 つの性質(ACID)をやさしく説明する

Atomicity(原子性):「全部やるか、全部やらないか」

Atomicity は「原子性」と訳されますが、
初心者目線では「全部やるか、全部やらないか」と覚えるのが一番しっくりきます。

さきほどの送金の例で言えば、

A から引いて B に足す、という 2 つの UPDATE は「セット」
どちらか片方だけが反映されることはない

という性質です。

途中でエラーが起きたら、
それまでに行った変更も含めて全部 ROLLBACK されます。

これがトランザクションの一番大事なポイントです。

Consistency(一貫性):「ルールが壊れないようにする」

Consistency は「一貫性」です。

例えば、「口座残高はマイナスになってはいけない」というルールがあるとします。
トランザクションの中でそのルールを破るような更新をしようとすると、
エラーになったり、ROLLBACK されたりして、
「ルールが壊れた状態」が確定されないようにします。

アプリ側のチェックやデータベースの制約(NOT NULL、UNIQUE、CHECK、外部キーなど)と組み合わさって、
「データが変な状態で保存されない」ことを保証するイメージです。

Isolation(分離性):「同時に動いても、お互いに変な影響を与えない」

Isolation は「分離性」です。

現実のシステムでは、
複数のユーザーが同時に操作します。

A さんが送金処理をしている最中に、
B さんが残高照会をするかもしれません。

このとき、

中途半端な状態(A からは引かれたけど B にはまだ入っていない状態)を
他のトランザクションから見えてしまうと、
「残高がおかしい」ように見えます。

Isolation は、

トランザクション同士が「ある程度お互いを見えなくする」ことで、
中途半端な状態を他から見えにくくする

という性質です。

実際には「分離レベル」という細かい設定がありますが、
初心者のうちは「同時に動いても変なことが起きないようにしてくれる」と理解しておけば十分です。

Durability(永続性):「COMMIT されたら、ちゃんと残る」

Durability は「永続性」です。

トランザクションが COMMIT されたら、
その変更は「ちゃんとディスクに書き込まれ、消えないように保証される」
という性質です。

例えば、COMMIT の直後にサーバーが落ちても、
再起動したときに変更内容がちゃんと残っている、
ということを保証します。

データベースは内部でログを書いたり、
ディスクへの書き込みを工夫したりして、
この性質を守っています。


SQL レベルでのトランザクションの書き方

BEGIN / COMMIT / ROLLBACK の基本

データベースによって細かい書き方は違いますが、
典型的なトランザクションの流れは次のようになります。

BEGIN;  -- トランザクション開始

UPDATE accounts
SET balance = balance - 1000
WHERE id = 'A';

UPDATE accounts
SET balance = balance + 1000
WHERE id = 'B';

COMMIT; -- ここで確定
SQL

もし途中で「やっぱりやめたい」「エラーが起きた」場合は、
COMMIT の代わりに ROLLBACK を呼びます。

BEGIN;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 'A';

-- ここで何かおかしいと気づいたとする

ROLLBACK;  -- ここまでの変更を全部なかったことにする
SQL

BEGIN から COMMIT / ROLLBACK までの間が、
「一つのトランザクション」です。

Python(素の DB-API)からのイメージ

Python からデータベースを扱うときも、
トランザクションの考え方は同じです。

DB-API 風に書くと、イメージはこんな感じです。

conn = get_connection()
try:
    with conn:
        with conn.cursor() as cur:
            cur.execute(
                "UPDATE accounts SET balance = balance - %s WHERE id = %s",
                (1000, "A"),
            )
            cur.execute(
                "UPDATE accounts SET balance = balance + %s WHERE id = %s",
                (1000, "B"),
            )
    # with conn ブロックを正常に抜けると COMMIT される
except Exception:
    # 例外が起きると自動で ROLLBACK される実装も多い
    raise
Python

Django なら transaction.atomic() を使うと、
この BEGIN / COMMIT / ROLLBACK をいい感じにラップしてくれます。


トランザクションが必要になる典型パターン

複数テーブルにまたがる更新

例えば、EC サイトで注文を確定するとき、

orders テーブルに注文を追加する
order_items テーブルに明細を追加する
products テーブルの在庫数を減らす

といった複数テーブルへの書き込みが発生します。

このどれか一つでも失敗したら、
「注文だけあるけど明細がない」
「在庫だけ減って注文がない」

といった「壊れた状態」になります。

だからこそ、これらを一つのトランザクションにまとめて、

全部成功したら COMMIT
どれか一つでも失敗したら ROLLBACK

という扱いにする必要があります。

残高や在庫など「数値の整合性」が重要なもの

残高、在庫、ポイント、スコアなど、
「数値の整合性」が重要なものは、
トランザクションと相性が良いです。

例えば、在庫を減らす処理で、

在庫を読み取る
在庫が 1 以上なら 1 減らす

という処理を、トランザクションなしで複数ユーザーが同時に行うと、
「在庫 1 なのに 2 人が買えてしまう」
といった問題が起きます。

トランザクションとロック(行ロックなど)を組み合わせることで、
こうした「同時実行によるおかしな状態」を防ぎます。


トランザクションでよくある誤解と注意点

「トランザクションを使えば、全部自動で安全になる」わけではない

トランザクションは強力ですが、
「使えば何でも自動で安全になる魔法」ではありません。

例えば、

トランザクションの外で変な UPDATE をしてしまう
アプリ側のロジックが間違っていて、そもそも計算がおかしい

といった場合は、
トランザクションがあっても「間違った結果を COMMIT」してしまいます。

トランザクションが守ってくれるのは、

途中で失敗したときに「中途半端な状態」が確定されない
同時実行時に「変な矛盾」が起きにくくなる

という部分です。

ロジックそのものの正しさは、
アプリ側の責任として設計・テストする必要があります。

トランザクションを長く持ちすぎると「ロック地獄」になる

トランザクション中に更新した行やテーブルは、
他のトランザクションからロックされることがあります。

例えば、

トランザクションを開始する
行を更新する
そのまま 1 分間何もしない

といったコードがあると、
その行を触りたい他の処理が 1 分間待たされる、
ということが起きます。

そのため、実務では、

トランザクションは「必要な処理だけを短くまとめる」
ユーザー入力待ちなどをトランザクションの中に入れない

といった設計がとても重要になります。


まとめ(トランザクションは「壊れた状態を残さないための最後の砦」)

トランザクションを初心者目線で整理すると、こうなります。

トランザクションは、「一連の処理をひとまとめにして、全部成功か全部失敗かで扱う」仕組み。
BEGIN から COMMIT / ROLLBACK までが 1 単位で、その中で行われた変更は、途中でエラーが起きたら全部なかったことにできる。
ACID(Atomicity, Consistency, Isolation, Durability)は、「全部やるか全部やらない」「ルールを壊さない」「同時実行でも変にならない」「確定したらちゃんと残る」という性質のセット。
複数テーブルにまたがる更新、残高や在庫の更新など、「途中で止まると困る処理」は必ずトランザクションで包むべき。
ただし、トランザクションを長く持ちすぎるとロックで詰まりやすくなるので、「必要な処理だけを短くまとめる」意識が大事。

タイトルとURLをコピーしました