1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Supabase】UNIQUE制約は「壁」、エラーハンドリングは「壁にぶつかった人への張り紙」 — セットで初めて完成する話

1
Posted at

はじめに

LINE連携アプリで「同じLINEアカウントが複数人に紐付いてしまう」バグを
直す過程で、DB制約とアプリ層のエラーハンドリングの関係を理解しました。

「DBに UNIQUE 制約を貼ったから安心」と思いがちですが、それだけだとユーザー体験は最悪のままです。

結論

やる側 役割
DBの UNIQUE 制約 重複データを物理的に防ぐ警備員
アプリのエラーハンドリング 警備員が止めた時の説明メッセージを整える張り紙

両方揃って初めて「重複登録できないし、なぜダメかユーザーにも伝わる」状態になる。

起きていたこと

LINE連携アプリで、こういうバグが起きていました。

登録ボタン押す
  ↓
DB「ダメ!(UNIQUE違反)」
  ↓
画面「Internal Server Error」

ユーザー視点では「なんで失敗したのか全然わからん」状態。
お店に「登録できない」とクレームが来るレベル。

修正① DBに UNIQUE 制約を貼る(Supabase migration)

girls.line_user_id カラムに UNIQUE 制約を追加。
ただし NULL(未紐付け)は何行あっても OK にしたいので、
Postgres の 部分インデックスを使う:

CREATE UNIQUE INDEX IF NOT EXISTS girls_line_user_id_unique
  ON girls (line_user_id)
  WHERE line_user_id IS NOT NULL;

意味

line_user_id が NULL じゃない行だけを対象に、UNIQUE を保証する」

これで

  • NULL は何行あっても OK(紐付け前の行は複数 OK)
  • NULL 以外は重複 NG(同じ LINE を 2 人に紐付けるのを拒否)

修正② アプリ層でエラーコードをハンドリング

Before(Internal Server Error が返る)

const { data, error } = await supabaseAdmin
  .from("girls")
  .update({ line_user_id: userId })
  .eq("id", girlId)
  .is("line_user_id", null)
  .select();

if (error) {
  return new Response(JSON.stringify({ error: error.message }), {
    status: 500,
  });
}

これだと UNIQUE 違反でもただの 500 エラーが返るだけ。ユーザーには「サーバーが死んでます」みたいなメッセージしか見えない。

After(UNIQUE違反だけ409で返す)

if (error) {
  // 23505 = PostgresのUNIQUE違反
  if (error.code === "23505") {
    return new Response(
      JSON.stringify({
        error: "このLINEアカウントは既に別の女の子で登録されています。お店に確認してください。",
      }),
      { status: 409 },
    );
  }
  return new Response(JSON.stringify({ error: error.message }), {
    status: 500,
  });
}

ポイントは error.code === "23505"。これは Postgres が UNIQUE 違反時に必ず付ける世界共通のエラーコード

コード 意味
23505 UNIQUE 違反
23503 外部キー違反
23502 NOT NULL 違反

なので「このエラーコードだけ特別扱い」っていう分岐が安全に書ける。

なぜ片方だけだとダメなのか

DB制約だけある場合

  • 重複は防げる ✅
  • でもユーザーには Internal Server Error しか見えない ❌
  • 「何度も連打する」「お店にクレーム」が起きる

アプリ層のチェックだけある場合

  • 「すでに登録されているか?」を SELECT で先に確認 → なければ INSERT/UPDATE
  • でもチェックと書き込みの間に他のリクエストが入り込む可能性(race condition)
  • DBレベルでガードしないと、結局重複が発生し得る

両方あると

  • DB が物理的にガード(race conditionでも安全)
  • アプリ層がエラーを翻訳して人間が読めるメッセージに変換
  • ユーザー体験 ◎

学び

  • DB制約は「壁」アプリのエラーハンドリングは「壁の前の張り紙」
  • 片方だけだと「動くけど不親切」or「親切だけど穴がある」になる
  • Postgres のエラーコードを覚えとくと、特定のエラーだけハンドリングできて便利
  • 「未連携=NULL」を許容したいなら部分インデックス(WHERE ... IS NOT NULL)が定番

「Supabase で重複防ぎたい」って時、UNIQUE 制約を貼るだけで終わらせず、アプリ側でも error.code を見て適切なメッセージを返すところまでセットでやるのがおすすめです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?