← 記事一覧に戻る
テクノロジー

DB制約で不正データを止める設計

DB制約で不正データを止める設計

「バリデーションは書いてあるのに、本番の DB に空の値が入っていた」。原因を追うと、たいていアプリのバリデーションを通らない経路が見つかります。この記事は設計フェーズの話です。アプリ側のバリデーションと DB 制約の責務を、実装に入る前に分けておく方法を紹介します。

なぜバリデーションだけでは足りないのか

アプリケーションのバリデーションが守れるのは「そのコードを通った入力」だけです。次の経路は素通りします。

  • 並行リクエスト: 「無ければ作成」を同時に2本走らせると、両方の存在チェックが通って重複が入ります(check-then-insert の競合)
  • スクリプト・コンソール経由: データ移行や update_allsave(validate: false) はバリデーションを飛ばします
  • 別のアプリ・バッチ: 同じ DB を触る別サービスは、このバリデーションを知りません

責務分担表: アプリと DB で何を持つか

守りたいこと

アプリ側の役割

DB 側の役割

必須項目

入力欄に「必須です」と表示

NOT NULL

一意性

「すでに使われています」と案内

UNIQUE インデックス

値の範囲・種類

選択肢の表示、エラー文言

CHECK 制約

参照の整合性

存在しない選択肢を出さない

外部キー

原則は「アプリ側はユーザーに分かりやすく伝える」「DB は何があっても不正値を入れない」です。最後の砦は DB に置きます。

実例: アプリでは弾いたのに不正値が入った

クーポンは1ユーザー1回まで、という仕様を考えます。アプリで「使用済みか」を確認してから注文を作る実装は、ボタンの連打やリトライで2本のリクエストが同時に届くと、どちらも「未使用」と判定して二重に注文が作られます。

DB 側に制約を置くと、2本目は確実に弾かれます。

CREATE TABLE orders (
  id          BIGINT PRIMARY KEY,
  user_id     BIGINT NOT NULL REFERENCES users(id),
  status      TEXT NOT NULL DEFAULT 'pending'
              CHECK (status IN ('pending','paid','shipped','canceled')),
  quantity    INTEGER NOT NULL CHECK (quantity > 0),
  coupon_code TEXT
);

-- 同じユーザーが同じクーポンを2回使えない(NULL は対象外)
CREATE UNIQUE INDEX uniq_orders_user_coupon
  ON orders (user_id, coupon_code)
  WHERE coupon_code IS NOT NULL;

アプリ側は違反エラーを受け取って、ユーザー向けの文言に変換します。Rails の例です。

def redeem!
  Order.create!(user: user, coupon_code: code)
rescue ActiveRecord::RecordNotUnique
  raise CouponAlreadyUsed, "このクーポンは使用済みです"
end

ポイントは、バリデーションを消すのではなく両方置くことです。バリデーションは親切なエラー表示のため、制約は取りこぼしを防ぐために働きます。

既存テーブルに制約を足す手順

すでに動いているテーブルに制約を足すときは、次の順で進めます。

  1. 違反データを数える: SELECT count(*) FROM orders WHERE quantity <= 0;
  2. データを直す: 補正するのか、削除するのか、業務側に確認して決める
  3. ロックを短くして追加する: PostgreSQL なら NOT VALID で追加してから検証します
  4. エラー処理を先に入れる: 違反が 500 のまま返らないようにする
ALTER TABLE orders
  ADD CONSTRAINT quantity_positive CHECK (quantity > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT quantity_positive;

-- UNIQUE は本番のロックを避けて作る
CREATE UNIQUE INDEX CONCURRENTLY uniq_orders_user_coupon ...;

設計レビューのチェックリスト

  • 「必須」の項目は NOT NULL になっているか(NULL と空文字を区別しているか)
  • 一意であるべき組み合わせに UNIQUE があるか
  • ステータス・種別の列に CHECK か enum があるか
  • 数量・金額・期間に CHECK があるか(0 以上、開始日 <= 終了日など)
  • 制約違反のときにユーザーへ返す文言と HTTP ステータスが決まっているか

Bugoon での実践

不正データ起因のバグは、報告者には「画面がおかしい」としか見えません。Bugoon のウィジェットをサイトに埋め込むと、報告者はスクリーンショットに注釈を書き込むだけで報告でき、操作ステップ、コンソールログ、ネットワークエラーが自動で添付されます。制約違反で API がエラーを返していれば、そのリクエストが証拠として残ります。

報告は GitHub Issue として起票され、カンバンボードで進捗を追えます。MCP サーバー経由で Claude Code や Cursor に渡せば、報告の内容から該当データの調査と、制約を足す修正案の作成まで進められます。不正データを見つけたら「直して終わり」にせず、どの制約があれば防げたかまで確認する習慣をつけると、同じバグの再発を設計で止められます。

チームのバグ報告を、もっとスムーズに。

Bugoon は無料で始められます。サイトにタグを 1 行追加するだけで、QA と開発の往復がなくなります。

無料で始める