{T}

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 参数说明

参数类型说明
dataCreateInput[]要创建的数据数组
skipDuplicatesboolean跳过唯一约束冲突的数据

四、嵌套创建(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.ts

7.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() + skipDuplicates

9.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 装饰器!


十二、参考资料