NestJS【实战】操纵数据库(含集成 prisma,创建表,添加数据校验,数据的 ...

打印 上一主题 下一主题

主题 862|帖子 862|积分 2586



   单库 or 多库 ?

  方案长处缺点适用场景单库成本低
开发摆设快捷性能受限
扩展性有限
故障风险数据量适中
安全性需求不高多库可高度自定义架构复杂,摆设和维护成本较高
数据一致性题目
分布式事务处理较为复杂自定义需求高
数据量大
安全性需求高
合规合法要求(物理隔离)   ORM 库选型

  



  • 需访问多种关系数据库,且数据库版本比较低,则选择 TypeORM
  • 需访问的数据库版本比较高,优先选择 Prisma
  • 仅需访问 MongoDB ,优先选择 Mongoose
   集成 ORM 库 – prisma

  安装 prisma
  1. pnpm i -D prisma
复制代码
vscode 安装官方插件

初始化 prisma
  1. npx prisma init --datasource-provider postgresql
复制代码


  • 使用的 postgresql 数据库
  • 初始化后,会生成文件 .env 和 prisma\schema.prisma
修改 .env
  1. DATABASE_URL="postgresql://数据库用户名:密码@数据库IP:数据库端口/数据库名称?schema=public"
复制代码
最终范比方下:
  1. DATABASE_URL="postgresql://testuser:test6666@localhost:5432/testdb?schema=public"
复制代码
package.json 中新增脚本
  1.     "prisma:generate": "prisma generate",
  2.     "prisma:pull-DB": "prisma db pull",
  3.     "prisma:push-DB": "prisma db push",
  4.     "prisma:migrate-create": "prisma migrate dev",
  5.     "prisma:migrate-deploy": "prisma migrate deploy",
复制代码


  • prisma generate 用于安装 Prisma Client
  • prisma db push 用于在开发情况中,将项目中表定义的改动,同步到数据库中
  • prisma db pull 用于在开发情况中,将数据库的表设定,同步到项目中
  • prisma migrate dev 用于在开发情况,将项目中表定义的改动,同步到数据库中,并在项目中生成一条操纵记录
  • prisma migrate deploy 用于在生产情况,将项目中表定义的改动,摆设到数据库中
其他下令
prisma migrate reset 用于重置数据库,会删除数据库中的所有数据并重新应用所有迁移,通常用于开发过程中快速重置数据库状态。
prisma\schema.prisma 中定义表
  1. model Blog {
  2.   id        Int   @id @default(autoincrement())
  3.   title     String
  4.   content   String
  5.   author    String?
  6.   createdAt DateTime @default(now())
  7.   updatedAt DateTime @updatedAt
  8.   @@map("blogs")
  9. }
复制代码


  • Blog 为表模型名称
  • @id 声明为 id 字段
  • @default 指定默认值
  • autoincrement() 自增
  • ? 可选
  • now() 当前时间
  • @updatedAt 更新时更新
  • @@map 自定义数据库表名称
实验脚本 prisma generate 安装 Prisma Client
实验脚本 prisma migrate dev 根据 prisma\schema.prisma 中定义的表在数据库中创建数据表

扣问输入一个镜像名称时,输入init 即可
实验成功后,在数据库中,可见表 blogs

创建 prisma 的 module
  1. nest g mo prisma
复制代码
创建 prisma 的 service
  1. nest g s prisma --no-spec
复制代码


  • --no-spec 为不生成测试文件
修改 src\prisma\prisma.service.ts 的内容为
  1. import { Injectable } from '@nestjs/common';
  2. import { PrismaClient } from '@prisma/client';
  3. @Injectable()
  4. export class PrismaService extends PrismaClient {}
复制代码
修改 src\prisma\prisma.module.ts 的内容为
  1. import { Module } from '@nestjs/common';
  2. import { PrismaService } from './prisma.service';
  3. @Global()  // 声明为全局可用
  4. @Module({
  5.   providers: [PrismaService],
  6.   exports: [PrismaService],
  7. })
  8. export class PrismaModule {}
复制代码
  创建表

  prisma\schema.prisma 中定义表
  1. model Diary {
  2.   id        Int      @id @default(autoincrement())
  3.   title     String
  4.   content   String
  5.   createdAt DateTime @default(now())
  6.   updatedAt DateTime @updatedAt
  7. }
复制代码
实验脚本 "prisma migrate dev" (此时不能启动项目,否则无法 Generated Prisma Client)
输入本次操纵的名称后回车,即可在数据库中看到创建好的表。

一对多

以 课程类型 vs 课程 为例


  • 每个课程类型对应多个课程
  • 每个课程只属于一种课程类型
prisma\schema.prisma
  1. // 课程--如 vue实战开发商城
  2. model Course {
  3.   id    Int     @id @default(autoincrement())
  4.   title String  @unique
  5.   // 链接到课程详情
  6.   url   String?
  7.   // 【一对一】课程 vs 课程类型(每个课程只属于一种课程类型)
  8.   typeId Int?
  9.   // 【外链】课程类型 --  本表的 typeId 外链 CourseType 表的 id
  10.   type CourseType? @relation(fields: [typeId], references: [id])
  11. }
  12. // 课程类型-- 如前端、后端
  13. model CourseType {
  14.   id   Int    @id @default(autoincrement())
  15.   name String @unique
  16.   // 【一对多】课程类型 vs 课程(每个课程类型有多个课程)
  17.   courses Course[]
  18. }
复制代码
  添加数据校验

  

安装相关依靠
  1. pnpm i --save class-validator class-transformer
复制代码
  1. pnpm i --save @nestjs/mapped-types
复制代码
全局导入使用 src\main.ts 的 app.listen 上添加
  1.   app.useGlobalPipes(
  2.     new ValidationPipe({
  3.       transform: true, // 全局启用 transform
  4.     }),
  5.   );
复制代码
  同一报错处理

  src\common\errHandle.ts
  1. import { HttpException } from '@nestjs/common';
  2. import { Prisma } from '@prisma/client';
  3. export function errHandler(error: any) {
  4.   let message = '未知错误';
  5.   let status = 500;
  6.   if (error instanceof Prisma.PrismaClientKnownRequestError) {
  7.     message = String(error.meta?.cause);
  8.     // 不同的错误代码可以有不同的处理逻辑
  9.     switch (error.code) {
  10.       case 'P2002':
  11.         message = '数据已存在,请勿重复添加';
  12.         status = 409;
  13.         break;
  14.       case 'P2025':
  15.         message = '操作的数据不存在';
  16.         break;
  17.       default:
  18.         message = error.message;
  19.     }
  20.   } else if (error instanceof Prisma.PrismaClientValidationError) {
  21.     // 数据验证错误,通常是由于输入的数据不满足模型的约束条件
  22.     message = error.message;
  23.     status = 400;
  24.     const regex = /Argument `(\w+)` is missing/;
  25.     const match = message.match(regex);
  26.     if (match) {
  27.       message = `缺失参数 ${match[1]}`;
  28.     }
  29.     const regex2 = /Argument `(\w+)`: Invalid value provided.(.*)\./;
  30.     const match2 = message.match(regex2);
  31.     if (match2) {
  32.       message = `非法参数 ${match2[1] + ':' + match2[2]} `;
  33.     }
  34.   } else if (error instanceof Prisma.PrismaClientInitializationError) {
  35.     // 初始化错误,通常是由于数据库连接问题
  36.     message = 'Prisma 初始化错误:' + error.message;
  37.   } else if (error.message.includes('Foreign key constraint failed')) {
  38.     message = '外键约束冲突,请检查关联数据';
  39.   }
  40.   throw new HttpException(message, status);
  41. }
复制代码
使用范例
  1. import { errHandler } from '@/common/errHandle';
复制代码
  1.   create_courseType(dto: Create_CourseType_Dto) {
  2.     return this.prisma.courseType.create({ data: dto }).catch((err) => {
  3.       errHandler(err);
  4.     });
  5.   }
复制代码
  新增数据

  新建文件 src\modules\diary\dto.create.ts
  1. import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
  2. export class CreateDto {
  3.   @IsString()
  4.   @IsNotEmpty()
  5.   title: string;
  6.   @IsString()
  7.   @IsOptional()
  8.   content: string;
  9. }
复制代码


  • @IsString() 校验是否为字符串
  • @IsNotEmpty() 校验不能为空
  • @IsOptional() 校验可选
src\modules\diary\diary.controller.ts
  1.   // 对应的接口为 /api/v1/dairy/create  在body中传入json数据
  2.   @Post('create')
  3.   create(@Body() dto: CreateDto) {
  4.     return this.service.create(dto);
  5.   }
复制代码
src\modules\diary\diary.service.ts
  1.   create(dto: CreateDto) {
  2.     return this.prisma.diary.create({ data: dto });
  3.   }
复制代码
批量新增

src\modules\course\dto.courseType.ts
  1. export class Create_CourseType_Dto {
  2.   @IsString()
  3.   @IsNotEmpty()
  4.   name: string;
  5. }
  6. export class Batch_Create_CourseType_Dto {
  7.   @ValidateNested()
  8.   @Type(() => Create_CourseType_Dto)
  9.   CourseTypeList: Create_CourseType_Dto[];
  10. }
复制代码
src\modules\course\course.controller.ts
  1.   // 对应的接口为 /api/v1/course/courseType/batch_create 在body中传入json数据
  2.   @Post('courseType/batch_create')
  3.   batch_create_courseType(@Body() dto: Batch_Create_CourseType_Dto) {
  4.     return this.service.batch_create_courseType(dto);
  5.   }
复制代码
src\modules\course\course.service.ts
  1.   batch_create_courseType(dto: Batch_Create_CourseType_Dto) {
  2.     return this.prisma.courseType
  3.       .createMany({ data: dto.CourseTypeList })
  4.       .catch((err) => {
  5.         errHandler(err);
  6.       });
  7.   }
复制代码
  删除数据

  src\modules\diary\diary.controller.ts
  1.   // 对应的接口为 /api/v1/dairy/del?id=目标id   id必传
  2.   @Delete('del')
  3.   delete(@Query('id', ParseIntPipe) id: number) {
  4.     return this.service.delete(id);
  5.   }
复制代码
src\modules\diary\diary.service.ts
  1.   delete(id: number) {
  2.     return this.prisma.diary
  3.       .delete({
  4.         where: { id },
  5.       })
  6.       .catch((err) => {
  7.         throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
  8.       });
  9.   }
复制代码
删除的数据不存在时,返回报错信息

删除成功时,返回删除的数据

   修改数据

  新建文件 src\modules\diary\dto.update.ts
  1. import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
  2. import { CreateDto } from './dto.create';
  3. import { OmitType } from '@nestjs/mapped-types';
  4. export class UpdateDto extends OmitType(CreateDto, ['title']) {
  5.   @IsNotEmpty()
  6.   @IsNumber()
  7.   id: number;
  8.   @IsString()
  9.   @IsNotEmpty()
  10.   @IsOptional()
  11.   title: string;
  12. }
复制代码


  • extends 继续 CreateDto ,免去重复誊写字段
  • OmitType 用于移除 CreateDto 中 title 的校验设置
  • @IsNumber() 用于校验是否为数字
src\modules\diary\diary.controller.ts
  1.   // 对应的接口为 /api/v1/dairy/update  在body中传入json数据,id必传
  2.   @Put('update')
  3.   update(@Body() dto: UpdateDto) {
  4.     return this.service.update(dto);
  5.   }
复制代码
src\modules\diary\diary.service.ts
  1.   update(dto: UpdateDto) {
  2.     return this.prisma.diary
  3.       .update({
  4.         where: { id: dto.id },
  5.         data: dto,
  6.       })
  7.       .catch((err) => {
  8.         throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
  9.       });
  10.   }
复制代码
更新的数据不存在时,返回报错信息

更新成功时,返回更新后的数据

   查询数据

  查询多条数据(含分页参数校验)

src\common\dto\pagination.dto.ts
  1. import { Type } from 'class-transformer';
  2. import { IsNumber } from 'class-validator';
  3. export class PaginationDto {
  4.   @IsNumber()
  5.   @Type(() => Number)
  6.   page: number = 1;
  7.   @IsNumber()
  8.   @Type(() => Number)
  9.   size: number = 10;
  10. }
复制代码
src\modules\course\dto.course.ts
  1. import { PaginationDto } from '@/common/dto/pagination.dto';
  2. export class GetList_Course_Dto extends PaginationDto {
  3.   @IsString()
  4.   @IsOptional()
  5.   title: string;
  6. }
复制代码
src\modules\course\course.controller.ts
  1.   // 对应的接口为 /api/v1/course/list
  2.   @Get('list')
  3.   async get_course_list(@Query() query: GetList_Course_Dto) {
  4.     let [data, total] = await this.service.get_course_list(query);
  5.     // 数据脱敏
  6.     data = data.map((item) => {
  7.       // 删除返回的id字段
  8.       delete item.id;
  9.       return item;
  10.     });
  11.     return {
  12.       data,
  13.       total,
  14.       'current-page': query.page,
  15.       'page-size': query.size,
  16.     };
  17.   }
复制代码
src\modules\course\course.service.ts
  1.   async get_course_list(
  2.     query: GetList_Course_Dto,
  3.   ): Promise<[Course[], number]> {
  4.     const skip = (query.page - 1) * query.size;
  5.     const take = query.size;
  6.     return await this.prisma.$transaction([
  7.       // 分页查询
  8.       this.prisma.course.findMany({
  9.         skip,
  10.         take,
  11.         where: {
  12.           // 模糊查询
  13.           title: {
  14.             contains: query.title,
  15.           },
  16.         },
  17.         orderBy: [
  18.           {
  19.             // 按创建时间倒序(最新创建的数据在最前面)
  20.             createdAt: 'desc',
  21.           },
  22.           {
  23.             // 按标题正序
  24.             title: 'asc',
  25.           },
  26.         ],
  27.       }),
  28.       // 查询总数
  29.       this.prisma.course.count(),
  30.     ]);
  31.   }
复制代码


  • $transaction 为事务:一连实验多个操纵,若操纵失败则会回滚。
根据外键 id 联查外表数据 include

如根据课程中的 typeId 查询课程类型信息
  1.       // 分页查询
  2.       this.prisma.course.findMany({
  3.         skip,
  4.         take,
  5.         include: {
  6.           // 包含课程类型信息
  7.           type: true,
  8.         },
  9.       }),
复制代码
查询效果如下:
  1. {
  2.     "data": [
  3.         {
  4.             "id": 1,
  5.             "title": "vue实战开发商城",
  6.             "url": null,
  7.             "typeId": 1,
  8.             "type": {
  9.                 "id": 1,
  10.                 "name": "前端"
  11.             }
  12.         },
  13.         {
  14.             "id": 2,
  15.             "title": "react 实战开发后台管理系统",
  16.             "url": null,
  17.             "typeId": 1,
  18.             "type": {
  19.                 "id": 1,
  20.                 "name": "前端"
  21.             }
  22.         },
  23.         {
  24.             "id": 3,
  25.             "title": "nextJS 实战开发后台管理系统",
  26.             "url": null,
  27.             "typeId": 2,
  28.             "type": {
  29.                 "id": 2,
  30.                 "name": "后端"
  31.             }
  32.         }
  33.     ],
  34.     "total": 3,
  35.     "current-page": 1,
  36.     "page-size": 10
  37. }
复制代码
排序 orderBy



  • 升序 asc
  • 降序 desc
  1.       this.prisma.course.findMany({
  2.         skip,
  3.         take,
  4.         orderBy: {
  5.           // 按照创建时间倒序(最新创建的数据在最前面)
  6.           createdAt: 'desc',
  7.         },
  8.       }),
复制代码
多字段排序
  1.         orderBy: [
  2.           {
  3.             // 按创建时间倒序(最新创建的数据在最前面)
  4.             createdAt: 'desc',
  5.           },
  6.           {
  7.             // 按标题正序
  8.             title: 'asc',
  9.           },
  10.         ],
复制代码
搜索条件 where

https://blog.csdn.net/weixin_41192489/article/details/145450412
   实战范例

  src\modules\diary\diary.controller.ts

  1. import {  Body,  Controller,  DefaultValuePipe,  Delete,  Get,  ParseIntPipe,  Post,  Put,  Query,} from '@nestjs/common';import { DiaryService } from './diary.service';import { CreateDto } from './dto.create';import { UpdateDto } from './dto.update';@Controller('diary')export class DiaryController {  constructor(private service: DiaryService) {}  // 对应的接口为 /api/v1/dairy/list  @Get('list')  async getList(    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,    @Query('size', new DefaultValuePipe(10), ParseIntPipe) size: number,  ) {    const [data, total] = await this.service.findMany(page, size);    return {      data,      total,      'current-page': page,      'page-size': size,    };  }  // 对应的接口为 /api/v1/dairy/create  在body中传入json数据
  2.   @Post('create')
  3.   create(@Body() dto: CreateDto) {
  4.     return this.service.create(dto);
  5.   }
  6.   // 对应的接口为 /api/v1/dairy/update  在body中传入json数据,id必传
  7.   @Put('update')
  8.   update(@Body() dto: UpdateDto) {
  9.     return this.service.update(dto);
  10.   }
  11.   // 对应的接口为 /api/v1/dairy/del?id=目标id   id必传
  12.   @Delete('del')
  13.   delete(@Query('id', ParseIntPipe) id: number) {
  14.     return this.service.delete(id);
  15.   }
  16. }
复制代码
src\modules\diary\diary.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { DiaryService } from './diary.service';
  3. import { DiaryController } from './diary.controller';
  4. @Module({
  5.   providers: [DiaryService],
  6.   controllers: [DiaryController],
  7. })
  8. export class DairyModule {}
复制代码
src\modules\diary\diary.service.ts

  1. import { Injectable, NotFoundException } from '@nestjs/common';import { Diary } from '@prisma/client';import { PrismaService } from 'src/prisma/prisma.service';import { CreateDto } from './dto.create';import { UpdateDto } from './dto.update';@Injectable()export class DiaryService {  constructor(private prisma: PrismaService) {}  async findMany(page: number, size: number): Promise<[Diary[], number]> {    const skip = (page - 1) * size;    const take = size;    return await this.prisma.$transaction([      // 分页查询      this.prisma.diary.findMany({        skip,        take,      }),      // 查询总数      this.prisma.diary.count(),    ]);  }  create(dto: CreateDto) {
  2.     return this.prisma.diary.create({ data: dto });
  3.   }
  4.   update(dto: UpdateDto) {
  5.     return this.prisma.diary
  6.       .update({
  7.         where: { id: dto.id },
  8.         data: dto,
  9.       })
  10.       .catch((err) => {
  11.         throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
  12.       });
  13.   }
  14.   delete(id: number) {
  15.     return this.prisma.diary
  16.       .delete({
  17.         where: { id },
  18.       })
  19.       .catch((err) => {
  20.         throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
  21.       });
  22.   }
  23. }
复制代码
src\modules\diary\dto.create.ts

  1. import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
  2. export class CreateDto {
  3.   @IsString()
  4.   @IsNotEmpty()
  5.   title: string;
  6.   @IsString()
  7.   @IsOptional()
  8.   content: string;
  9. }
复制代码
src\modules\diary\dto.update.ts

  1. import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
  2. import { CreateDto } from './dto.create';
  3. import { OmitType } from '@nestjs/mapped-types';
  4. export class UpdateDto extends OmitType(CreateDto, ['title']) {
  5.   @IsNotEmpty()
  6.   @IsNumber()
  7.   id: number;
  8.   @IsString()
  9.   @IsNotEmpty()
  10.   @IsOptional()
  11.   title: string;
  12. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

火影

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表