メインコンテンツまでスキップ

第5章:サブコレの落とし穴(削除・移動・集計)💥🧩🗑️

サブコレ(例:posts/{postId}/comments/{commentId})って、設計が気持ちよく整理できて最高なんだけど… 運用フェーズで 「え、そうなの!?」 となりがちな地雷が3つあります👇💣

  1. 削除がややこしい(親を消しても子が残る)
  2. 移動がめんどい(コピー→切替→削除、が基本)
  3. 集計がズレやすい(件数/いいね等が「削除とセット」で崩れる)

この章は、それを 先に踏んでおく章です😄✨


1) 読む📚:サブコレの「落とし穴3兄弟」👨‍👩‍👧‍👦💥

落とし穴①:親ドキュメントを消しても、サブコレは消えない😇

Orphan Documents on Parent Delete

アプリのコードで deleteDoc(postRef) しても、サブコレのドキュメントは自動で消えません。 つまり「記事は消えたのにコメントだけ残る(孤児化)」が起きます。(Google Cloud Documentation)

ただし、コンソール上での削除は “ネストしたデータ(サブコレ等)も含めて削除” になる挙動が案内されています。 「コンソールでは消えたのに、コードだと残る」って混乱しやすいポイントなので、ここは分けて覚えるのが大事です🧠⚡(Firebase)


落とし穴②:削除は「一撃で全部」にならない(途中で止まることもある)🫠

Recursive Delete Failure

サブコレ含む “ツリー削除” は、裏では大量のドキュメントを順番に消す処理になりがちで、途中で失敗して半分だけ消えることもあります。(Google Cloud Documentation)

だから設計段階で👇を決めておくと、未来の自分が助かります🫶

  • 「ユーザー操作の削除」は 論理削除(ゴミ箱) にする?🗑️
  • 「管理者操作だけ」物理削除(完全消去)を許す?👮‍♂️
  • 監査ログ(誰が何を消した)を残す?🧾

落とし穴③:移動(=構造変更)は“コピー&切替”が基本🚚📦

Data Migration Strategy

Firestoreの構造を後で変えたくなったら、基本は👇 新しい場所へコピー → 読み取りを並行運用 → 切替 → 古い方を削除。(Firebase)

「コレクション名を変更」とか「ここからここへ移動」を一発でやるイメージは持たない方が安全です😄


2) 手を動かす🛠️:わざと“孤児コメント”を作って、対策を決める😈➡️😇

ここからは「記事 / コメント」でいきます📝✨

Step A:まず“事故”を体験する💥

  1. posts/{postId} を1件作る
  2. posts/{postId}/comments にコメントを2〜3件作る
  3. アプリコードで「記事だけ削除」を実行(deleteDoc
  4. コンソールで コメントが残ってないか を見てみる👀

この「残ってるじゃん!」が落とし穴①です🧨(Google Cloud Documentation)


Step B:削除方針を3択から選ぶ🎛️🗑️

方針1:論理削除(ゴミ箱)🗑️(いちばん事故りにくい)

Logical Deletion Flag

記事を物理削除せず、isDeleted: truedeletedAt を付けて隠します。 コメントも同じ方針にしておくと、親が消えた/残ったに振り回されにくいです😄

import { doc, updateDoc, serverTimestamp } from "firebase/firestore";
import { db } from "./firebase";

// 記事を「ゴミ箱へ」🗑️(物理削除しない)
export async function trashPost(postId: string) {
await updateDoc(doc(db, "posts", postId), {
isDeleted: true,
deletedAt: serverTimestamp(),
});
}

ポイント💡

  • 一覧クエリは where("isDeleted", "==", false) を基本にする
  • “復元”もできる(isDeleted=false)✨

方針2:論理削除+TTLで「○日後に自動で完全削除」⏳🧹

ゴミ箱に入れた記事に expireAt を入れて、TTLポリシーで自動削除させます。 TTLは「その時刻を過ぎたら即消える」じゃなく、だいたい24時間以内に削除みたいな動きです⏱️(Firebase)

超重要⚠️ TTLで親ドキュメントが消えても、サブコレが自動で消えるわけではありません。 つまり「TTLで記事だけ消える → コメントが孤児化」は普通に起こりえます。(Firebase)

なので実務では👇どれかにします👍

  • comments にもTTL(expireAt)を付けて同じく掃除🧹
  • あるいは後述の「サーバー側の再帰削除」でツリーごと消す👮‍♂️

方針3:物理削除(ツリーごと完全消去)🔥

「管理者だけが実行できる」前提で、ツリー削除を用意するのが王道です👮‍♂️

公式の代表例として、Callable Functionsから Firebase CLI の firestore:delete を使って再帰削除する解決策が紹介されています。(Google Cloud Documentation) このやり方だと、指定パス配下を探索して消すので、状況によっては「孤児ドキュメントを見つけて削除」もありえます。(Google Cloud Documentation)

ただし⚠️

(この章では「そういう設計が必要」まで理解できればOK👌 実装は第20章寄りの話です⚙️)


3) 集計の落とし穴📊:コメント数がズレる“あるある”😵

