NestJS 🧑‍🍳 厨子必修课(六):Prisma 集成(下)

1. 前言

上篇讲了如何集成 Prisma,那么如何在 NestJS 中把 Prisma 真正用起来呢?这篇告诉你答案。

欢迎加入技术交流群

  1. NestJS 🧑‍🍳 厨子必修课(一):后端的本质
  2. NestJS 🧑‍🍳 厨子必修课(二):项目创建
  3. NestJS 🧑‍🍳 厨子必修课(三):控制器
  4. NestJS 🧑‍🍳 厨子必修课(四):服务类
  5. NestJS 🧑‍🍳 厨子必修课(五):Prisma 集成(上)
  6. 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 类实现了 OnModuleInitOnModuleDestroy 接口。这意味着这个类将提供这两个接口定义的方法,这些方法将在模块的生命周期的特定时刻被调用。
    • 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 类中的 createfindAllfindOneupdateremove 分别实现了创建记录、读取记录、读取单个记录、更新记录、删除记录。

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 模型的 userIduser 需要说明:

  • 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 the User 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 提供了 skiptake 参数,用于实现分页功能。

前端需要传递页码 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),
      },
    };
  }
}
  • pageNumpageSize 是方法的参数,分别表示请求的页码和每页的记录数。它们都有默认值,pageNum 默认为 1,pageSize 默认为 10。
  • pageNum = Math.max(1, pageNum);pageSize = Math.max(1, pageSize);:这两行代码确保 pageNumpageSize 的值至少为 1,防止出现负数或零的情况。
  • skip:计算在分页查询中需要跳过的记录数。
  • const [total, users] = await Promise.all([ ... ]);:使用 Promise.all 同时执行两个异步操作:计算总记录数和获取当前页的用户数据。
    • total 是数据库中用户记录的总数。
    • users 是当前页的用户数据。
  • this.prisma.user.count():调用 PrismaServicecount 方法来获取用户表中的总记录数。
  • 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 进行实际的数据库集成。掌握这些技能后,能够更高效地开发与数据库交互的功能模块,同时也提升了代码的可维护性和稳定性。

相关推荐
假装我不帅13 分钟前
asp.net mvc 常用特性
后端·asp.net·mvc
修炼室22 分钟前
从拥堵到畅通:HTTP/2 如何解决 Web 性能瓶颈?
前端·网络协议·http
让开,我要吃人了1 小时前
HarmonyOS鸿蒙开发实战( Beta5.0)页面加载效果实现详解实践案例
开发语言·前端·华为·移动开发·harmonyos·鸿蒙·鸿蒙系统
洞窝技术2 小时前
重塑前端开发:如何利用 micro-app 实现高效微前端架构
前端·javascript
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(三)
前端·javascript·react.js
想做一只快乐的修狗2 小时前
【react案例】实现评论列表
前端·react.js·前端框架
m0_719414562 小时前
【Vue.js基础】
前端·vue.js·flutter
fxshy2 小时前
01-Cesium添加泛光线
开发语言·前端·javascript
Hanking652032 小时前
Android程序员怎么从零到一开发一个自己的AI小程序并上线
前端·微信小程序·小程序·云开发
小旋风-java2 小时前
springboot整合dwr
java·spring boot·后端·dwr