購入トランザクション
購入は1つの操作ではない
フリマで商品が買われたとき、やることは 2 つあります。
- その商品を売り切れにする
- 購入の記録を残す
素直に書くと、こうなります。
JavaScript
db.prepare("UPDATE items SET sold = 1 WHERE id = ?").run(id);
db.prepare("INSERT INTO purchases (item_id, buyer) VALUES (?, ?)").run(id, buyer);動きます。ただし1 行目と 2 行目のあいだで何かが起きたらどうなるでしょうか。
途中で止まると壊れる
サーバーが落ちる、電源が切れる、2 行目でエラーになる。何であれ、途中で止まると次の状態が残ります。
プレーンテキスト
商品は売り切れ。だが購入記録は無い。誰も買っていないのに、誰も買えない商品ができあがります。どちらか片方だけが起きた状態が、いちばん困ります。
全部か、何もしないか
トランザクションは、複数の操作をまとめて 1 つとして扱う仕組みです。
JavaScript
db.exec("BEGIN");
try {
db.prepare("UPDATE items SET sold = 1 WHERE id = ?").run(id);
db.prepare("INSERT INTO purchases (item_id, buyer) VALUES (?, ?)").run(id, buyer);
db.exec("COMMIT");
} catch (error) {
db.exec("ROLLBACK");
throw error;
}| 命令 | 意味 |
|---|---|
BEGIN | ここからをひとまとまりにする |
COMMIT | 全部を確定する |
ROLLBACK | 全部を無かったことにする |
ROLLBACK を呼ぶと、BEGIN の後にした変更がすべて消えます。1 つ目の UPDATE が成功していても戻ります。
途中の状態が残らない。これがトランザクションの値打ちです。
売り切れを先に確かめる
もう 1 つ必要なことがあります。すでに売れている商品は、買えません。
JavaScript
const item = db.prepare("SELECT id, sold FROM items WHERE id = ?").get(id);
if (!item) {
return res.status(404).json({ error: "商品が見つかりません" });
}
if (item.sold === 1) {
return res.status(409).json({ error: "すでに売り切れです" });
}409 は「いまの状態と矛盾する依頼」を表すコードです。商品は存在するので 404 ではなく、依頼の書き方も正しいので 400 でもありません。状態が合わないという失敗です。
手を動かす
演習では、購入の処理をトランザクションで書きます。途中で失敗したときに、何も起きていない状態へ戻せることを確かめてください。
要件
- 商品が無ければ
{status:"not_found", sold:0, purchases:0}を返す - すでに売り切れなら
{status:"conflict", sold:1, purchases:0}を返す - 買う人の名前が空なら
ROLLBACKして{status:"invalid", sold:0, purchases:0}を返し、正常ならCOMMITして{status:"ok", sold:1, purchases:1}を返す
入出力例
buy(1, "田中", false) → {"purchases":1,"sold":1,"status":"ok"}
buy(1, "", false) → {"purchases":0,"sold":0,"status":"invalid"}
buy(1, "田中", true) → {"purchases":0,"sold":1,"status":"conflict"}
buy(999, "田中", false) → {"purchases":0,"sold":0,"status":"not_found"}ヒント
編集 ゆめさく編集部