1. 1回だけなら買える
  2. 続けて2回買おうとすると2回目は409
  3. 3回でも同じ
  4. 無い商品は404
コース一覧
Express道場 fleama開発室のIssue10件
#109 同じ商品が2人に売れた

Express道場 fleama開発室のIssue10件

Node.js入門で作り上げた fleama API の開発室に、スプリントの助っ人として参加する演習専用コースです。解説はありません。1問が1件の Issue で、報告者は商店街の住人たち。依頼と完成条件だけを読み、白紙から自分で組み立てます。手が止まったときのために、方針・使う構文・部分解の3段階のヒントを各問に用意しています。ヘルスチェックから始まり、一覧API、404の扱い、並び替えとページング、入力検証、自作ミドルウェア、エラー形式の統一、所有者チェック、二重販売を止めるトランザクションを経て、最後はいいね機能をエンドポイント設計から任されるところまで、10件の Issue で Express の主要な道具を一巡します。1問15分から45分、全10問で約5時間です。Node.js入門を終えて「作れたけれど実務で通用するか不安」と感じている方に向いています。

1
今週のスプリント
01. #101 監視用ヘルスチェックが欲しい15分
02. #102 カテゴリ一覧APIが欲しい20分
03. #103 消えた商品を開くと落ちる20分
04. #104 検索に並び替えとページングを25分
05. #105 0円出品が通ってしまう25分
06. #106 調査用のアクセスログを25分
07. #107 エラーの形式がバラバラ30分
08. #108 他人の出品を値下げできてしまう30分
09. #109 同じ商品が2人に売れた35分
10. #110 新機能「いいね」を設計から45分

#109 同じ商品が2人に売れた

師範 / 目安 35分


Issue #109 同じ商品が2人に売れた

報告者: 松田(事務局)

「事件だ。ワシが買うたはずの湯呑みが、別の人にも売れとる。二重販売じゃ」


コードを読むと、購入はこう書かれています。

JavaScript

const item = db.prepare("SELECT * FROM items WHERE id = ?").get(id); if (item.sold === 1) { /* 売切れなら断る */ } db.prepare("UPDATE items SET sold = 1 WHERE id = ?").run(id); db.prepare("INSERT INTO purchases ...").run(id, buyer);

一見すると正しく見えます。確認してから更新しているからです。

問題はその隙間です。確認と更新のあいだに、別の購入が割り込めます。

プレーンテキスト

松田さん 確認(まだ売れてない) ────→ 更新 陽葵さん 確認(まだ売れてない) ────→ 更新

2人とも「まだ売れていない」を見て、2人とも更新します。湯呑みは1つしかないのに、購入記録は2件残ります。

直し方は2つを組み合わせます。

1つ目は、確認と更新を不可分にすることです。BEGIN から COMMIT までをひとまとまりとして扱い、途中で失敗したら ROLLBACK で全部戻します。「売切れにはなったが購入記録が無い」という中途半端な状態を残しません。

2つ目は、更新の条件で確かめることです。

SQL クエリ

UPDATE items SET sold = 1 WHERE id = ? AND sold = 0

AND sold = 0 があると、すでに売れている行は更新されません。実際に何行変わったかは changes で分かります。0 なら誰かに先を越されたということです。

条件を WHERE に持ち込むと、確認と更新が1つの操作になります。隙間がなくなります。

すでに売れていたときは 409 です。商品は存在するので 404 ではなく、依頼の書き方も正しいので 400 でもありません。いまの状態と矛盾するという失敗です。

完成条件

POST /items/:id/purchase の競合を直してください。

購入できたときは 200 で {"id":1,"sold":1,"purchases":1} を返します。すでに売れていたときは 409 で すでに売り切れです、商品が無ければ 404 です。

同じ商品を2回買おうとしたら、2回目は必ず 409 になるようにしてください。

要件

  1. UPDATE items SET sold = 1 WHERE id = ? AND sold = 0 の形で、条件を更新に持ち込む
  2. changes が 0 なら 409 で すでに売り切れです を返す
  3. 売切れ化と購入記録を BEGIN から COMMIT までで不可分にする

入出力例

request("POST", "/items/1/purchase", {"buyCount":1}) → "200 {"id":1,"sold":1,"purchases":1}" request("POST", "/items/1/purchase", {"buyCount":2}) → "409 {"error":"すでに売り切れです"}" request("POST", "/items/2/purchase", {"buyCount":3}) → "409 {"error":"すでに売り切れです"}" request("POST", "/items/999/purchase", {"buyCount":1}) → "404 {"error":"商品が見つかりません"}"

ヒント

前のヒントを開くと次が開きます。開かずに解けると未開封クリアです

初期コードは確認と更新が別々なので、2回目も通ってしまいます

ヒント 2ヒント 1 を開くと読めます
ヒント 3ヒント 2 を開くと読めます
生田 陸人
監修生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア監修者プロフィールを見る →
編集 ゆめさく編集部·公開 2026/08/10

関連レッスン

  • #110 新機能「いいね」を設計から

    付ける・外す・件数取得の3エンドポイントを、設計から任されます。

  • #108 他人の出品を値下げできてしまう

    出品者だけが値下げできるよう、401と403を使い分けて守ります。

  • #107 エラーの形式がバラバラ

    バラバラだったエラー応答を、共通ハンドラで1つの形に統一します。

  • #106 調査用のアクセスログを

    全リクエストのメソッドとパスを記録するミドルウェアを自作します。

main.js
学習モード
エディタを読み込んでいます

メモ

#109 同じ商品が2人に売れた

⌘S で保存