MySQL | SQLite経験者向け、30日で習得するMySQL:実務SQL力 - Day15 中間課題

SQL MySQL
スポンサーリンク

Day15 前半のゴール

「ここまで学んだ要素を“注文処理システム”として頭の中で組み立てられるようにする」

Day15 は中間課題です。
テーマは「注文処理システムを実装」です。

ここまでで、あなたはすでにかなりの武器を持っています。

ストアドプロシージャ
トリガー
トランザクション
JOIN・サブクエリ・ビュー

Day15 前半のゴールは、

注文処理システムに必要なテーブル構造をイメージできる
「注文を確定する」ときに何が起きるかを分解できる
どこでトランザクションを張るべきか、感覚的に分かる

ここまで行けば、後半で実際のSQL・ストアドプロシージャを書いていくときに、迷子になりにくくなります。


まずは“最低限の注文システム”を言葉で描く

「何が起きたら“注文が1件できた”と言えるのか」

いきなりSQLに行く前に、「注文処理」を日本語で分解してみます。

ユーザーが商品を選ぶ
カートに入れる
注文ボタンを押す
注文が確定する
在庫が減る

この中で、DBが本気を出すのは「注文が確定する」タイミングです。

注文確定の瞬間に、DB側ではだいたいこんなことが起きます。

注文のヘッダ情報を保存する(誰が、いつ、合計いくら)
注文の明細行を保存する(どの商品を何個)
在庫を減らす
必要ならログや履歴を残す

この「一連の処理」が、Day14 までにやってきた

トランザクション
ストアドプロシージャ
トリガー

の出番です。


注文処理に必要なテーブルを整理する

「“ヘッダ”と“明細”を分けて考える」

最低限の注文システムで必要になるテーブルを、ざっくり決めてしまいましょう。

ユーザーを表すテーブル
商品を表すテーブル
注文のヘッダ(1件の注文そのもの)
注文の明細(注文の中の1行1行)

これをSQLで書くと、例えばこうなります。

