NestJS + Prisma + PostgreSQL

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 和具体驱动对接起来

  1. 提供连接配置
ts 复制代码
//它拿着你的连接字符串(`DATABASE_URL`),负责建立和管理到数据库的连接。

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
  1. 把 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 通信协议的那个。它做的事:

  1. 通过 TCP 连上 PostgreSQL 服务器
  2. 把 SQL 和参数按 PostgreSQL 协议打包发出去
  3. 接收数据库返回的原始结果
  4. 把结果转成 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 列。这就是自关联------外键和被引用的主键在同一张表里。

相关推荐
苍何1 小时前
开源微信流 APP,手机微信聊天记录,可以直接给 WorkBuddy 和 Obsidian 了
后端
智驭未来掌门人1 小时前
Qt 项目不建议导入 CLion 说明
后端
Lumos_2 小时前
P1 签名 (PKCS#1) vs 裸签 (Raw Signature)
后端
今天早睡2 小时前
调用真实模型,用 Pydantic 校验后再路由
后端
智驭未来掌门人2 小时前
CLion 创建并运行 Qt 项目指南
后端
数据库小学妹2 小时前
OLAP数据库怎么选、怎么优化?路线、提速与边界
运维·数据库·后端
加洛斯2 小时前
万字推演Elasticsearch修炼方法,从炼气到元婴
后端·elasticsearch·面试
当时只道是寻常_2 小时前
WorkBuddy中调用本地ComfyUi工作流免费生图
后端
用户394995084632 小时前
Python 导入机制实战:改了代码为什么不生效
后端