はじめに
ブログやCMSで「指定した日時に記事を自動公開する」予約投稿機能を実装する際、データベース設計にはいくつかのアプローチがあります。
本記事では、各設計パターンのトレードオフを比較し、バッチ処理不要でシンプルなクエリ時評価型を推奨する理由を解説します。
要件の整理
予約投稿機能では、以下の状態を管理します。
| 状態 | 説明 | ユーザーへの表示 |
|---|---|---|
| 下書き | 編集中 | 非公開 |
| 予約中 | 公開日時を設定済み | 非公開 |
| 公開済み | 公開日時を過ぎた | 公開 |
設計パターンの比較
パターン1: バッチ処理型(従来型)
cronやCloud Functionsで定期的にステータスを更新する方式です。
-- テーブル設計
CREATE TABLE articles (
id UUID PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft, scheduled, published
scheduled_at TIMESTAMPTZ,
published_at TIMESTAMPTZ
);
-- バッチ処理(1分ごとに実行)
UPDATE articles
SET status = 'published', published_at = NOW()
WHERE status = 'scheduled' AND scheduled_at <= NOW();メリット
- ステータスが明示的で直感的
- クエリがシンプル(
WHERE status = 'published')
デメリット
- バッチ処理のインフラが必要(cron、pg_cron、Cloud Functions等)
- 公開タイミングがバッチ間隔に依存(1〜5分の遅延)
- バッチ障害時に公開されない
パターン2: クエリ時評価型(推奨)
バッチ処理を使わず、クエリ実行時に公開判定を行う方式です。
-- テーブル設計
CREATE TABLE articles (
id UUID PRIMARY KEY,
is_draft BOOLEAN NOT NULL DEFAULT TRUE,
scheduled_at TIMESTAMPTZ
);
-- 公開記事の取得(クエリ時に判定)
SELECT * FROM articles
WHERE is_draft = FALSE AND scheduled_at <= NOW();メリット
- バッチ処理が不要(インフラがシンプル)
- 公開タイミングが秒単位で正確
- 障害ポイントが少ない
デメリット
- クエリに公開判定ロジックが必要(ビューで解決可能)
- DBの状態と表示上の状態が異なる
- ステータス変更イベントが発生しない(後述)
なぜクエリ時評価型を選ぶのか
本質的な問い:「公開済み」は状態か、それとも条件か?
バッチ処理型では「公開済み」をDBに保存された状態として扱います。一方、クエリ時評価型では「公開済み」を scheduled_at <= NOW() という条件として扱います。
予約投稿において「公開済み」は時間経過によって自動的に決まる性質のため、条件として評価する方が自然です。
技術的根拠
この設計パターンは、PostgreSQLの以下の公式機能と同じ思想に基づいています。
| 機能 | 評価タイミング | 説明 |
|---|---|---|
| View | クエリ時 | NOW() が参照のたびに評価される |
| Virtual Generated Columns | クエリ時 | PostgreSQL 18+で読み取り時に計算 |
| Row Level Security | クエリ時 | ポリシーが毎クエリ評価される |
また、Soft Delete(論理削除)パターンと同じ設計思想です。
// Soft Delete: deletedAt で判定
where: { deletedAt: null }
// 公開予約: scheduled_at で判定
where: { isDraft: false, scheduledAt: { lte: new Date() } }推奨設計の実装
テーブル定義
CREATE TABLE articles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
body TEXT NOT NULL,
-- 状態管理
is_draft BOOLEAN NOT NULL DEFAULT TRUE,
scheduled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 制約: 公開状態では scheduled_at が必須
CONSTRAINT chk_published CHECK (
is_draft = TRUE OR scheduled_at IS NOT NULL
)
);
-- 公開記事の検索用インデックス
CREATE INDEX idx_articles_published ON articles(scheduled_at DESC)
WHERE is_draft = FALSE;ビューで公開判定を隠蔽
CREATE VIEW published_articles AS
SELECT *, scheduled_at AS published_at
FROM articles
WHERE is_draft = FALSE AND scheduled_at <= NOW()
ORDER BY scheduled_at DESC;アプリケーションは SELECT * FROM published_articles で公開記事を取得できます。ビューにより、公開判定ロジックがアプリケーションコードに散らばることを防ぎます。
基本操作
-- 下書き作成
INSERT INTO articles (author_id, title, slug, body)
VALUES (?, ?, ?, ?);
-- 即時公開
UPDATE articles SET is_draft = FALSE, scheduled_at = NOW()
WHERE id = ?;
-- 予約公開(未来日時を設定)
UPDATE articles SET is_draft = FALSE, scheduled_at = '2024-12-31 10:00:00+09'
WHERE id = ?;
-- 下書きに戻す
UPDATE articles SET is_draft = TRUE, scheduled_at = NULL
WHERE id = ?;設計パターン比較表
| 観点 | バッチ処理型 | クエリ時評価型 |
|---|---|---|
| バッチ処理 | 必要 | 不要 |
| 公開タイミング | バッチ間隔依存 | 秒単位で正確 |
| インフラ複雑度 | 高い | 低い |
| 障害耐性 | バッチ障害リスク | 高い |
| クエリの複雑さ | シンプル | ビューで解決 |
| Webhookトリガー | 可能 | 不可 |
アーキテクチャ別の注意点
SSG/ISR を使用する場合
Next.js などで SSG(静的サイト生成) や ISR(Incremental Static Regeneration) を使用している場合、クエリ時評価型には注意が必要です。
クエリ時評価型では DB のステータスが変更されないため、Webhook 等で再ビルドをトリガーできません。予約時刻が来ても、次回の再ビルドまたは Revalidation が走るまでサイトに反映されません。
| アーキテクチャ | クエリ時評価型との相性 | 対策 |
|---|---|---|
| SSR / 動的レンダリング | ◎ 最適 | そのまま使用可能 |
| ISR | ○ 条件付き | revalidate を短く設定(例: 60秒) |
| SSG | △ 要検討 | 定期ビルドの設定、またはバッチ処理型を検討 |
// Next.js ISR: revalidate を短く設定
export const revalidate = 60; // 60秒ごとに再検証
export default async function ArticlePage() {
const articles = await getPublishedArticles();
// ...
}SSG で厳密な公開タイミングが必要な場合は、バッチ処理型を採用し、ステータス変更時に Webhook で再ビルドをトリガーする方式が適しています。
まとめ
公開予約機能のテーブル設計には、クエリ時評価型を推奨します。
- 状態管理:
is_draft(BOOLEAN) +scheduled_at(TIMESTAMPTZ) - 公開判定:
is_draft = FALSE AND scheduled_at <= NOW() - バッチ処理: 不要
- ビュー: 公開判定ロジックを隠蔽
- 適したアーキテクチャ: SSR、ISR(短い revalidate 設定)
「公開済み」を状態ではなく条件として捉えることで、バッチ処理が不要になり、シンプルで堅牢な設計が実現できます。
ただし、SSG で厳密な公開タイミングが求められる場合は、Webhook でビルドをトリガーできるバッチ処理型も検討してください。