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

はじめに

こんにちは、もんすんです。

自作の個人用トレンドビューアに、新しいタブを新規追加したときの設計の話です。

このアプリ、もともとは「1日1枚のまとめカード」を表示するものでした。
技術トレンド・ビジネストレンド・Xの技術ポスト、それぞれその日の総まとめを1カードとして並べる、日付中心のダッシュボードです。

そこに、別の自動化(URLを要約して溜めるClaude Codeのスキル)の出力を表示する「DIGEST」タブを足したくなりました。

ところが、表示モデルが根本的に違ったので、どうするか迷いました。

日次まとめは「日付ごとに1枚」。
一方 DIGEST は「1URL = 1枚、新しい順にどんどん流れる無限スクロール」。

性質がまるで違う2つを、どう同居させるか。
この記事はその設計判断の記録です。

「1つのDBテーブルに、性質の違うコンテンツを混ぜたい」というのは個人開発でよく出会う場面だと思います。
私の答えは 「同じテーブルに入れるが、流路は分ける」 でした。

対象読者・前提

  • 1つのテーブルに性質の異なるデータを混ぜる設計に悩んでいる人
  • Supabase / Postgres の基礎がある人

ぶつかった問題:2つの表示モデルは水と油

まず、2つのモデルがどれだけ違うかを並べます。

日次まとめ DIGEST
単位 1日 = 1カード 1URL = 1カード
キー (date, category) 1行=1URL(同日に何枚でも)
並び 日付で切り替え created_at 降順で無限ロード
構造 本文に OVERVIEW の要約あり OVERVIEWなし、本文インライン
画面 日付ダッシュボード 専用の縦スクロールページ

日次まとめ → 「カレンダーで日を選ぶ」UI
DIGEST →「Twitterのタイムラインみたいに流す」UI

同じカード描画では成立しません。

素直に考えると「テーブルを分ける」のが正解に見えます。
が、私は テーブルは1つ(posts)のままを選択しました。

設計判断:同じテーブル・別流路

posts テーブルを共用する

DIGEST も同じ posts テーブルに入れます。区別は category カラムだけ。

type Category = 'trend-a' | 'trend-b' | 'trend-c' | 'digest';

DIGEST は category = 'digest'、1行=1URL。
title がカードの見出し、content が要約+元リンク、並びは created_at

テーブルを分けなかった理由は単純で、投入スクリプト・RLS・型・接続を二重に持ちたくなかったからです。

日次の流路から digest を[除外]する

ここが肝です。
テーブルを共用すると、何もしなければ DIGEST のカードが日次ダッシュボードに漏れてきます。これをローダー側で明示的に除外します。

// postLoader.ts
// 日次ダッシュボードに出すのはこの3つだけ。digest は意図的に含めない。
const CATEGORIES: Category[] = ['trend-a', 'trend-b', 'trend-c'];

日次ビューを組み立てる関数は、この CATEGORIES だけを対象にします。
digest はここに入っていないので、どう転んでも日次画面には出てきません

そのうえで、DIGEST 専用の流路を別に用意します。

  • getDigestBatch() … 日付・フリーテキスト検索つきのバッチ取得
  • getDigestDates() … 日付一覧
  • /api/digest … 無限ロード用のエンドポイント
  • /digest … 専用ページ(DigestStream + DigestCard で描画)

つまり、テーブルは共用しつつ、「読み出すクエリ」と「表示するルート・コンポーネント」は完全に分けたわけです。

posts テーブル(器は1つ)
 ├─ a/b/c ──► postLoader(CATEGORIES)──► /date/[date] 日次ダッシュボード
 └─ digest ───────────► getDigestBatch ──────────► /digest 無限ストリーム

落とし穴:同一バッチ投入で並び順が壊れた

...と、設計どおり動かしたら、DIGEST の並びがおかしくなりました。

created_at の降順にしたのに、同じ日のカードの順番がリロードのたびに変わる。

原因は、投入方法にありました。
DIGEST は複数URLを 1回の upsert(バッチ)でまとめて入れます
すると全行が同じトランザクション時刻になり、created_at同値になります。

ORDER BY created_at だけでは、同値の行の相対順序は保証されません(Postgresの仕様)。
だから並びが不定になっていたわけです。

修正は、タイブレーク列を重ねるだけです。

// ❌ 同値だと順序が不定
.order('created_at', { ascending: false })

// ✅ タイブレーク(ソート条件)を足して安定ソートにする
.order('created_at', { ascending: false })
.order('date',       { ascending: false })
.order('title')

取り込み側との整合も忘れずに

テーブルを共用するということは、投入側にも digest を教える必要があるということです。

  • 要約スキルは contents/digest/YYYY-MM-DD_NN.md(frontmatter に category: digest)を生成
  • upload-post.mjs --dir contents/digest で一括投入

ここで一度ハマったのが、投入スクリプト側の「許可カテゴリ一覧」に digest を追加し忘れると、対象外として黙ってスキップされることでした。
器を増やしたら、入口(投入)と出口(表示の除外/専用流路)の両方を更新する、と覚えておくと事故りません。

まとめ:混ぜる、でも分ける

性質の違う2つの表示モデルを、私はこう同居させました。

  • テーブルは共用
    • posts 1つ、区別は category だけ
    • 投入・RLS・型を二重化しない
  • 流路は分離
    • 日次ローダーから digest を除外
    • 専用の getter / API / ルート / コンポーネントを用意
  • 並びはタイブレークで安定化
    • created_at 同値に備えて副キーを重ねる

「テーブルを分けるか、混ぜるか」の二択に見えて、「混ぜるが、読み出しと表示で分ける」 という第三の道がきれいにハマりました。
同じような分岐で悩んでいる方の参考になれば幸いです。

それでは、良いデータモデリングを。

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