1. 前言
上篇讲了如何集成 Prisma,那么如何在 NestJS 中把 Prisma 真正用起来呢?这篇告诉你答案。
欢迎加入技术交流群。
- NestJS 🧑🍳 厨子必修课(一):后端的本质
- NestJS 🧑🍳 厨子必修课(二):项目创建
- NestJS 🧑🍳 厨子必修课(三):控制器
- NestJS 🧑🍳 厨子必修课(四):服务类
- NestJS 🧑🍳 厨子必修课(五):Prisma 集成(上)
- NestJS 🧑🍳 厨子必修课(六):Prisma 集成(下)
2. 共享 Prisma 服务
Prisma Client 在不同接口都会使用到,借助 NestJS 的依赖注入可以将其抽象为一个服务以共享给其他的模块或服务。
bash
nest generate service prisma
2.1 定义 Prisma 服务类
修改 prisma.service.ts 如下:
tsx
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
extends PrismaClient
:定义了一个名为PrismaService
的类,它继承自PrismaClient
。这意味着PrismaService
将拥有 Prisma Client 的所有方法和属性。implements OnModuleInit, OnModuleDestroy
: 这一行指定PrismaService
类实现了OnModuleInit
和OnModuleDestroy
接口。这意味着这个类将提供这两个接口定义的方法,这些方法将在模块的生命周期的特定时刻被调用。onModuleInit
方法将在模块初始化时被调用,执行 Prisma Client 的$connect
方法来建立数据库连接onModuleDestroy
方法将在模块销毁时被调用,执行 Prisma Client 的$disconnect
方法来关闭数据库连接。
另外,在 app.module.ts 的 providers
数组中自动注册了这个服务类,把这里的 PrismaService
移除。
2.2 注入与注册 Prisma 服务类
运行以下命令创建 users 资源:
bash
nest g resource users
直接核心来到 users.service.ts 中注入 Prisma 服务类,使之可以真实地与数据库交互。
diff
+ import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class UsersService {
+ constructor(private readonly prisma: PrismaService) {}
}
但是!注意在NestJS中,模块的作用域决定了服务、控制器、守卫、拦截器等组件的可见性和可注入性。因此需要将 PrismaService
类注册进 user.module.ts 的 providers
中:
tsx
// ...
import { PrismaService } from 'src/prisma/prisma.service';
@Module({
// ...
providers: [UsersService, PrismaService],
})
export class UsersModule {}
这样,users 模块这个作用域下的服务就能使用 PrismaService
类了。
2.3 调用 Prisma Client 实例完成 CRUD
默认示例中返回了字符串,现在修改如下:
diff
// ...
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
create(createUserDto: CreateUserDto) {
- return 'This action adds a new user';
+ return this.prisma.user.create({
+ data: createUserDto,
+ });
}
findAll() {
- return `This action returns all users`;
+ return this.prisma.user.findMany();
}
findOne(id: number) {
- return `This action returns a #${id} user`;
+ return this.prisma.user.findUnique({
+ where: { id },
+ });
}
update(id: number, updateUserDto: UpdateUserDto) {
- return `This action updates a #${id} user`;
+ return this.prisma.user.update({
+ where: { id },
+ data: updateUserDto,
+ });
}
remove(id: number) {
- return `This action removes a #${id} user`;
+ return this.prisma.user.delete({
+ where: { id },
+ });
}
}
UsersService
类中的 create
、findAll
、findOne
、update
、remove
分别实现了创建记录、读取记录、读取单个记录、更新记录、删除记录。
this.prisma
就是 Prisma Client 实例的引用,Prisma Client 是Prisma ORM的客户端库,它提供了与数据库交互的方法。
通过 .user
就能唤起 Prisma Client 中定义的模型引用,user 是模型的名称,它对应于数据库中的一个表。
this.prisma.user.create
是 Prisma Client 提供的方法,用于在数据库中创建新的记录。{ data: createUserDto }
是一个对象,指定了要创建的数据。这里的data
属性包含了用户的所有信息,这些信息将被插入到数据库中。this.prisma.user.findMany
用于查询数据库中的多条记录,返回一个包含所有用户记录的数组。this.prisma.user.findUnique
用于查找数据库中的一条特定记录。{ where: { id } }
是一个对象,指定了查找条件。这里的where
属性定义了查找记录的条件,即 ID 必须与提供的id
参数匹配。this.prisma.user.update
用于更新数据库中的记录。{ where: { id }, data: updateUserDto }
是一个对象,指定了更新的条件和数据。where
属性定义了要更新的记录的条件,而data
属性包含了要更新的数据。this.prisma.user.delete
用于从数据库中删除一条记录。{ where: { id } }
是一个对象,指定了要删除的记录的条件。这里的where
属性定义了要删除的记录的 ID。
值得注意的是 createUserDto 的类型 CreateUserDto 以及 updateUserDto 的类型 UpdateUserDto,这类 dto 文件用于定义请求参数的类型,例如创建和更新 user 时,需要接收的数据类型是已经决定的:
tsx
// src/users/dto/create-user.dto.ts
export class CreateUserDto {
name?: string;
email: string;
password: string;
}
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
PartialType(CreateUserDto)
是一个泛型调用,它将CreateUserDto
中的所有字段转换为可选字段。这意味着在更新用户信息时,用户可以传递任何字段,也可以不传递任何字段,更加灵活。extends PartialType(CreateUserDto)
则表示UpdateUserDto
继承自PartialType(CreateUserDto)
。
访问 http://localhost:3000/users 可以看到结果:
3. 高级查询
Prisma 还支持复杂的查询操作,如关联查询、分页、过滤等。
3.1 关联查询(Relational Queries)
每一个用户都有自己的订单记录,这是典型的 one-to-many 关联(一对多)关系:
上图中可以看出 User 表中的路人甲(id
为 1)拥有三笔订单,分别对应 Order 表中 id
为 1、2、4 的记录。
模型定义如下:
kotlin
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
在 User 模型定义中新增了两个字段:
updatedAt
:日期时间字段,记录用户记录最后一次更新的时间。它被标记为@updatedAt
,Prisma 会自动在每次更新用户记录时设置这个字段。orders
:关系字段,表示用户和订单之间的关系。它是一个Order
数组,意味着一个用户可以有多个订单。
新建的 Order 模型的 userId
和 user
需要说明:
userId
:整型字段,用作外键,引用 User 模型的id
字段。它用于建立 User 和 Order 之间的关系。user
:关系字段,表示订单和用户之间的关系。它被标记为@relation
,指定了关系的细节,包括使用的字段(fields: [userId]
)和引用的字段(references: [id]
)。这意味着每个订单都关联到一个用户,可以通过userId
字段来访问。
⚠️ 注意:在 Prisma 中,关系字段用于定义模型间的逻辑关系,而不是存储实际的数据,因此并不会真实出现在数据表中。实际的关系是通过在相关模型的表中使用外键来实现的,比如上面的 userId
就是一个外键,它引用 User 模型的 id
字段。
每次修改好模型定义后,需要迁移数据库:
bash
npx prisma migrate dev --name add-orders
在执行后会有报错:
Step 0 Added the required column
updatedAt
to theUser
table without a default value. There are 3 rows in this table, it is not possible to execute this step.You can use prisma migrate dev --create-only to create the migration file, and manually modify it to address the underlying issue(s). Then run prisma migrate dev to apply it and verify it works.
是说 User 表里本来没有 updatedAt
字段,需要一个默认值。同时它给出了解决办法,让我们使用 prisma migrate dev --create-only
来创建一个迁移文件,然后手动修改给 updatedAt
一个默认值,然后再运行 prisma migrate dev
才行。
在运行 prisma migrate dev --name add-orders --create-only
后生成了一个迁移文件:
进入 migration.sql:
sql
/*
Warnings:
- Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
在 Warings 部分提示要加上默认值:在 ALTER TABLE
所在行的最后追加 DEFAULT CURRENT_TIMESTAMP
。表示 updatedAt
的默认值为当前时间戳。
修改完 sql 文件后运行:
bash
npx prisma migrate dev --name add-orders
这样就完成迁移了。
每次完成数据库迁移后,最好都执行一次 npx prisma generate
来更新 Prisma 客户端。
⚠️ 注意:在生产环境中进行这种更改时要格外小心,并确保在应用更改之前备份数据。
关联查询的目的是:获取用户信息的同时得到他的订单。
diff
// users.service.ts
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
findOne(id: number) {
return this.prisma.user.findUnique({
where: { id },
+ include: {
+ orders: true,
+ },
});
}
}
include
:这是一个包含选项对象,用于指定在查询时应该加载哪些关联数据。在这个例子中,orders: true
表示应该加载与找到的用户关联的所有订单。
访问 http://localhost:3000/users/4 的结果:
3.2 分页(Pagination)
Prisma 提供了 skip
和 take
参数,用于实现分页功能。
前端需要传递页码 pageNum
和页面数据承载数量 pageSize
:
tsx
// users.controller.ts
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(
@Query('pageNum') pageNum?: number,
@Query('pageSize') pageSize?: number,
) {
return this.usersService.findAll(pageNum, pageSize);
}
}
tsx
// users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async findAll(pageNum: number = 1, pageSize: number = 10) {
pageNum = Math.max(1, pageNum);
pageSize = Math.max(1, pageSize);
const skip = (pageNum - 1) * pageSize;
const [total, users] = await Promise.all([
this.prisma.user.count(),
this.prisma.user.findMany({
skip,
take: pageSize,
orderBy: {
id: 'asc',
},
}),
]);
return {
users,
meta: {
total,
pageNum,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
};
}
}
pageNum
和pageSize
是方法的参数,分别表示请求的页码和每页的记录数。它们都有默认值,pageNum
默认为 1,pageSize
默认为 10。pageNum = Math.max(1, pageNum);
和pageSize = Math.max(1, pageSize);
:这两行代码确保pageNum
和pageSize
的值至少为 1,防止出现负数或零的情况。skip
:计算在分页查询中需要跳过的记录数。const [total, users] = await Promise.all([ ... ]);
:使用Promise.all
同时执行两个异步操作:计算总记录数和获取当前页的用户数据。total
是数据库中用户记录的总数。users
是当前页的用户数据。
this.prisma.user.count()
:调用PrismaService
的count
方法来获取用户表中的总记录数。orderBy: { id: 'asc' }
用于指定返回记录的排序方式,这里按照id
字段升序排序。
http://localhost:3000/users 如图:
http://localhost:3000/users?pageNum=1&pageSize=2 如图:
3.3 过滤(Filtering)
在之前通过 id 能够过滤出满足要求的用户,现在看看更为高级的过滤方法。使用 where 实现模糊查询。
添加 /users/search 接口,前端需要传 query
过来:
tsx
// users.controller.ts
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('search')
search(@Query('query') query: string) {
return this.usersService.searchUsers(query);
}
}
添加模糊查询业务逻辑:
tsx
// users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async searchUsers(query: string) {
return this.prisma.user.findMany({
where: {
OR: [{ name: { contains: query } }, { email: { contains: query } }],
},
});
}
}
query
参数是用户输入的搜索字符串where: { OR: [{ name: { contains: query } }, { email: { contains: query } }] }
:where
属性定义了查询的过滤条件。OR
是一个逻辑操作符,表示满足数组中任意一个条件的记录都应该被检索出来。- 第一个条件是
name
字段包含query
字符串,使用了contains
过滤器,表示搜索name
字段中包含给定query
字符串的用户。 - 第二个条件是
email
字段包含query
字符串,同样使用了contains
过滤器,表示搜索email
字段中包含给定query
字符串的用户。
http://localhost:3000/users/search?query=bo 如图:
4. 总结
通过 Prisma 集成相关内容,大家已经知道如何在 NestJS 中使用 Prisma 进行实际的数据库集成。掌握这些技能后,能够更高效地开发与数据库交互的功能模块,同时也提升了代码的可维护性和稳定性。