NestJS创建课程与嵌套插入实战
学习目标:掌握课程服务创建逻辑、批量创建、嵌套创建(Nested Writes)、DTO 扩展和 Prisma 关联数据插入。
一、课程服务创建逻辑概述
1.1 课程相关数据模型
code
课程相关数据模型关系:
│
├── Course(课程)
│ ├── id: 课程 ID
│ ├── title: 课程标题
│ ├── authorId: 作者 ID(关联 User)
│ └── 关联关系
│ ├── tags: CourseTag[](多对多)
│ └── author: User(多对一)
│
├── CourseTag(课程标签关联表)
│ ├── id: 关联 ID
│ ├── courseId: 课程 ID
│ ├── tagId: 标签 ID
│ └── 关联关系
│ ├── course: Course
│ └── tag: Tag
│
└── Tag(标签)
├── id: 标签 ID
├── name: 标签名称
├── typeId: 类型 ID
└── 关联关系
├── type: Type
└── courses: CourseTag[]1.2 创建课程的核心逻辑
code
创建课程核心流程:
│
├── 第一步:创建课程基本信息
│ └── createCourse(createCourseDto)
│
├── 第二步:创建课程与标签的关联
│ ├── 单个创建:createCourseTag(dto)
│ └── 批量创建:createCourseTags(dto[])
│
├── 第三步:嵌套创建(推荐)
│ └── 创建课程时同时创建关联关系
│
└── 第四步:验证数据
├── 查询课程详情
└── 查询关联的标签二、Service 层实现
2.1 创建课程基本方法
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateCourseDto } from './dto/create-course.dto';
import { CreateCourseTagDto } from './dto/create-course-tag.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 创建课程
async createCourse(createCourseDto: CreateCourseDto) {
return this.prisma.courses.create({
data: createCourseDto,
});
}
// 创建课程标签关联关系(单个)
async createCourseTag(createCourseTagDto: CreateCourseTagDto) {
return this.prisma.courseTags.create({
data: createCourseTagDto,
});
}
}2.2 DTO 定义
CreateCourseDto
typescript
// src/modules/course/dto/create-course.dto.ts
import { IsString, IsNotEmpty, IsInt } from 'class-validator';
export class CreateCourseDto {
@IsString()
@IsNotEmpty()
title: string;
@IsInt()
authorId: number;
}CreateCourseTagDto
typescript
// src/modules/course/dto/create-course-tag.dto.ts
import { IsInt } from 'class-validator';
export class CreateCourseTagDto {
@IsInt()
courseId: number;
@IsInt()
tagId: number;
}2.3 Prisma Schema 定义
prisma
// prisma/schema.prisma
// 课程表
model Courses {
id Int @id @default(autoincrement())
title String
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id])
tags CourseTags[] // 一对多关系
@@map("courses")
}
// 课程标签关联表
model CourseTags {
id Int @id @default(autoincrement())
courseId Int
tagId Int
course Courses @relation(fields: [courseId], references: [id], onDelete: Cascade)
tag Tags @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([courseId, tagId]) // 唯一约束
@@map("course_tags")
}
// 标签表
model Tags {
id Int @id @default(autoincrement())
name String
typeId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
type Types @relation(fields: [typeId], references: [id])
courses CourseTags[] // 一对多关系
@@map("tags")
}三、批量创建实现
3.1 批量创建课程标签关联
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateCourseTagDto } from './dto/create-course-tag.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 创建课程标签关联(支持单个和批量)
async createCourseTags(dto: CreateCourseTagDto | CreateCourseTagDto[]) {
// 判断是否为数组
if (Array.isArray(dto)) {
// 批量创建
return this.prisma.courseTags.createMany({
data: dto,
skipDuplicates: true, // 跳过重复数据
});
} else {
// 单个创建
return this.prisma.courseTags.create({
data: dto,
});
}
}
}3.2 createMany 方法详解
typescript
// createMany 语法
await prisma.model.createMany({
data: [
{ field1: value1, field2: value2 },
{ field1: value3, field2: value4 },
],
skipDuplicates: true, // 可选:跳过重复数据
});
// 示例:批量创建课程标签关联
await prisma.courseTags.createMany({
data: [
{ courseId: 1, tagId: 1 },
{ courseId: 1, tagId: 2 },
{ courseId: 1, tagId: 3 },
],
skipDuplicates: true,
});3.3 createMany 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
data | CreateInput[] | 要创建的数据数组 |
skipDuplicates | boolean | 跳过唯一约束冲突的数据 |
四、嵌套创建(Nested Writes)
4.1 嵌套创建概述
code
嵌套创建定义:
│
├── 什么是嵌套创建
│ ├── 在创建主记录时同时创建关联记录
│ ├── 一次请求完成多表操作
│ └── 自动处理关联关系
│
├── 优势
│ ├── 减少数据库往返次数
│ ├── 保证数据一致性
│ ├── 简化代码逻辑
│ └── 原子性操作
│
└── 使用场景
├── 创建课程时同时创建标签关联
├── 创建用户时同时创建个人资料
└── 创建订单时同时创建订单项4.2 嵌套创建语法
一对一/多对一关系
typescript
// 创建 Post 并关联已存在的 User
await prisma.post.create({
data: {
title: '文章标题',
author: {
connect: { id: 1 }, // 连接已存在的用户
},
},
});
// 创建 Post 并创建新的 User
await prisma.post.create({
data: {
title: '文章标题',
author: {
create: { name: 'John', email: 'john@example.com' },
},
},
});一对多/多对多关系
typescript
// 创建 Course 并关联多个 Tag(单个)
await prisma.courses.create({
data: {
title: '课程标题',
authorId: 1,
tags: {
create: { tagId: 1 }, // 创建单个关联
},
},
});
// 创建 Course 并关联多个 Tag(批量)
await prisma.courses.create({
data: {
title: '课程标题',
authorId: 1,
tags: {
create: [
{ tagId: 1 },
{ tagId: 2 },
{ tagId: 3 },
], // 创建多个关联
},
},
});4.3 嵌套创建实战
Service 方法实现
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateCourseWithTagsDto } from './dto/create-course-with-tags.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 创建课程并关联标签(嵌套创建)
async createCourseWithTags(dto: CreateCourseWithTagsDto) {
const { tags, ...courseData } = dto;
return this.prisma.courses.create({
data: {
...courseData,
tags: {
create: tags, // 嵌套创建关联关系
},
},
include: {
tags: {
include: {
tag: true, // 包含标签详情
},
},
},
});
}
}DTO 定义
typescript
// src/modules/course/dto/create-course-with-tags.dto.ts
import { IsString, IsNotEmpty, IsInt, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreateCourseDto } from './create-course.dto';
// 标签关联项
class TagCreateItem {
@IsInt()
tagId: number;
}
// 创建课程并关联标签的 DTO
export class CreateCourseWithTagsDto extends CreateCourseDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => TagCreateItem)
tags: TagCreateItem[];
}4.4 嵌套创建执行流程
code
嵌套创建执行流程:
│
├── 第一步:创建主记录(Course)
│ └── INSERT INTO courses (title, authorId) VALUES ('课程标题', 1)
│
├── 第二步:获取主记录 ID
│ └── courseId = 8
│
├── 第三步:创建关联记录(CourseTag)
│ ├── INSERT INTO course_tags (courseId, tagId) VALUES (8, 24)
│ ├── INSERT INTO course_tags (courseId, tagId) VALUES (8, 25)
│ └── INSERT INTO course_tags (courseId, tagId) VALUES (8, 29)
│
└── 第四步:返回完整数据
└── 包含 Course 和关联的 Tags五、REPL 模式实战测试
5.1 创建课程
javascript
// 启动 REPL 模式
// $ pnpm start:repl
// 创建课程
await get(CourseService).createCourse({
title: 'Vue3 项目实战',
authorId: 1
})
// 输出示例
{
id: 4,
title: 'Vue3 项目实战',
authorId: 1,
createdAt: 2024-01-01T00:00:00.000Z,
updatedAt: 2024-01-01T00:00:00.000Z
}5.2 创建课程标签关联(单个)
javascript
// 创建课程标签关联
await get(CourseService).createCourseTag({
courseId: 4,
tagId: 23
})
// 输出示例
{
id: 1,
courseId: 4,
tagId: 23
}
// 创建更多关联
await get(CourseService).createCourseTag({ courseId: 4, tagId: 24 })
await get(CourseService).createCourseTag({ courseId: 3, tagId: 25 })5.3 创建课程标签关联(批量)
javascript
// 批量创建课程标签关联
await get(CourseService).createCourseTags([
{ courseId: 4, tagId: 26 },
{ courseId: 4, tagId: 27 },
{ courseId: 4, tagId: 28 }
])
// 输出示例
{ count: 3 } // 成功创建 3 条记录5.4 嵌套创建测试
javascript
// 创建课程并关联标签(嵌套创建)
await get(CourseService).createCourseWithTags({
title: '嵌套写入测试课程',
authorId: 1,
tags: [
{ tagId: 24 },
{ tagId: 25 },
{ tagId: 29 },
{ tagId: 39 }
]
})
// 输出示例
{
id: 7,
title: '嵌套写入测试课程',
authorId: 1,
createdAt: 2024-01-01T00:00:00.000Z,
updatedAt: 2024-01-01T00:00:00.000Z,
tags: [
{ id: 1, courseId: 7, tagId: 24, tag: { id: 24, name: 'Vue3' } },
{ id: 2, courseId: 7, tagId: 25, tag: { id: 25, name: 'React' } },
{ id: 3, courseId: 7, tagId: 29, tag: { id: 29, name: 'TypeScript' } },
{ id: 4, courseId: 7, tagId: 39, tag: { id: 39, name: 'Node.js' } }
]
}5.5 验证嵌套创建结果
javascript
// 验证课程是否创建成功
const course = await get(CourseService).getCourseById(7)
console.log('课程标题:', course.title)
// 输出:课程标题:嵌套写入测试课程
// 验证关联关系是否创建成功
const courseTags = await get(CourseService).getCourseTags(7)
console.log('关联的标签数量:', courseTags.length)
// 输出:关联的标签数量:4
courseTags.forEach((ct, index) => {
console.log(`${index + 1}. Tag ID: ${ct.tagId}, Tag Name: ${ct.tag.name}`)
})
// 输出:
// 1. Tag ID: 24, Tag Name: Vue3
// 2. Tag ID: 25, Tag Name: React
// 3. Tag ID: 29, Tag Name: TypeScript
// 4. Tag ID: 39, Tag Name: Node.js六、嵌套创建 vs 分步创建对比
6.1 方式对比
方式一:分步创建
typescript
// 分步创建(不推荐)
// 1. 创建课程
const course = await prisma.courses.create({
data: { title: '课程标题', authorId: 1 },
});
// 2. 创建标签关联(需要多次请求)
await prisma.courseTags.create({ data: { courseId: course.id, tagId: 1 } });
await prisma.courseTags.create({ data: { courseId: course.id, tagId: 2 } });
await prisma.courseTags.create({ data: { courseId: course.id, tagId: 3 } });方式二:嵌套创建(推荐)
typescript
// 嵌套创建(推荐)
// 一次请求完成所有操作
const course = await prisma.courses.create({
data: {
title: '课程标题',
authorId: 1,
tags: {
create: [
{ tagId: 1 },
{ tagId: 2 },
{ tagId: 3 },
],
},
},
include: {
tags: true,
},
});6.2 对比总结
| 维度 | 分步创建 | 嵌套创建 |
|---|---|---|
| 数据库请求 | 多次请求 | 一次请求 |
| 性能 | 较低 | 较高 |
| 事务性 | 需要手动管理 | 自动事务 |
| 代码复杂度 | 高 | 低 |
| 数据一致性 | 需要额外处理 | 自动保证 |
| 推荐度 |
七、完整实战示例
7.1 项目结构
code
src/modules/course/
├── course.module.ts
├── course.service.ts
├── course.controller.ts
└── dto/
├── create-course.dto.ts
├── create-course-tag.dto.ts
└── create-course-with-tags.dto.ts7.2 完整 Service 实现
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import { CreateCourseDto } from './dto/create-course.dto';
import { CreateCourseTagDto } from './dto/create-course-tag.dto';
import { CreateCourseWithTagsDto } from './dto/create-course-with-tags.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 创建课程
async createCourse(createCourseDto: CreateCourseDto) {
return this.prisma.courses.create({
data: createCourseDto,
});
}
// 创建课程标签关联(支持单个和批量)
async createCourseTags(dto: CreateCourseTagDto | CreateCourseTagDto[]) {
if (Array.isArray(dto)) {
// 批量创建
return this.prisma.courseTags.createMany({
data: dto,
skipDuplicates: true,
});
} else {
// 单个创建
return this.prisma.courseTags.create({
data: dto,
});
}
}
// 创建课程并关联标签(嵌套创建)
async createCourseWithTags(dto: CreateCourseWithTagsDto) {
const { tags, ...courseData } = dto;
return this.prisma.courses.create({
data: {
...courseData,
tags: {
create: tags,
},
},
include: {
tags: {
include: {
tag: true,
},
},
},
});
}
// 查询课程详情
async getCourseById(id: number) {
return this.prisma.courses.findUnique({
where: { id },
include: {
author: true,
tags: {
include: {
tag: true,
},
},
},
});
}
// 查询课程的所有标签
async getCourseTags(courseId: number) {
return this.prisma.courseTags.findMany({
where: { courseId },
include: {
tag: true,
},
});
}
}7.3 Controller 实现
typescript
// src/modules/course/course.controller.ts
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
import { CourseService } from './course.service';
import { CreateCourseWithTagsDto } from './dto/create-course-with-tags.dto';
@Controller('courses')
export class CourseController {
constructor(private courseService: CourseService) {}
// 创建课程并关联标签
@Post()
async create(@Body() createCourseWithTagsDto: CreateCourseWithTagsDto) {
return this.courseService.createCourseWithTags(createCourseWithTagsDto);
}
// 查询课程详情
@Get(':id')
async findOne(@Param('id') id: number) {
return this.courseService.getCourseById(id);
}
}7.4 Module 配置
typescript
// src/modules/course/course.module.ts
import { Module } from '@nestjs/common';
import { CourseController } from './course.controller';
import { CourseService } from './course.service';
import { PrismaModule } from '@/prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [CourseController],
providers: [CourseService],
exports: [CourseService],
})
export class CourseModule {}八、常见问题与解决方案
8.1 外键约束错误
问题:嵌套创建时提示外键约束错误。
javascript
// 错误信息
Foreign key constraint failed on the field: `CourseTags_tagId_fkey`
// 原因:tagId 不存在
await get(CourseService).createCourseWithTags({
title: '测试课程',
authorId: 1,
tags: [
{ tagId: 999 } // tagId 999 不存在
]
})解决方案:
javascript
// 确保 tagId 存在
// 1. 先查询所有 tag
const tags = await get(CourseService).getTags()
console.log(tags.map(t => ({ id: t.id, name: t.name })))
// 2. 使用存在的 tagId
await get(CourseService).createCourseWithTags({
title: '测试课程',
authorId: 1,
tags: [
{ tagId: 24 }, // 使用存在的 tagId
{ tagId: 25 }
]
})8.2 唯一约束冲突
问题:批量创建时出现唯一约束冲突。
javascript
// 错误信息
Unique constraint failed on the fields: (`courseId`,`tagId`)
// 原因:重复插入相同的关联关系
await get(CourseService).createCourseTags([
{ courseId: 1, tagId: 1 },
{ courseId: 1, tagId: 1 } // 重复
])解决方案:
typescript
// 使用 skipDuplicates 跳过重复数据
await this.prisma.courseTags.createMany({
data: dto,
skipDuplicates: true, // 跳过重复数据
});8.3 DTO 校验不生效
问题:嵌套对象的校验不生效。
解决方案:
typescript
// 使用 ValidateNested 和 Type 装饰器
import { ValidateNested, IsArray } from 'class-validator';
import { Type } from 'class-transformer';
class TagCreateItem {
@IsInt()
tagId: number;
}
export class CreateCourseWithTagsDto extends CreateCourseDto {
@IsArray()
@ValidateNested({ each: true }) // 嵌套校验
@Type(() => TagCreateItem) // 类型转换
tags: TagCreateItem[];
}九、最佳实践总结
9.1 创建方式选择
code
创建方式选择指南:
│
├── 简单场景
│ ├── 只创建主记录 → 直接使用 create()
│ └── 单个关联 → 分步创建
│
├── 复杂场景
│ ├── 主记录 + 多个关联 → 嵌套创建
│ └── 需要事务保证 → 嵌套创建
│
└── 批量场景
├── 批量创建主记录 → createMany()
└── 批量创建关联 → createMany() + skipDuplicates9.2 性能优化建议
code
性能优化建议:
│
├── 1. 减少数据库往返
│ ├── 使用嵌套创建代替分步创建
│ └── 使用 createMany 代替循环 create
│
├── 2. 批量操作
│ ├── 使用 createMany 批量插入
│ └── 使用 skipDuplicates 避免冲突
│
├── 3. 索引优化
│ ├── 外键字段添加索引
│ └── 唯一约束字段添加索引
│
└── 4. 事务处理
├── 嵌套创建自动事务
└── 复杂场景使用 $transaction十、命令速查表
10.1 Prisma 创建命令速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 创建单个记录 | prisma.model.create({ data }) | 创建一条记录 |
| 批量创建 | prisma.model.createMany({ data: [] }) | 批量创建多条记录 |
| 嵌套创建 | create({ data: { relation: { create: {} } } }) | 创建时同时创建关联 |
| 关联已存在记录 | create({ data: { relation: { connect: {} } } }) | 关联已存在的记录 |
| 跳过重复 | createMany({ skipDuplicates: true }) | 跳过唯一约束冲突 |
10.2 REPL 常用命令速查
| 操作 | 命令 |
|---|---|
| 创建课程 | await get(CourseService).createCourse({ title, authorId }) |
| 创建课程标签关联 | await get(CourseService).createCourseTag({ courseId, tagId }) |
| 批量创建关联 | await get(CourseService).createCourseTags([...]) |
| 嵌套创建 | await get(CourseService).createCourseWithTags({ title, authorId, tags: [...] }) |
十一、学习要点总结
11.1 核心概念总结
code
NestJS 创建课程核心要点:
│
├── 基本创建
│ ├── createCourse:创建课程
│ ├── createCourseTag:创建单个关联
│ └── createCourseTags:批量创建关联
│
├── 嵌套创建
│ ├── 定义:创建主记录时同时创建关联
│ ├── 语法:create({ data: { relation: { create: [] } } })
│ ├── 优势:一次请求、原子操作
│ └── 推荐:复杂关联场景使用
│
├── 批量创建
│ ├── 方法:createMany()
│ ├── 参数:data(数组)、skipDuplicates
│ └── 场景:批量插入关联数据
│
└── DTO 扩展
├── 继承基础 DTO
├── 添加关联字段
└── 使用 ValidateNested 校验11.2 学习路径规划
code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解嵌套创建的概念
│ ├── 理解批量创建的区别
│ └── 理解 DTO 扩展方式
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现基本创建
│ ├── 实现嵌套创建
│ └── 实现批量创建
│
└── 第三阶段:深入应用(持续)
├── 优化性能
├── 处理复杂关联
└── 完善数据校验11.3 重要提示
重要提示:嵌套创建是 Prisma 提供的强大功能,可以在创建主记录时同时创建关联记录,减少数据库往返次数,保证数据一致性。推荐在创建课程时同时创建标签关联,而不是分步创建。批量创建时使用
skipDuplicates避免唯一约束冲突,嵌套对象的校验需要使用@ValidateNested和@Type装饰器!