Day14 前半のゴール
「“お金の振込”をイメージしながらトランザクションを理解する」
今日のテーマはトランザクションとロックです。
ここから一気に「実務っぽさ」が増えます。
まず一言で言うと、トランザクションはこうです。
「この一連の処理は、全部成功するか、全部なかったことにするか」
Day14 前半のゴールは、
BEGIN / COMMIT / ROLLBACK の意味と流れを理解する
「途中で失敗しても“中途半端な状態”を残さない」イメージを持つ
ロックという言葉を“ざっくり”イメージできるようにしておく
ここまで行けば、後半の「ロックの種類」「競合の具体例」が入ってきます。
トランザクションとは何か
「“ひとかたまりの処理”を1単位として扱う仕組み」
まずはイメージからいきます。
テーブルは「データの箱」
SQLは「箱に対する操作」
トランザクションは、その操作をまとめる「枠」です。
例えば、銀行振込を考えます。
Aさんの口座から1万円引く
Bさんの口座に1万円足す
この2つはセットで成功してほしいですよね。
Aからは引かれたのに、Bに入っていない
Bに入ったのに、Aから引かれていない
こういう「中途半端な状態」は絶対にダメです。
トランザクションは、こういうときに使います。
「この2つ(あるいはもっと多く)のSQLは、ひとかたまりとして扱ってくれ。全部成功したら確定、途中で失敗したら全部なかったことにしてくれ」
これがトランザクションです。
BEGIN / COMMIT / ROLLBACK の基本
「“録画開始・保存・取り消し”の3ボタン」
MySQL では、トランザクションはだいたいこう書きます。
START TRANSACTION; -- または BEGIN;
-- ここに複数のSQLを書く
COMMIT; -- ここで確定
-- または
ROLLBACK; -- ここで全部なかったことにする
SQLイメージとしては、
START TRANSACTION / BEGIN
「ここから先の変更を“まとめて扱う”モードに入る」
COMMIT
「ここまでの変更を正式に保存する」
ROLLBACK
「ここまでの変更を全部取り消して、BEGIN前の状態に戻す」
という3つのボタンです。
重要なのは、
BEGIN から COMMIT / ROLLBACK までの間に行われた変更は、
COMMIT するまでは“仮の状態”として扱われる
という点です。
具体例:銀行振込テーブルで考える
「2つのUPDATEを“ひとかたまり”にする」
シンプルな口座テーブルを考えます。
CREATE TABLE accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
balance INT NOT NULL
) ENGINE=InnoDB;
SQLAさん(id=1)からBさん(id=2)に1万円振り込む処理は、本質的にはこうです。
UPDATE accounts
SET balance = balance - 10000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 10000
WHERE id = 2;
SQLこれをトランザクションなしで実行すると、
1つ目のUPDATEは成功
2つ目のUPDATEでエラー(例:Bさんの行がロックされていた)
という状況になったとき、
Aさんの残高だけ減って、Bさんには入っていない
という「中途半端な状態」がDBに残ってしまいます。
これを避けるために、トランザクションを使います。
振込処理をトランザクションで書く
「途中で失敗したら全部 ROLLBACK」
同じ処理を、トランザクション付きで書くとこうなります。
START TRANSACTION;
UPDATE accounts
SET balance = balance - 10000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 10000
WHERE id = 2;
COMMIT;
SQLこの場合、
2つのUPDATEが両方成功したら COMMIT され、
Aさんの残高減少とBさんの残高増加がセットで確定します。
もし2つ目のUPDATEでエラーが起きたら、
アプリ側やストアドプロシージャ側で ROLLBACK を呼び出すことで、
BEGIN 前の状態(振込前の残高)に戻す
ことができます。
ここでの本質は、
「成功したらまとめて反映、失敗したらまとめてなかったこと」
という一点です。
トランザクションがない世界との違い
「“1文ずつ即確定”から“まとめて確定”へ」
SQLite をデフォルト設定で使っていたとき、
多くの場合は「1つのSQLが終わるたびに自動で確定」していました。
MySQL でも、オートコミット(autocommit = 1)が有効だと、
1つのUPDATE / INSERT / DELETEが終わるたびに自動でCOMMIT
という動きになります。
トランザクションを明示的に使うというのは、
「自動で確定」モードを一時的に止めて、
自分で BEGIN / COMMIT / ROLLBACK を管理する
ということです。
これによって、
複数のSQLを「1つの論理的な操作」として扱える
ようになります。
ロックの概念を“ざっくり”つかむ
「“今この行はこの人が触ってるから、ちょっと待って”という仕組み」
トランザクションとセットで出てくるのが「ロック」です。
ロックを一言で言うと、
「このデータは今このトランザクションが触っているから、他の人は勝手に変えないで」
という“予約札”のようなものです。
例えば、
トランザクションAが、ある行の残高を更新しようとしている
その途中で、トランザクションBも同じ行を更新しようとする
このとき、何も仕組みがなければ、
AとBが同時に古い残高を読み、
それぞれ勝手に新しい残高を書き込んでしまう
という「書き込みの競合」が起きます。
ロックは、これを防ぐためにあります。
「Aがこの行を更新中だから、BはAが終わるまで待ってね」
というルールを、DBが自動で守ってくれます。
トランザクションとロックの関係
「“ひとかたまりの処理”の間、必要なデータを守る」
トランザクションを開始して、
ある行をUPDATEすると、その行にはロックがかかります。
そのトランザクションが COMMIT / ROLLBACK で終わるまで、
他のトランザクションはその行を勝手に更新できません。
これによって、
Aさんの残高を減らしている途中で、
別の処理が同じAさんの残高をいじってしまう
といった事故を防げます。
イメージとしては、
BEGIN で「作業開始」
必要な行にロックをかけて作業
COMMIT で「作業完了、ロック解除」
という流れです。
Day14 前半では、
ロック=「トランザクション中にデータを守るための“予約札”」
くらいのイメージを持っておけば十分です。
トランザクションが必要になる典型パターン
「“複数テーブル・複数行にまたがる更新”は基本トランザクション」
ここまでの話を、実務寄りにまとめると、
トランザクションを使うべきなのは、特にこんな場面です。
1つの操作が、複数のUPDATE / INSERT / DELETEから成り立っている
(振込、在庫引き当て+注文作成、など)
複数テーブルにまたがる整合性を守りたい
(注文テーブルと在庫テーブル、ユーザーと権限テーブル、など)
途中でエラーが起きたときに「中途半端な状態」を絶対に残したくない
逆に、
1行だけINSERTする
1行だけUPDATEする
といった単純な操作は、
オートコミットのままでも問題ないことが多いです。
Day14 の段階では、
「これは“ひとかたまり”として扱いたい処理か?」
と自分に問いかけて、
YES ならトランザクションを意識する、という癖をつけていくといいです。
Day14 前半のまとめ
トランザクションは「複数のSQLをひとかたまりの操作として扱い、全部成功したら COMMIT で確定し、途中で問題があれば ROLLBACK で BEGIN 前の状態に戻す」仕組みであり、銀行振込のように「Aから引いてBに足す」の2つのUPDATEをセットで成功させたい場面で、「片方だけ反映される」という中途半端な状態を防ぐために使う。
MySQL では START TRANSACTION(または BEGIN)でトランザクションを開始し、その後の UPDATE / INSERT / DELETE は COMMIT されるまで“仮の状態”として扱われ、同時に更新対象の行にはロックがかかるため、他のトランザクションが同じ行を勝手に書き換えることを防ぎ、「この行は今この処理が触っているから、終わるまで待って」という“予約札”のような役割を果たす。
SQLite の「1文ごと自動確定」的な世界から一歩進んで、「複数テーブル・複数行にまたがる更新を1つの論理操作として扱う」「途中で失敗したら全部なかったことにする」という感覚を持ち、特に振込・在庫引き当て・複数テーブル更新のような“絶対に中途半端にしたくない処理”では、トランザクションとロックをセットで意識することが、実務的なSQL力の土台になる。
後半では、
ロックの種類(行ロック・テーブルロック)や、
トランザクション同士がぶつかったときに何が起きるか(待ち・デッドロック)、
そして「安全にトランザクションを設計するコツ」を、
具体的な例と一緒に掘り下げていきます。
