Node.js 数据库设计与关联
上一篇的 User 表是孤立的,现实里的数据都有关联,用户有文章,文章有评论。这篇解决表之间怎么建立关系、怎么写关联查询,以及多个操作怎么放进一个事务。数据模型设计好,后面写 API 才顺。
一对多关系
一个用户能发多篇文章,一篇文章只属于一个用户,这就是一对多。
在 Prisma 里表达这种关系要两边都声明
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
}
关系字段的写法
User 这边写 posts Post[],表示一个用户有多篇文章。Post 这边写 author User,表示这篇文章属于哪个用户。关键在 @relation 注解,fields: [authorId] 说 Post 表上有个叫 authorId 的列,references: [id] 说它指向 User 表的 id 列。
外键字段命名有约定,关系名加 id 后缀,作者是 author 就叫 authorId,删掉两个字段里的任何一个,prisma validate 都会报错。
多对多关系
一篇文章可以打多个标签,一个标签对应多篇文章,这是多对多。Prisma 里写隐式多对多最简单,两边各加一个数组字段
model Post {
id Int @id @default(autoincrement())
tags Tag[]
}
model Tag {
name String @id
posts Post[]
}
Prisma 会自动建一张关联表 _PostToTag 存两边的主键,不需要手写中间模型。需要额外存关联信息,比如打标签的时间,就要改成显式中间模型,先了解概念即可。
关联查询
include 预加载
查文章时想连作者一起查出来,用 include
const posts = await prisma.post.findMany({
include: {
author: true
}
});
console.log(posts[0].author.name); // 作者名直接可用
没写 include 时 posts[0].author 是 undefined,Prisma 默认不加载关联数据。
为什么 include 能避免 N+1
不用 include 的写法是先在代码里查文章列表,再循环每条文章单独查一次作者,查 100 条文章就要发 101 次数据库请求,这叫 N+1 查询。include 让 Prisma 用一条带 JOIN 的 SQL 把数据一次拿回来,次数从 N+1 变成 1。
select 只取需要的字段
接口只需要几个字段时用 select 裁剪,顺带还能从关联对象里挑字段
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
author: {
select: { name: true }
}
}
});
响应体只含 id、title 和 author.name,不会把 content 整篇带出去。
事务处理
删除用户时他的文章也得删,两步操作必须要么全成功要么全失败,用 $transaction 包起来
await prisma.$transaction([
prisma.post.deleteMany({ where: { authorId: 1 } }),
prisma.user.delete({ where: { id: 1 } })
]);
中间任何一步报错,数据库自动回滚,不会出现用户没了文章还挂着的情况。交互式写法适合中间需要取值的场景
await prisma.$transaction(async (tx) => {
const user = await tx.user.findUnique({ where: { id: 1 } });
if (!user) throw new Error("用户不存在");
await tx.post.deleteMany({ where: { authorId: user.id } });
await tx.user.delete({ where: { id: user.id } });
});
事务里要用传入的 tx 对象,不能用外层的 prisma,否则操作不在事务范围内。
级联删除
删用户时把文章一起删掉,可以在模型上声明 onDelete: Cascade,数据库会随用户删除自动清理文章,代价是误删用户会连带删掉全部文章。
model Post {
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
}
实践
设计 User-Post-Comment 三张表,文章列表接口带出作者和评论。
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
comments Comment[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id])
postId Int
author User @relation(fields: [authorId], references: [id])
authorId Int
}
npx prisma migrate dev --name post_comment
先造数据,创建用户、文章和评论
const user = await prisma.user.create({ data: { name: "小明" } });
const post = await prisma.post.create({
data: {
title: "第一篇",
content: "内容",
authorId: user.id,
comments: {
create: [{ content: "好文", authorId: user.id }]
}
}
});
嵌套 create 一次建出文章和评论,authorId 指向同一个用户,这是关联写的一侧。查的功夫在下面。
查询接口用嵌套 include 一次带出两层关系
const posts = await prisma.post.findMany({
include: {
author: { select: { name: true } },
comments: {
select: { content: true, author: { select: { name: true } } }
}
}
});
每条文章带作者名和评论列表,一条查询的功夫完成,不用循环再去查。
常见坑
- 只在一端声明关系字段,prisma validate 报错,两边都要写。
- include 里写错关联名,比如把 comments 写成 comment,运行时报 Unknown field。
- 事务里用外层 prisma 对象,操作不在事务内,回滚不生效。
- 忘了跑 migrate,新表查询直接报表不存在。
- 外键字段忘了写 @relation,Prisma 不知道它指向哪张表。
表关系理清后,下一篇解决用户怎么登录,见 Node.js JWT 身份认证。