单库 or 多库 ?
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
单库 | 成本低 开发部署快捷 | 性能受限 扩展性有限 故障风险 | 数据量适中 安全性需求不高 |
多库 | 可高度自定义 | 架构复杂,部署和维护成本较高 数据一致性问题 分布式事务处理较为复杂 | 自定义需求高 数据量大 安全性需求高 合规合法要求(物理隔离) |
ORM 库选型
- 需访问多种关系数据库,且数据库版本比较低,则选择 TypeORM
- 需访问的数据库版本比较高,优先选择 Prisma
- 仅需访问 MongoDB ,优先选择 Mongoose
集成 ORM 库 – prisma
安装 prisma
pnpm i -D prisma
vscode 安装官方插件
初始化 prisma
npx prisma init --datasource-provider postgresql
- 使用的 postgresql 数据库
- 初始化后,会生成文件
.env
和prisma\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 Clientprisma 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;
}