文章目录
- 前言
-
- `REFERENCES`
- [`ON DELETE ...`](#
ON DELETE ...) - 实际场景对比
- [练兵场 🚀🏋️🏋️🏋️](#练兵场 🚀🏋️🏋️🏋️)
-
-
- [1. 创建两张表](#1. 创建两张表)
- [2. 插数据,看约束效果](#2. 插数据,看约束效果)
- [3. 试试引用违规](#3. 试试引用违规)
- [4. CASCADE 生效](#4. CASCADE 生效)
- [5. 换成 RESTRICT 对比](#5. 换成 RESTRICT 对比)
- [6. SET NULL](#6. SET NULL)
- [7. 回到你项目中的实际场景](#7. 回到你项目中的实际场景)
-
前言
分析这个Prisma 的sql,在AI客服💁项目删除了文档后关联文档chunk 也删除了。
sql
ALTER TABLE "DocumentChunk" ADD CONSTRAINT "DocumentChunk_documentId_fkey"
FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE;
拆开来讲。
这不是 Prisma 特有的,是 SQL 标准。几乎所有主流关系数据库都支持:
| 数据库 | 是否支持 ON DELETE CASCADE |
|---|---|
| PostgreSQL | ✅ |
| MySQL / MariaDB | ✅ |
| SQLite | ✅ |
| SQL Server | ✅ |
| Oracle | ✅ |
| DuckDB | ✅ |
写法几乎一模一样:
sql
-- PostgreSQL / MySQL / SQLite / SQL Server / Oracle
FOREIGN KEY (documentId) REFERENCES Document(id) ON DELETE CASCADE
实际上 Prisma 的 @relation(onDelete: Cascade) 最终生成的也是上面这句 SQL。我这里直接在迁移里写原生 SQL,是因为 vector(1536) 是 pgvector 扩展类型,Prisma schema 里标了 Unsupported,没法走它自己的关系声明,所以手动在 migration 文件里加上去了。
REFERENCES
声明"这个字段的值,必须是另一个表的某个字段里已经存在的值"
sql
FOREIGN KEY ("documentId") REFERENCES "Document"("id")
意思是:DocumentChunk.documentId 里填的值,必须在 Document.id 里找得到。如果你试图插入一个不存在的 documentId,数据库直接报错拒绝。
这叫 引用完整性(referential integrity)------保证数据不会指向一个不存在的东西。
ON DELETE ...
当被引用的行(Document)被删除时,关联的行(DocumentChunk)怎么办?有 4 种选项:
| 选项 | 行为 | 例子 |
|---|---|---|
CASCADE |
跟着删 | 删文档 → 分块自动全删 |
SET NULL |
把外键字段设成 NULL | 删文档 → 分块还在,但 documentId 变 NULL |
SET DEFAULT |
设成默认值 | 删文档 → documentId 变成 DEFAULT 指定的值 |
RESTRICT / NO ACTION |
禁止删除 | 删文档 → 数据库报错:"还有分块引用着它,不让删" |
实际场景对比
文档 → 删了
├── CASCADE → 分块也删 ✅ 知识库删除
├── SET NULL → 分块变孤儿记录 ⚠️ 极少用
└── RESTRICT → 删不了文档 ✅ 订单明细场景
订单 → 想删
└─ 还有订单明细引用它
→ RESTRICT → "先删明细,再删订单"(防止误删)
所以 ON DELETE CASCADE 的选择逻辑是: 子记录离开父记录没有独立存在的意义 → 父删子也删。DocumentChunk 就是这种场景。
练兵场 🚀🏋️🏋️🏋️
1. 创建两张表
sql
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE post (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INTEGER REFERENCES author(id) ON DELETE CASCADE
);
注意:
REFERENCES的完整写法是FOREIGN KEY (author_id) REFERENCES author(id),字段在同一行时可以简写。
2. 插数据,看约束效果
sql
INSERT INTO author (name) VALUES ('Alice'), ('Bob');
-- 2 行
INSERT INTO post (title, author_id) VALUES ('文章A', 1), ('文章B', 1), ('文章C', 2);
-- 3 行
现在查一下:
sql
SELECT p.title, a.name
FROM post p
JOIN author a ON a.id = p.author_id;
结果应该是:
| title | name |
|---|---|
| 文章A | Alice |
| 文章B | Alice |
| 文章C | Bob |
3. 试试引用违规
sql
INSERT INTO post (title, author_id) VALUES ('文章D', 999);
报错: insert or update on table "post" violates foreign key constraint。因为 author_id=999 不存在。数据库在保护你------不会让你插入一个指向不存在作者的帖子。
4. CASCADE 生效
sql
DELETE FROM author WHERE id = 1;
-- 删掉了 Alice
不用写 DELETE FROM post WHERE author_id = 1,PostgreSQL 自动把 Alice 的两篇文章也删了。
验证:
sql
SELECT * FROM author; -- 只剩 Bob
SELECT * FROM post; -- 只剩 文章C(Bob 的)
5. 换成 RESTRICT 对比
sql
-- 重建,换成 RESTRICT
DROP TABLE post, author;
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE post (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INTEGER REFERENCES author(id) ON DELETE RESTRICT
);
INSERT INTO author (name) VALUES ('Alice');
INSERT INTO post (title, author_id) VALUES ('文章A', 1);
现在试试删:
sql
DELETE FROM author WHERE id = 1;
报错: 还有文章引用作者,不让删。你得先删文章,再删作者:
sql
DELETE FROM post WHERE author_id = 1;
DELETE FROM author WHERE id = 1; -- 这次可以
这就是 RESTRICT 的场景------订单明细还在,不许删订单。
6. SET NULL
sql
CREATE TABLE post (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INTEGER REFERENCES author(id) ON DELETE SET NULL
);
-- 删掉作者 Alice
DELETE FROM author WHERE id = 1;
-- 文章还在,但 author_id 变成了 NULL
SELECT * FROM post;
| id | title | author_id |
|---|---|---|
| 1 | 文章A | null |
7. 回到你项目中的实际场景
sql
ALTER TABLE "DocumentChunk"
ADD CONSTRAINT "DocumentChunk_documentId_fkey"
FOREIGN KEY ("documentId")
REFERENCES "Document"("id")
ON DELETE CASCADE;
整个链路的流程:
js
你点删除 → fetch /api/documents?id=xxx,DELETE 方法
→ prisma.document.delete({ where: { id: xxx } })
→ Prisma 生成 DELETE FROM "Document" WHERE id = 'xxx'
→ PostgreSQL 执行 DELETE
→ 碰到 ON DELETE CASCADE,自动补一条:
DELETE FROM "DocumentChunk" WHERE "documentId" = 'xxx'
→ 文档 + 所有分块,一行代码都不用写,全部清干净