NestJS【实战】操作数据库(含集成 prisma,创建表,添加数据校验,数据的增删改查 CURD)

在这里插入图片描述

在这里插入图片描述

单库 or 多库 ?

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

ORM 库选型

在这里插入图片描述

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

集成 ORM 库 – prisma

安装 prisma

pnpm i -D prisma

vscode 安装官方插件

在这里插入图片描述
初始化 prisma

npx prisma init --datasource-provider postgresql
  • 使用的 postgresql 数据库
  • 初始化后,会生成文件 .envprisma\schema.prisma

修改 .env

DATABASE_URL="postgresql://数据库用户名:密码@数据库IP:数据库端口/数据库名称?schema=public"

最终范例如下:

DATABASE_URL="postgresql://testuser:test6666@localhost:5432/testdb?schema=public"

package.json 中新增脚本

    "prisma:generate": "prisma generate",
    "prisma:pull-DB": "prisma db pull",
    "prisma:push-DB": "prisma db push",
    "prisma:migrate-create": "prisma migrate dev",
    "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 中定义表

model Blog {
  id        Int   @id @default(autoincrement())
  title     String
  content   String
  author    String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  @@map("blogs")
}
  • Blog 为表模型名称
  • @id 声明为 id 字段
  • @default 指定默认值
  • autoincrement() 自增
  • ? 可选
  • now() 当前时间
  • @updatedAt 更新时更新
  • @@map 自定义数据库表名称

执行脚本 prisma generate 安装 Prisma Client

执行脚本 prisma migrate dev 根据 prisma\schema.prisma 中定义的表在数据库中创建数据表

在这里插入图片描述
询问输入一个镜像名称时,输入init 即可

执行成功后,在数据库中,可见表 blogs

在这里插入图片描述
创建 prisma 的 module

nest g mo prisma

创建 prisma 的 service

nest g s prisma --no-spec
  • --no-spec 为不生成测试文件

修改 src\prisma\prisma.service.ts 的内容为

import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient {}

修改 src\prisma\prisma.module.ts 的内容为

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()  // 声明为全局可用
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

创建表

prisma\schema.prisma 中定义表

model Diary {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

执行脚本 "prisma migrate dev" (此时不能启动项目,否则无法 Generated Prisma Client)

输入本次操作的名称后回车,即可在数据库中看到创建好的表。

在这里插入图片描述

一对多

课程类型 vs 课程 为例

  • 每个课程类型对应多个课程
  • 每个课程只属于一种课程类型

prisma\schema.prisma

// 课程--如 vue实战开发商城
model Course {
  id    Int     @id @default(autoincrement())
  title String  @unique
  // 链接到课程详情
  url   String?

  // 【一对一】课程 vs 课程类型(每个课程只属于一种课程类型)
  typeId Int?

  // 【外链】课程类型 --  本表的 typeId 外链 CourseType 表的 id
  type CourseType? @relation(fields: [typeId], references: [id])
}

// 课程类型-- 如前端、后端
model CourseType {
  id   Int    @id @default(autoincrement())
  name String @unique

  // 【一对多】课程类型 vs 课程(每个课程类型有多个课程)
  courses Course[]
}

添加数据校验

在这里插入图片描述
安装相关依赖

pnpm i --save class-validator class-transformer
pnpm i --save @nestjs/mapped-types

全局导入使用 src\main.ts 的 app.listen 上添加

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true, // 全局启用 transform
    }),
  );

统一报错处理

src\common\errHandle.ts

import { HttpException } from '@nestjs/common';

import { Prisma } from '@prisma/client';

export function errHandler(error: any) {
  let message = '未知错误';
  let status = 500;

  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    message = String(error.meta?.cause);
    // 不同的错误代码可以有不同的处理逻辑
    switch (error.code) {
      case 'P2002':
        message = '数据已存在,请勿重复添加';
        status = 409;
        break;
      case 'P2025':
        message = '操作的数据不存在';
        break;
      default:
        message = error.message;
    }
  } else if (error instanceof Prisma.PrismaClientValidationError) {
    // 数据验证错误,通常是由于输入的数据不满足模型的约束条件

    message = error.message;
    status = 400;

    const regex = /Argument `(\w+)` is missing/;
    const match = message.match(regex);

    if (match) {
      message = `缺失参数 ${match[1]}`;
    }

    const regex2 = /Argument `(\w+)`: Invalid value provided.(.*)\./;
    const match2 = message.match(regex2);

    if (match2) {
      message = `非法参数 ${match2[1] + ':' + match2[2]} `;
    }
  } else if (error instanceof Prisma.PrismaClientInitializationError) {
    // 初始化错误,通常是由于数据库连接问题
    message = 'Prisma 初始化错误:' + error.message;
  } else if (error.message.includes('Foreign key constraint failed')) {
    message = '外键约束冲突,请检查关联数据';
  }

  throw new HttpException(message, status);
}