サブコレでコメントを持つと、コメント数はだいたい欲しくなりますよね?😄

典型パターンA:postscommentCount を持つ(速いけどズレやすい)⚡

  • コメント追加→ commentCount+1
  • コメント削除→ commentCount-1
  • でも…「論理削除」「TTL」「途中で削除失敗」などが混ざると、ズレやすい😇

典型パターンB:Aggregation queryでその場カウント(ズレないけど都度コスト)🔎

Firestoreには count/sum/avg の集計クエリがあります。(Firebase) 論理削除してるなら「生きてるコメントだけ数える」みたいに設計できて、ズレにくいです👍

例(コメントに postId を持たせて、コレクショングループで数える案)👇

import { collectionGroup, getCountFromServer, query, where } from "firebase/firestore";
import { db } from "./firebase";

export async function countAliveComments(postId: string) {
const q = query(
collectionGroup(db, "comments"),
where("postId", "==", postId),
where("isDeleted", "==", false),
);
const snap = await getCountFromServer(q);
return snap.data().count;
}

4) ミニ課題🎯:記事削除時、コメントをどう扱う?(設計を文章で決める)📝✨

次の3つを、あなたのアプリ方針として決めてみてください👇😄

  1. 記事削除は

    • A. ゴミ箱(論理削除)🗑️
    • B. 即完全削除🔥
    • C. ゴミ箱→30日後に完全削除⏳
  2. コメントは

    • A. 記事と同じ方針にする
    • B. コメントだけ先に消す(or 先に隠す)
    • C. “監査用”に残す(表示はしない)
  3. 集計(コメント数)は

    • A. commentCount を持つ(サーバー側で更新)
    • B. その場カウント(Aggregation)
    • C. 両方(普段はキャッシュ、ズレ修正は集計で)

書けたら勝ちです🏆✨


5) チェック✅(この章のゴール達成ライン)

  • 親を消してもサブコレは消えない、を腹で理解した😄(Google Cloud Documentation)
  • TTLは便利だけど「即時じゃない」「サブコレは消さない」を理解した⏳(Firebase)
  • 構造変更(移動)は「コピー→並行→切替→削除」が基本だと分かった🚚(Firebase)
  • 削除と集計はセットで設計する、と思えるようになった📊

6) AIブースト🤖⚡(Antigravity / Gemini CLI で一気に固める)

Antigravityの使いどころ🧠🛰️

Antigravityは「エージェントが計画→実行→検証まで回す」思想の開発環境で、ブラウズや安全設定(ポリシー)も含めて扱えます。(Google Codelabs) この章だと「削除方針の比較表」「失敗ケース洗い出し」「実装の危険箇所レビュー」に強いです💪

Gemini CLIの使いどころ⌨️🤖

Gemini CLIはターミナル中心で、最近は拡張の入れ方や設定がわかりやすく案内されています(例:v0.28.0+ の拡張設定ファイル)。

コピペ依頼例📎

  • posts/{postId}/comments/{commentId} の設計で、削除方針を3案(論理削除/TTL/再帰削除)比較して。メリデメ、事故パターン、集計への影響も書いて」
  • 「論理削除にした時のクエリ一覧と必要なインデックス候補(第2キー含む)を出して」
  • 「“途中で削除が止まる”前提で、UIに出すべき状態(削除中/失敗/再試行)を提案して」

Firebase AI Logic もここに直結するよ🔐🤖

生成AIを絡めると「リクエスト乱発」「コスト」「不正アクセス」が現実問題になります😇 Firebase AI Logicは ユーザー単位のレート制限(デフォルト例:100 RPM) があり、調整も可能です。(Firebase) さらに App Check 連携で、正規アプリ以外からの呼び出しを弾く設計にもできます。(Firebase)


次の章(第6章:正規化 vs 非正規化⚖️)に入ると、「表示のための複製」と「削除時の整合性」がガッツリ繋がってきて、めちゃくちゃ面白くなりますよ😄🔥