

AIキャラクターとチャットできるWebアプリを開発中、ページをリロードすると会話が消えてしまう問題がありました。ユーザー体験としてはリロードしても今日の会話が残っている状態が理想です。さらに、キャラクターごと・日付ごとにセッションを分けて管理したいという要件もありました。
キャラ × 日付でユニークなチャットセッションのDB設計
Prismaスキーマ(Character / ChatSession / ChatMessage)の設計パターン
OpenAI APIの会話文脈(previous_response_id)をセッション単位で管理する方法
Server Actionsでのセッション取得/作成/メッセージ保存の実装
Next.js App Router + Prismaの基本がわかる方
チャットアプリのDB設計に興味がある方
3つのモデルを追加しました。ポイントはChatSessionの複合ユニーク制約(userId × characterId × date)で、同じユーザー・同じキャラ・同じ日に1セッションだけ存在する設計です。
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[]
}
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]) // 時系列取得の高速化
}
「今日のセッションがあれば取得、なければ作成」という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 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設計で、デフォルト/個人キャラを統一管理