使用范例

import { errHandler } from '@/common/errHandle';
  create_courseType(dto: Create_CourseType_Dto) {
    return this.prisma.courseType.create({ data: dto }).catch((err) => {
      errHandler(err);
    });
  }

新增数据

新建文件 src\modules\diary\dto.create.ts

import { IsString, IsOptional, IsNotEmpty } from 'class-validator';

export class CreateDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  @IsOptional()
  content: string;
}
  • @IsString() 校验是否为字符串
  • @IsNotEmpty() 校验不能为空
  • @IsOptional() 校验可选

src\modules\diary\diary.controller.ts

  // 对应的接口为 /api/v1/dairy/create  在body中传入json数据
  @Post('create')
  create(@Body() dto: CreateDto) {
    return this.service.create(dto);
  }

src\modules\diary\diary.service.ts

  create(dto: CreateDto) {
    return this.prisma.diary.create({ data: dto });
  }

批量新增

src\modules\course\dto.courseType.ts

export class Create_CourseType_Dto {
  @IsString()
  @IsNotEmpty()
  name: string;
}

export class Batch_Create_CourseType_Dto {
  @ValidateNested()
  @Type(() => Create_CourseType_Dto)
  CourseTypeList: Create_CourseType_Dto[];
}

src\modules\course\course.controller.ts

  // 对应的接口为 /api/v1/course/courseType/batch_create 在body中传入json数据
  @Post('courseType/batch_create')
  batch_create_courseType(@Body() dto: Batch_Create_CourseType_Dto) {
    return this.service.batch_create_courseType(dto);
  }

src\modules\course\course.service.ts

  batch_create_courseType(dto: Batch_Create_CourseType_Dto) {
    return this.prisma.courseType
      .createMany({ data: dto.CourseTypeList })
      .catch((err) => {
        errHandler(err);
      });
  }

删除数据

src\modules\diary\diary.controller.ts

  // 对应的接口为 /api/v1/dairy/del?id=目标id   id必传
  @Delete('del')
  delete(@Query('id', ParseIntPipe) id: number) {
    return this.service.delete(id);
  }

src\modules\diary\diary.service.ts

  delete(id: number) {
    return this.prisma.diary
      .delete({
        where: { id },
      })
      .catch((err) => {
        throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
      });
  }

删除的数据不存在时,返回报错信息

在这里插入图片描述
删除成功时,返回删除的数据

在这里插入图片描述

修改数据

新建文件 src\modules\diary\dto.update.ts

import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
import { CreateDto } from './dto.create';
import { OmitType } from '@nestjs/mapped-types';

export class UpdateDto extends OmitType(CreateDto, ['title']) {
  @IsNotEmpty()
  @IsNumber()
  id: number;

  @IsString()
  @IsNotEmpty()
  @IsOptional()
  title: string;
}
  • extends 继承 CreateDto ,免去重复书写字段
  • OmitType 用于移除 CreateDto 中 title 的校验设置
  • @IsNumber() 用于校验是否为数字

src\modules\diary\diary.controller.ts

  // 对应的接口为 /api/v1/dairy/update  在body中传入json数据,id必传
  @Put('update')
  update(@Body() dto: UpdateDto) {
    return this.service.update(dto);
  }

src\modules\diary\diary.service.ts

  update(dto: UpdateDto) {
    return this.prisma.diary
      .update({
        where: { id: dto.id },
        data: dto,
      })
      .catch((err) => {
        throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
      });
  }

更新的数据不存在时,返回报错信息
在这里插入图片描述
更新成功时,返回更新后的数据

在这里插入图片描述

查询数据

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

src\common\dto\pagination.dto.ts

import { Type } from 'class-transformer';
import { IsNumber } from 'class-validator';

export class PaginationDto {
  @IsNumber()
  @Type(() => Number)
  page: number = 1;

  @IsNumber()
  @Type(() => Number)
  size: number = 10;
}

src\modules\course\dto.course.ts

import { PaginationDto } from '@/common/dto/pagination.dto';

export class GetList_Course_Dto extends PaginationDto {
  @IsString()
  @IsOptional()
  title: string;
}