CREATE TABLE users (
  id    INT AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(50) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE products (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  name      VARCHAR(100) NOT NULL,
  price     INT          NOT NULL,
  stock_qty INT          NOT NULL
) ENGINE=InnoDB;

CREATE TABLE orders (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  user_id    INT          NOT NULL,
  total      INT          NOT NULL,
  created_at DATETIME     NOT NULL,
  CONSTRAINT fk_orders_user
    FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;

CREATE TABLE order_items (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  order_id   INT          NOT NULL,
  product_id INT          NOT NULL,
  quantity   INT          NOT NULL,
  unit_price INT          NOT NULL,
  CONSTRAINT fk_items_order
    FOREIGN KEY (order_id) REFERENCES orders(id),
  CONSTRAINT fk_items_product
    FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;
SQL

ここで大事なのは、「ヘッダ」と「明細」を分けていることです。

orders
1件の注文そのもの(誰が・いつ・合計いくら)

order_items
その注文の中身(どの商品を何個・単価はいくら)

この構造は、実務でもほぼそのまま使われる“定番パターン”です。


「注文確定」の流れをSQL目線で分解する

「どの順番で、どのテーブルに、何を入れるか」

さっきのテーブルを前提に、「注文確定」をSQLの流れで考えてみます。

ユーザーがカートの中身を確定して「注文する」を押した瞬間、DB側ではこう動きます。

orders に1行INSERTする
その注文ID(AUTO_INCREMENTで採番された値)を取得する
order_items に、カートの中身の件数分INSERTする
products の在庫を、注文された数量分だけ減らす

これを、もう少し具体的なSQLに落としてみます。

START TRANSACTION;

INSERT INTO orders (user_id, total, created_at)
VALUES (1, 3000, NOW());

SET @order_id = LAST_INSERT_ID();

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
  (@order_id, 1, 2, 1000),
  (@order_id, 2, 1, 1000);

UPDATE products
SET stock_qty = stock_qty - 2
WHERE id = 1;

UPDATE products
SET stock_qty = stock_qty - 1
WHERE id = 2;

COMMIT;
SQL

ここで重要なのは2つです。

orders → order_items → products の順に処理している
全体を START TRANSACTION〜COMMIT で囲んでいる

つまり、

「注文ヘッダを作る」
「明細を作る」
「在庫を減らす」

という3ステップを、1つのトランザクションとして扱っている、ということです。


なぜトランザクションが必須なのか

「“注文だけあるのに在庫が減っていない”を絶対に避けたい」

もし、上の処理をトランザクションなしでバラバラに実行したらどうなるかを想像してみてください。

orders へのINSERTは成功
order_items へのINSERTも途中まで成功
products のUPDATEでエラー(在庫不足やロックなど)

このとき、

注文ヘッダと明細は存在する
でも在庫は減っていない

という「中途半端な状態」がDBに残ります。

これは、実務的にはかなり危険です。

在庫管理が狂う
後から「この注文は本当に有効なのか?」が分からなくなる
不正利用の余地が生まれる

だからこそ、「注文確定」のような処理では、

BEGIN(START TRANSACTION)
全部のSQLが成功したら COMMIT
どこかで失敗したら ROLLBACK

という形が必須になります。

Day14 でやったトランザクションの話が、ここで一気に“生きた話”になります。


在庫チェックをどこでやるか

「“在庫が足りない注文は通さない”をDB側で保証するかどうか」

もう一歩だけ踏み込みます。

注文処理では、「在庫が足りないのに注文を通してしまう」ことも避けたいですよね。

在庫チェックのタイミングは、ざっくり2つあります。

アプリ側で、注文ボタンを押す前に在庫を確認する
DB側で、トランザクションの中で在庫を確認する

実務では両方やることも多いですが、
「最終的な保証」としてはDB側のチェックが強いです。

例えば、トランザクションの中でこう書くイメージです。

START TRANSACTION;

SELECT stock_qty
INTO @stock
FROM products
WHERE id = 1
FOR UPDATE;

IF @stock < 2 THEN
  -- 在庫不足なので ROLLBACK して終了
  ROLLBACK;
  -- アプリ側には「在庫不足」のエラーを返す
ELSE
  -- 在庫が足りているので注文処理を続行
  UPDATE products
  SET stock_qty = stock_qty - 2
  WHERE id = 1;

  -- orders / order_items のINSERTもここで行う

  COMMIT;
END IF;
SQL

ここでのポイントは、

在庫行を FOR UPDATE でロックしてからチェックしている
在庫が足りなければ、その場で ROLLBACK して注文を通さない

という2点です。

Day15 前半では、
ここまで細かく書けなくても大丈夫ですが、

「在庫チェックもトランザクションの中に入れて、DB側で最終的な“OK/NG”を決める」

という発想だけ持っておいてください。


中間課題としての“設計の問い”

「あなたなら、どこまでDB側に寄せるか?」

Day15 は「中間課題」なので、
単にSQLを書くだけでなく、設計の問いも含めたいです。

例えば、こんな問いを自分に投げてみてください。

注文処理はストアドプロシージャにまとめるか?
create_order のようなプロシージャを作るか)

在庫チェックはDB側でやるか、アプリ側だけに任せるか?

注文確定時に、トリガーで監査ログを残すか?

これに「絶対の正解」はありません。
システムの規模、チームのスキル、運用方針によって変わります。

でも、Day1〜Day14で学んだ要素を思い出しながら、

ここはストアドに寄せた方が一貫性を守りやすそう
ここはトリガーで最低限のログだけDB側に任せたい
ここはアプリ側に置いておいた方が読みやすい

と、自分なりの理由を持って選べるようになることが、
「実務SQL力」の中間地点としてすごく大事です。


Day15 前半のまとめ

注文処理システムの最小構成としては、ユーザー・商品・注文ヘッダ(orders)・注文明細(order_items)・在庫(products)といったテーブルを用意し、「1件の注文」は orders に1行、「その中身」は order_items に複数行という“ヘッダ+明細”構造で表現するのが定番パターンになる。
「注文確定」の瞬間にDB側で起きることは、「orders にINSERT」「採番された order_id を使って order_items に複数行INSERT」「products の在庫を減らす」といった複数の更新であり、これらを START TRANSACTIONCOMMIT でひとかたまりにし、どこかで失敗したら ROLLBACK で全部なかったことにすることで、「注文だけあるのに在庫が減っていない」といった中途半端な状態を防ぐことが、実務的に必須になる。
さらに一歩進めると、「在庫が足りない注文は通さない」というルールをDB側で保証するために、トランザクションの中で SELECT ... FOR UPDATE で在庫行をロックしてから数量をチェックし、足りなければその場で ROLLBACK する、といった設計も見えてきて、「ストアドプロシージャに注文処理をまとめるか」「トリガーで監査ログを残すか」といった“どこまでDB側に寄せるか”を自分で考え始めることが、この中間課題の一番おいしいところになる。

後半では、
実際に create_order 的なストアドプロシージャを1本書き上げるイメージで、
トランザクション・在庫チェック・エラー時のROLLBACK・ログ記録までを
具体的なSQLとして組み立てていきます。

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