2
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?

家族のレシピアプリ - コツを忘れないための Firestore 設計(基本機能編 2)

2
Last updated at Posted at 2026-10-15

わが家の脱・料理属人化プロジェクト、基本機能編の 2 回目です。

前回はアプリの全体像を説明したので、今回からは導入編で書いた困りごとを、機能ごとに解決していきます。

最初に取り上げるのは、コツやアレンジが片方の頭の中にしかなかったり、忘れてしまったりする、という困りごとです。
そのノウハウをどう残しているかを書きます。

困っていたこと

交代で料理をしていると、片方しか知らないコツやアレンジがどうしても出てきます。
自分のコツやアレンジでも、時間が経ってレパートリーが増えてくると、忘れたりあいまいになったりしていました。

アプリで記録をつけるまでは、料理を食べる直前に「そういえば〇〇入れた?」「あ、忘れてた」というやり取りがよくありました。

3 つの置き場所

ノウハウの置き場所を、性質ごとに 3 つ用意しました。

置き場所 何を残すか 例
メモ レシピごとのコツ・アレンジ(家族で 1 つを共有) 「あんかけくらいとろみをつける」
作った記録 いつ・誰が作ったか、ひとこと 「9/1 私:もも肉 → むね肉で代用」
変更履歴 いつ・誰が・どこを変えたか 「2026/10/05 A さんが材料・手順を更新しました」

このアプリには権限の仕組みがなく、家族なら誰でもどのレシピでも編集できます。
そのかわり、変更履歴で「誰が変えたか」は分かるようにしました。
権限で縛るのではなく、記録を残すという考え方です。

変更前後の比較や「元に戻す」は作っていません。
家族なので、気になったら本人に聞けば済みます。

2 人が同時に編集したら、片方の変更が消える

最初に気になったのは同時編集でした。

妻が材料を直している間に、私がメモを書き足したとします。
編集画面の内容をまるごと保存していると、後から保存した私の画面にある古い材料で上書きされ、妻の修正が消えてしまいます。

そこで、編集の前後を比べて、変わった項目だけを書き込むようにしています。

/**
 * 編集で変わった項目だけを取り出す。何も変わっていなければ空のオブジェクト。
 * 編集では変わった項目だけを書き込み、2 人が別々の項目を同時に編集しても両方が残るようにする
 */
export function diffRecipeContent(before: RecipeContent, after: RecipeContent): Partial<RecipeContent> {
  const changed: Partial<Record<RecipeContentField, unknown>> = {};
  for (const field of CONTENT_FIELDS) {
    if (!isEqual(before[field], after[field])) {
      changed[field] = after[field];
    }
  }
  return changed as Partial<RecipeContent>;
}

isEqual はよくある比較関数です。
材料や手順などの配列も中身で比べます。

/** JSON で表せる値が同じか(オブジェクトのキーの順は問わない) */
function isEqual(a: unknown, b: unknown): boolean {
  if (a === b) {
    return true;
  }
  if (Array.isArray(a) && Array.isArray(b)) {
    return a.length === b.length && a.every((item, index) => isEqual(item, b[index]));
  }
  if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
    const aRecord = a as Record<string, unknown>;
    const bRecord = b as Record<string, unknown>;
    const keys = Object.keys(aRecord);
    return keys.length === Object.keys(bRecord).length && keys.every((key) => key in bRecord && isEqual(aRecord[key], bRecord[key]));
  }
  return false;
}

Firestore の update は渡した項目しか書き換えないので、変わった項目だけを渡せば、別々の項目を同時に編集しても両方が残ります。
同じ項目を同時に編集したときは後から保存したほうが残りますが、家族だけで使うアプリなので警告は出していません。

何も変わっていなければ、そもそも書き込みません。
保存ボタンを押すたびに、中身のない変更履歴が積み上がっても困るからです。

変更履歴は、同じバッチで 1 件だけ追記する

変更履歴は、レシピの下のサブコレクション history に、保存 1 回につき 1 件ずつ追加しています。
レシピの更新と同じバッチで書くので、レシピは変わったのに履歴がない、ということは起きません。

/** 内容を編集する。changes には変わった項目だけを入れる(diffRecipeContent)。同じバッチで変更履歴を 1 件追加する */
export async function updateRecipe(hid: string, recipeId: string, changes: Partial<RecipeContent>, author: Author): Promise<void> {
  const batch = writeBatch(getFirestore());
  batch.update(recipeRef(hid, recipeId), {
    ...recipeContentSchema.partial().parse(changes),
    updatedAt: serverTimestamp(),
    updatedBy: author.uid,
    updatedByName: author.displayName,
    changedAt: serverTimestamp(),
  });
  batch.set(doc(recipeHistoryCollection(hid, recipeId)), historyEntry(author, { action: 'updated', /* (一部を省略) */ parts: historyPartsOf(changes) }));
  await batch.commit();
}

/** 編集で変わった項目(diffRecipeContent の結果)から、変わった部分を表示の順に求める */
export function historyPartsOf(changes: object): HistoryPart[] {
  const parts = new Set(Object.keys(changes).flatMap((field) => PART_OF_FIELD[field] ?? []));
  return HISTORY_PARTS.filter((part) => parts.has(part));
}

気をつけたのは次の 3 点です。

  • 書き込む前に zod(recipeContentSchema)で型を確かめる
    → Security Rules では型を確かめないため、ここで不正なデータの流入を防ぐ
  • changedAt は、どの書き込みでもサーバーの時刻にする
    → 変わったレシピだけを取り直す差分同期の目印にする(オフライン同期の回で紹介します)
  • どのセクション(材料、手順、メモなど)を変更したかだけを履歴に残す
    → 履歴は期限なく残すため、変更前後の内容まで残すと無料プランの容量を圧迫する

履歴は Security Rules で追記専用にして、改ざんを防ぐ

変更履歴は追加だけができ、編集も削除もできません。
Firestore の Security Rules で次のように決めています。

/** 変更履歴。レシピの追加・内容の編集と同じバッチで追加する。追加のみで、編集・削除はできない */
match /history/{historyId} {
  allow read: if isMember(hid);
  allow create: if isMember(hid)
    && request.resource.data.by == request.auth.uid
    && request.resource.data.at == request.time;
  allow update, delete: if false;
}

確かめているのは次の 3 つだけです。

  • 書いた人(by)は本人でなければならない
  • 日時(at)はサーバーの時刻でなければならない
  • 編集・削除はできない

作った記録がわが家の人気ランキングになる

作った記録には、日付とひとこと、作った人を残します。
レシピの詳細画面のボタンから記録できるほか、料理モード(後の回で紹介します)でタブを閉じるときにも記録できます。

記録と同じバッチでレシピの「作った回数」と「最後に作った日」も更新しているので、レシピ一覧を「よく作る順」「最近作った順」で並べられます。
よく作る順は、そのままわが家の人気ランキングです。

まとめ

ノウハウは、メモ・作った記録・変更履歴の 3 か所に残すようにしました。
編集では変わった項目だけを書き込みます。
変更履歴は同じバッチで追記し、Security Rules で追記専用にしています。

家族みんなで同じレシピを直していくアプリには、権限で縛るより記録を残すほうが合っていると考えています。

次回は、レシピの入力を楽にする話を予定しています。

2
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
2
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?