Nestjs部分
Service 调用 Service
一个 Service 也可以调用另一个 Service,在构造函数里注入即可:
ts
// 假设 UserService 需要用到 MailService 发邮件
@Injectable()
export class UserService {
// 注入另一个 Service
constructor(private readonly mailService: MailService) {}
createUser(dto: CreateUserDto) {
// ...创建用户逻辑
this.mailService.sendWelcomeEmail(dto.email) // 调用另一个 service
return { success: true }
}
}
注意:被注入的 Service 必须在同一个模块的 providers 里注册,或者从其他模块 exports 出来.
比如 AppService 定义在 UserModule,想在 OrderModule 用:
ts
// user.module.ts
@Module({
providers: [UserService],
exports: [UserService], // ← 必须导出
})
export class UserModule {}
ts
// order.module.ts
@Module({
imports: [UserModule], // ← 必须导入那个模块
providers: [OrderService],
})
export class OrderModule {}
ts
// order.service.ts
@Injectable()
export class OrderService {
constructor(private userService: UserService) {} // 现在能注入了
}
Prisma 7
安装Prisma 依赖
bash
# Prisma CLI(开发依赖)
npm install prisma@latest --save-dev
# Prisma Client(运行时)
npm install @prisma/client@latest
# Prisma 7 必须安装的 PostgreSQL Driver Adapter(核心变化!)
# Prisma 7 移除了内置数据库驱动,必须手动安装 adapter
npm install @prisma/adapter-pg pg
# pg 的 TypeScript 类型定义
npm install @types/pg --save-dev
@prisma/client
- 是 Prisma ORM 的核心库,简单说就是你在代码里实际调用、用来查询数据库的那个"客户端" 。你根据自己定义的数据库模型(Schema),通过
prisma generate命令生成的代码,用它来写查询,而不是手写 SQL。prisma.user.findMany()的地方,负责把请求变成 SQL 语句。一句话总结 :@prisma/client是你用 Prisma 操作数据库的"官方手把手教出来的查询助手",你所有prisma.xxx的写法都源于它。
@prisma/adapter-pg pg
@prisma/adapter-pg:Prisma 的适配层,负责对接 Prisma 和底层驱动,把 Prisma Client 和pg驱动对接起来pg:底层驱动,真正负责连数据库、发 SQL
Prisma 7之前和现在的区别:
-
Prisma 7 改变了连接数据库的方式,不再"自带"驱动,而是要求你明确指定并安装一个中间的"翻译官"------adapter
-
在 Prisma 7 之前,Prisma Client 内部捆绑了一个用 Rust 编写的查询引擎,这个引擎自己就包含了连接 PostgreSQL 等数据库所需的驱动。你不需要额外安装什么,只要在 schema 里配好连接字符串,它就能直接连上数据库。 但从 Prisma 7 开始,Prisma 转向了更轻量的、纯 TypeScript 的实现,不再包含那个庞大的 Rust 引擎,自然也就不再内置任何数据库驱动了。所以,你现在需要一个新角色来负责"连接数据库"这件事。
prisma/adapter-pg
arduino
Prisma Client → @prisma/adapter-pg → pg(驱动)→ PostgreSQL
- Prisma 的转接层,它内部会用到 pg,把 Prisma 的查询交给 pg 去执行
所以你装 @prisma/adapter-pg 时,它通常会把 pg 作为依赖一起带上,你一般不用单独再装 pg(但也可以显式装)。
pg
- PostgreSQL 数据库和你的 Node.js 程序是两套完全不同的东西,它们之间要通信,需要一种双方都能听懂的方式。数据库用的是 PostgreSQL 通信协议,而 JS 程序只会讲 JS。
你的 JS 代码 → pg 驱动 → PostgreSQL 协议 → PostgreSQL 数据库 pg 这个包(node-postgres)就是最流行的 PostgreSQL 的 JavaScript 驱动。
less
你的代码 → @prisma/client → @prisma/adapter-pg → pg → PostgreSQL
sql
你的代码
↓
Prisma Client(把 findMany 变成 SQL)
↓
Driver Adapter(适配器,翻译 + 转发)> Adapter 是 Prisma 和具体数据库驱动之间的"转接头",让 Prisma 不用关心底层到底用的是哪个驱动。
↓
JS 数据库驱动(如 pg,真正连数据库)
↓
PostgreSQL 数据库(执行 SQL,返回结果)
↓
原路返回
具体实现:
- 第 1 层:Prisma Client 把查询转成 SQL
ts
prisma.user.findMany({ where: { age: { gt: 18 } } })
但 PostgreSQL 根本看不懂 findMany 这种写法,它只认 SQL。所以 Prisma Client 的第一步,是把你这句查询编译成一条标准 SQL 。Prisma 只是"把话写好了",但还没真正发出去。它手里现在攥着一条 SQL 字符串,等着有人帮它送到数据库。
- 第 2 层:Adapter 接手
职责是把 Prisma 和具体驱动对接起来
- 提供连接配置
ts
//它拿着你的连接字符串(`DATABASE_URL`),负责建立和管理到数据库的连接。
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
- 把 SQL 转发给驱动去执行
sql
Prisma 生成的 SQL,不会直接扔给数据库,而是交给 Adapter,Adapter 再调用底层驱动的方法去执行。
ts
// 简化示意,实际更复杂
class PrismaPgAdapter {
async queryRaw(sql: string, params: any[]) {
// 它内部持有 pg 驱动
return this.pgClient.query(sql, params)
}
}
所以 Adapter 本身不执行 SQL,它只是"转交"。真正干活的是下一层的驱动。
关键点:Adapter 是适配层,负责把 Prisma 的调用翻译成某个具体驱动能接受的调用形式。
- 第 3 层:JS 驱动真正连数据库
底层驱动(比如 pg)才是真正懂 PostgreSQL 通信协议的那个。它做的事:
- 通过 TCP 连上 PostgreSQL 服务器
- 把 SQL 和参数按 PostgreSQL 协议打包发出去
- 接收数据库返回的原始结果
- 把结果转成 JS 能用的格式(对象、数组等)
ts
// pg 驱动真正执行的
const result = await pgClient.query('SELECT * FROM "User" WHERE "age" > $1', [18])
数据库执行完 SQL,把结果返回给驱动,驱动再一层层往回传:
arduino
PostgreSQL → pg 驱动 → Adapter → Prisma Client → 你的代码
为什么要有 Adapter 这一层?
你可能会问:既然 pg 已经能连数据库了,Prisma 为什么不直接调用 pg,非要中间加个 Adapter?
原因是 Prisma 要支持很多种数据库和运行环境,而每种环境用的驱动都不一样:
| 数据库 / 环境 | 底层驱动 |
|---|---|
| PostgreSQL(Node.js) | pg |
| PostgreSQL(Serverless/边缘) | @neondatabase/serverless 等 |
| MySQL | mysql2 |
| SQLite | better-sqlite3 |
| Cloudflare D1 | D1 自己的 API |
如果 Prisma 直接写死调用 pg,那换一个环境就得改 Prisma 源码。有了 Adapter 这层抽象,Prisma Client 只认 Adapter 接口,具体用哪个驱动由你选的 Adapter 决定。
arduino
Prisma Client → 统一的 Adapter 接口 → 各种具体 Adapter → 各种驱动
这就是软件设计里的适配器模式:让不兼容的两方(Prisma 和某个驱动)能协同工作,而不用改动任何一方的核心代码。
@types/pg
@types/pg 是给 pg 补 TypeScript 类型用的,只在开发时需要,所以用 --save-dev 装到开发依赖里,和生产环境无关。
TS 不知道 Client 是什么、有哪些方法、参数是什么类型。装上 @types/pg 后,编辑器就能给你类型提示和自动补全,写错参数也会报错。
配置 schema.prisma
prisma/schema.prisma
ts
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
output = "../src/generated/prisma"生成了什么
当你运行 npx prisma generate 时,Prisma 会读取你的 schema.prisma,并在 output 指定的目录里生成一套只属于你项目的、完整的 TypeScript 查询客户端代码
TypeScript 查询客户端代码?
它是用 TypeScript 写的、专门给你用来查询数据库的一套代码,而且带完整类型。
执行迁移
migrate dev(建表)
bash
# 第一次建表
npx prisma migrate dev --name init
根据你的
schema.prisma定义,在开发环境中创建并应用一次数据库迁移,--name init则给这次迁移起名为 "init"
generate 生成 Prisma Client(每次修改 schema 后执行
bash
npx prisma generate
两者区别
🗄️ prisma migrate dev:改数据库
它负责让数据库的表结构,跟上你 schema.prisma 里定义的模型。
- 做什么 :对比 schema 和数据库现状,生成 SQL 迁移文件,并实际去修改数据库(建表、加字段、改类型等)
- 作用对象 :数据库本身
- 产出 :
prisma/migrations/下的迁移文件 + 数据库结构真的变了 - 使用场景:你在开发环境改了 schema,需要同步到数据库时
⚙️ prisma generate:生成代码
它负责根据 schema.prisma 生成你代码里要 import 的 Prisma Client 代码。
- 做什么 :读取 schema,生成一套带完整类型的查询客户端代码(
prisma.user.findMany()等) - 作用对象 :你项目里的代码文件
- 产出 :
output目录下(如src/generated/prisma)的一堆.ts文件 - 使用场景 :schema 改了,需要让代码里的
prisma认识新模型时
migrate dev改数据库,generate改代码。 前者让数据库结构跟上 schema,后者让你代码里的prisma客户端跟上 schema。Prisma 7 里两者必须分开手动执行。
字段修饰符(@装饰器)
索引
ts
// @@index 给字段加索引,提升查询性能
// 适合经常作为 where 条件的字段
@@index([authorId])
// 多字段组合索引
@@index([status, createdAt])
它是提前给数据库建一个"目录",让某些查询变快,而不是查询本身。
"组合"索引
把两个字段合在一起排序,像目录是按"先状态、再时间"两级排序的。
Prisma Client 查询常用写法
select include
prisma
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String?
city String?
userId Int @unique
user User @relation(fields: [userId], references: [id])
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
authorId Int
author User @relation(fields: [authorId], references: [id])
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
text String
postId Int
post Post @relation(fields: [postId], references: [id])
}
ts
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
profile: true,
posts: true,
},
})
js
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
password: 'hashed_xxx', // ⚠️ 注意:include 不会排除字段,password 也返回了
profile: { // 对象(一对一)
id: 1,
bio: '热爱编程',
city: '北京',
userId: 1
},
posts: [ // 数组(一对多)
{ id: 1, title: '第一篇', content: '...', published: true, createdAt: '2024-01-01T...', authorId: 1 },
{ id: 2, title: '第二篇', content: '...', published: false, createdAt: '2024-01-02T...', authorId: 1 }
]
}
ts
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
include: { comments: true }, // 每篇文章再带出评论
where: { published: true }, // 只要已发布
orderBy: { createdAt: 'desc' }, // 时间倒序
},
},
})
js
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
password: 'hashed_xxx',
posts: [ // 只含 published = true 的文章
{
id: 5,
title: '最新文章',
content: '...',
published: true,
createdAt: '2024-03-01T...',
authorId: 1,
comments: [ // 每篇文章的评论
{ id: 10, text: '写得好', postId: 5 },
{ id: 11, text: '学习了', postId: 5 }
]
},
{
id: 3,
title: '较早的文章',
content: '...',
published: true,
createdAt: '2024-02-01T...',
authorId: 1,
comments: [] // 没评论就是空数组
}
]
}
ts
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
name: true,
email: true,
// password 不写 → 不返回
posts: {
select: { id: true, title: true },
},
},
})
js
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
// 没有 password ✅
posts: [
{ id: 1, title: '第一篇' }, // 只有 id 和 title
{ id: 2, title: '第二篇' },
{ id: 3, title: '第三篇' }
]
}
三段对比总结
| 对比项 | 第一段 include | 第二段 嵌套include | 第三段 select |
|---|---|---|---|
| 返回 User 字段 | 全部(含 password) | 全部(含 password) | 只返回指定字段 |
| 关联过滤/排序 | ❌ 无 | ✅ where + orderBy | ✅ 可加 |
| 嵌套深度 | 1 层 | 2 层(posts→comments) | 1 层 |
| 敏感字段控制 | ❌ 做不到 | ❌ 做不到 | ✅ 能做到 |
| 适用场景 | 快速拿全量关联 | 复杂关联树 | 精确控制输出 |
1. select 和 include 不能在同一层级同时用
ts
// ❌ 报错
{ select: { name: true }, include: { posts: true } }
// ✅ 正确:用 select 嵌套
{ select: { name: true, posts: { select: { title: true } } } }
2. include 不会帮你隐藏字段
很多人以为
include只返回关联,其实它默认返回本表所有标量字段 。要排除敏感字段(password、token),必须用select。
排序
PRISMA
// 多字段排序
prisma.post.findMany({
orderBy: [
{ published: 'desc' },
{ createdAt: 'desc' },
],
})
多字段排序 ,意思是:先按
published降序排,published相同的再按createdAt降序排。
模型之间的关联关系
自关联(同一张表内的关系)
js
model Comment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
// 外键:父评论的 id(顶级评论该字段为 null)
// Int? 末尾 ? 表示可选,顶级评论没有父评论
parentId Int?
// 关联到同一张表的父评论
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
// 反向关联:该评论下的所有子评论(回复)
// 注意:双方都要有相同的关系名 "CommentReplies"
replies Comment[] @relation("CommentReplies")
@@map("comments")
}
parent:这是一个关系字段 ,不是数据库里的列。它让你在代码里能直接访问"这条评论的父评论对象",比如comment.parent.content。Comment?:类型是Comment,问号表示可能没有父评论(顶级评论就是 null)。@relation("CommentReplies", ...):关系名叫"CommentReplies",这个名字是两边配对用的钥匙,不是数据库字段。fields: [parentId]:说明这个关系用本表的parentId列作为外键。references: [id]:说明外键指向本表(Comment)的id列。replies:反向关系字段,让你能拿到"这条评论下的所有回复",比如comment.replies是一个数组。Comment[]:类型是 Comment 数组,表示一对多里的"多"。@relation("CommentReplies"):关系名必须和上面parent的完全一致,Prisma 靠这个名字知道这两个字段描述的是同一个关系的两端。
为什么两边都要写关系名
因为这是同一张表关联自己 (自关联)。如果不写关系名,Prisma 无法区分"哪两个字段是一对"。写了同一个名字
"CommentReplies",Prisma 就知道:parent和replies是同一个关系的两端------一条评论有一个父评论(parent),一个父评论有多条回复(replies)。
数据库本身并不知道"parentId存的是评论 id"。是 Prisma 通过@relation告诉数据库:把parentId这一列设成外键,让它去引用comments表自己的id列。
"CommentReplies":关系名,只是给这个关系起个标签,用来和另一边的replies配对。它不是列,也不指向任何表。fields: [parentId]:说的是 "外键在本表的哪一列" 。答案是本表的parentId列。也就是说,这条评论把"我爹是谁"记录在自己的parentId字段里。references: [id]:说的是 "外键指向对方的哪一列" 。这里的"对方"就是Comment模型自己,所以指向的是Comment的id列。合起来就是:parentId的值,必须是comments表里某条评论的id。所以完整读法是:本表的
parentId列,引用本表id列。这就是自关联------外键和被引用的主键在同一张表里。