src\modules\course\course.controller.ts

  // 对应的接口为 /api/v1/course/list
  @Get('list')
  async get_course_list(@Query() query: GetList_Course_Dto) {
    let [data, total] = await this.service.get_course_list(query);

    // 数据脱敏
    data = data.map((item) => {
      // 删除返回的id字段
      delete item.id;
      return item;
    });

    return {
      data,
      total,
      'current-page': query.page,
      'page-size': query.size,
    };
  }

src\modules\course\course.service.ts

  async get_course_list(
    query: GetList_Course_Dto,
  ): Promise<[Course[], number]> {
    const skip = (query.page - 1) * query.size;
    const take = query.size;

    return await this.prisma.$transaction([
      // 分页查询
      this.prisma.course.findMany({
        skip,
        take,
        where: {
          // 模糊查询
          title: {
            contains: query.title,
          },
        },
        orderBy: [
          {
            // 按创建时间倒序(最新创建的数据在最前面)
            createdAt: 'desc',
          },
          {
            // 按标题正序
            title: 'asc',
          },
        ],
      }),
      // 查询总数
      this.prisma.course.count(),
    ]);
  }
  • $transaction 为事务:连续执行多个操作,若操作失败则会回滚。

根据外键 id 联查外表数据 include

如根据课程中的 typeId 查询课程类型信息

      // 分页查询
      this.prisma.course.findMany({
        skip,
        take,
        include: {
          // 包含课程类型信息
          type: true,
        },
      }),

查询结果如下:

{
    "data": [
        {
            "id": 1,
            "title": "vue实战开发商城",
            "url": null,
            "typeId": 1,
            "type": {
                "id": 1,
                "name": "前端"
            }
        },
        {
            "id": 2,
            "title": "react 实战开发后台管理系统",
            "url": null,
            "typeId": 1,
            "type": {
                "id": 1,
                "name": "前端"
            }
        },
        {
            "id": 3,
            "title": "nextJS 实战开发后台管理系统",
            "url": null,
            "typeId": 2,
            "type": {
                "id": 2,
                "name": "后端"
            }
        }
    ],
    "total": 3,
    "current-page": 1,
    "page-size": 10
}

排序 orderBy

  • 升序 asc
  • 降序 desc
      this.prisma.course.findMany({
        skip,
        take,
        orderBy: {
          // 按照创建时间倒序(最新创建的数据在最前面)
          createdAt: 'desc',
        },
      }),

多字段排序

        orderBy: [
          {
            // 按创建时间倒序(最新创建的数据在最前面)
            createdAt: 'desc',
          },
          {
            // 按标题正序
            title: 'asc',
          },
        ],

搜索条件 where

https://blog.csdn.net/weixin_41192489/article/details/145450412

实战范例

src\modules\diary\diary.controller.ts

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数据
  @Post('create')
  create(@Body() dto: CreateDto) {
    return this.service.create(dto);
  }

  // 对应的接口为 /api/v1/dairy/update  在body中传入json数据,id必传
  @Put('update')
  update(@Body() dto: UpdateDto) {
    return this.service.update(dto);
  }

  // 对应的接口为 /api/v1/dairy/del?id=目标id   id必传
  @Delete('del')
  delete(@Query('id', ParseIntPipe) id: number) {
    return this.service.delete(id);
  }
}

src\modules\diary\diary.module.ts

import { Module } from '@nestjs/common';
import { DiaryService } from './diary.service';
import { DiaryController } from './diary.controller';

@Module({
  providers: [DiaryService],
  controllers: [DiaryController],
})
export class DairyModule {}

src\modules\diary\diary.service.ts

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) {
    return this.prisma.diary.create({ data: dto });
  }

  update(dto: UpdateDto) {
    return this.prisma.diary
      .update({
        where: { id: dto.id },
        data: dto,
      })
      .catch((err) => {
        throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
      });
  }

  delete(id: number) {
    return this.prisma.diary
      .delete({
        where: { id },
      })
      .catch((err) => {
        throw new NotFoundException(err.meta ? err.meta?.cause : '未知错误');
      });
  }
}

src\modules\diary\dto.create.ts

import { IsString, IsOptional, IsNotEmpty } from 'class-validator';

export class CreateDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  @IsOptional()
  content: string;
}

src\modules\diary\dto.update.ts

import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';
import { CreateDto } from './dto.create';
import { OmitType } from '@nestjs/mapped-types';

export class UpdateDto extends OmitType(CreateDto, ['title']) {
  @IsNotEmpty()
  @IsNumber()
  id: number;

  @IsString()
  @IsNotEmpty()
  @IsOptional()
  title: string;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朝阳39

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值