
【実装】Next.js + Prismaでチャット履歴を永続化する設計と実装
はじめに
AIキャラクターとチャットできるWebアプリを開発中、ページをリロードすると会話が消えてしまう問題がありました。ユーザー体験としてはリロードしても今日の会話が残っている状態が理想です。さらに、キャラクターごと・日付ごとにセッションを分けて管理したいという要件もありました。
この記事でわかること
-
キャラ × 日付でユニークなチャットセッションのDB設計
-
Prismaスキーマ(Character / ChatSession / ChatMessage)の設計パターン
-
OpenAI APIの会話文脈(previous_response_id)をセッション単位で管理する方法
-
Server Actionsでのセッション取得/作成/メッセージ保存の実装
対象読者
-
Next.js App Router + Prismaの基本がわかる方
-
チャットアプリのDB設計に興味がある方
DBスキーマ設計
3つのモデルを追加しました。ポイントはChatSessionの複合ユニーク制約(userId × characterId × date)で、同じユーザー・同じキャラ・同じ日に1セッションだけ存在する設計です。
Characterモデル
model Character {
id String @id @default(cuid())
slug String @unique // "akari", "haruto"
name String // 表示名
imagePath String // アバター画像パス
promptFile String // プロンプトファイル名
userId String? // null=全員利用可, 有=個人専用
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(...)
chatSessions ChatSession[]
}
ChatSession / ChatMessageモデル
model ChatSession {
id String @id @default(cuid())
userId String
characterId String
date DateTime @db.Date // 日付のみ(時刻なし)
lastResponseId String? // OpenAI文脈維持用
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(...)
character Character @relation(...)
messages ChatMessage[]
@@unique([userId, characterId, date]) // 核心の複合ユニーク制約
}
model ChatMessage {
id String @id @default(cuid())
sessionId String
role String // "user" | "assistant"
content String
createdAt DateTime @default(now())
session ChatSession @relation(...)
@@index([sessionId, createdAt]) // 時系列取得の高速化
}
セッション管理のServer Action
「今日のセッションがあれば取得、なければ作成」というgetOrCreateパターンを使います。
"use server";
function todayDate() {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
}
export async function getOrCreateTodaySession(
userId: string,
characterId: string,
) {
const date = todayDate();
const existing = await prisma.chatSession.findUnique({
where: {
userId_characterId_date: { userId, characterId, date },
},
include: {
messages: { orderBy: { createdAt: "asc" } },
},
});
if (existing) return existing;
return prisma.chatSession.create({
data: { userId, characterId, date },
include: {
messages: { orderBy: { createdAt: "asc" } },
},
});
}
OpenAI文脈の管理
OpenAI Responses APIのprevious_response_idを使うと、過去の会話を再送信せずに文脈を維持できます。このIDをChatSessionのlastResponseIdに保存し、日をまたぐとnullにリセット(新セッション作成時にnull)することで、1日単位で文脈がリセットされます。
// メッセージ送信後にresponseIdを保存
await updateLastResponseId(sessionId, responseId);
// 次のリクエストでprevious_response_idとして渡す
const response = await client.responses.create({
model: "gpt-4.1-mini",
input: userInput,
previous_response_id: session.lastResponseId,
instructions: systemPrompt,
});
まとめ
-
複合ユニーク制約(userId × characterId × date)でセッションの一意性を保証
-
getOrCreateパターンでセッション管理をシンプルに
-
lastResponseIdで日付単位のAI文脈リセットを実現
-
Character.userIdのnullable設計で、デフォルト/個人キャラを統一管理
最新記事
- 【設定・環境構築】OpenNext でNext.js SSGサイトをCloudflare Workersにデプロイする完全ガイド
2026/3/19
- 【実装】Notion calloutブロックをNext.jsでカラフルなUIコンポーネントとして表示する
2026/3/19
- 【トラブルシューティング】Cloudflare Pages → Workers 移行で遭遇したEdge Runtime問題集
2026/3/19
- 【実践】Next.js 13→16メジャーアップグレードの全記録 — 破壊的変更と対応策
2026/3/19
- 【自動化】Gemini Imagen APIでブログのeyecatch画像を自動生成してR2にアップロードする
2026/3/19
- 【実装】Notion APIでブログシステムを構築する(Next.js 13 App Router × SDK v5)
2026/3/19
- 【移行ガイド】microCMSからNotion APIへブログCMSを完全移行する
2026/3/19
- 【トラブルシューティング】本番デプロイで遭遇した問題と解決策まとめ
2026/3/15
- 【環境構築】Next.js × Cloudflare Workers の本番環境を一から構築する
2026/3/15
- 【設定・環境構築】Neon → Prisma Postgres 移行とローカル開発環境の構築
2026/2/26


