概要
deletedAt フィールドに削除日時を記録する論理削除(soft delete)パターン。物理的にレコードを消さずに「削除済み」として扱い、通常のクエリでは deletedAt: null を条件に付けることで自動的に除外する。
インストール
npm install prisma @prisma/client
npx prisma init
実装
スキーマ定義
// prisma/schema.prisma
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // null = 有効, 値あり = 論理削除済み
}
Prisma クライアント(シングルトン)
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({ log: ["query"] });
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
論理削除を含む CRUD 操作
// lib/posts.ts
import { prisma } from "@/lib/prisma";
// 有効な投稿のみ取得(deletedAt: null でフィルタ)
export async function getPosts() {
return prisma.post.findMany({
where: { deletedAt: null },
orderBy: { createdAt: "desc" },
});
}
// ID 指定取得(削除済みも除外)
export async function getPost(id: number) {
return prisma.post.findFirst({
where: { id, deletedAt: null },
});
}
// 論理削除: deletedAt に現在時刻をセット
export async function softDeletePost(id: number) {
return prisma.post.update({
where: { id },
data: { deletedAt: new Date() },
});
}
// 論理削除の取り消し(復元)
export async function restorePost(id: number) {
return prisma.post.update({
where: { id },
data: { deletedAt: null },
});
}
// 削除済みレコードのみ取得(管理画面用など)
export async function getDeletedPosts() {
return prisma.post.findMany({
where: { deletedAt: { not: null } },
orderBy: { deletedAt: "desc" },
});
}
// 完全削除(物理削除): 必要に応じて
export async function hardDeletePost(id: number) {
return prisma.post.delete({ where: { id } });
}
Route Handler での使用
// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";
import { softDeletePost, getPost } from "@/lib/posts";
type Params = { params: Promise<{ id: string }> };
export async function DELETE(_req: Request, { params }: Params) {
const { id } = await params;
const postId = Number(id);
const post = await getPost(postId);
if (!post) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
await softDeletePost(postId);
return NextResponse.json({ message: "削除しました" });
}
管理画面向け: 削除済み一覧と復元
// app/api/posts/deleted/route.ts
import { NextResponse } from "next/server";
import { getDeletedPosts } from "@/lib/posts";
export async function GET() {
const posts = await getDeletedPosts();
return NextResponse.json(posts);
}
// app/api/posts/[id]/restore/route.ts
import { NextResponse } from "next/server";
import { restorePost } from "@/lib/posts";
type Params = { params: Promise<{ id: string }> };
export async function POST(_req: Request, { params }: Params) {
const { id } = await params;
await restorePost(Number(id));
return NextResponse.json({ message: "復元しました" });
}
ポイント
- 論理削除は
deletedAt: nullで「有効」、deletedAt: DateTimeで「削除済み」を表す。全クエリにwhere: { deletedAt: null }を付けることで削除済みレコードを除外する - 物理削除(
prisma.post.delete)との使い分け: 監査ログが必要・復元が必要・参照整合性を保ちたい場合は論理削除。ストレージを節約したい・GDPR の完全消去が必要な場合は物理削除 getPostではfindUniqueではなくfindFirstを使う。findUniqueはwhereにdeletedAt: nullを組み合わせられないため- Prisma の
$extendsを使うとsoftDeleteをモデルメソッドとして追加できるが、このサンプルでは可読性のためシンプルな関数ラッパーで示す - 定期的なハードデリートが必要な場合は、
deletedAt < N日前を条件に cron ジョブで物理削除するパターンと組み合わせる