NestJS多表联合查询与数据排序实战
NestJS多表联合查询与数据排序实战
学习目标:掌握 Prisma 多表联合查询、嵌套 include 使用、查询结果排序、数据结构化处理。
一、多表联合查询场景
1.1 业务场景描述
code
课程推荐系统查询场景:
│
├── 页面展示需求
│ ├── 推荐内容(分类)
│ ├── 每日一课(分类)
│ ├── 精品微课(分类)
│ └── 学习计划(分类)
│
├── 每个分类包含
│ ├── 分类名称
│ ├── 课程列表
│ │ ├── 课程图片
│ │ ├── 课程标题
│ │ ├── 作者信息
│ │ ├── 价格
│ │ └── 学习人数
│ └── 标签列表
│
└── 查询需求
├── 查询所有分类
├── 每个分类包含的标签
├── 每个标签关联的课程
└── 按分类顺序排列1.2 数据表关系
code
数据表关系链路:
│
├── CourseTypes(课程分类)
│ ├── id: 分类 ID
│ ├── name: 分类名称
│ ├── order: 排序字段
│ └── 关联:tags[]
│
├── Tags(标签)
│ ├── id: 标签 ID
│ ├── name: 标签名称
│ ├── typeId: 分类 ID
│ └── 关联:courses[]
│
├── CourseTags(课程标签关联表)
│ ├── id: 关联 ID
│ ├── courseId: 课程 ID
│ ├── tagId: 标签 ID
│ └── 关联:course, tag
│
└── Courses(课程)
├── id: 课程 ID
├── title: 课程标题
├── authorId: 作者 ID
└── 关联:tags[], author1.3 查询链路分析
code
完整查询链路:
│
├── 第一步:查询分类表
│ └── SELECT * FROM course_types
│
├── 第二步:查询关联的标签
│ └── SELECT * FROM tags WHERE typeId IN (分类 IDs)
│
├── 第三步:查询标签关联的课程
│ └── SELECT * FROM course_tags WHERE tagId IN (标签 IDs)
│
├── 第四步:查询课程详情
│ └── SELECT * FROM courses WHERE id IN (课程 IDs)
│
└── 第五步:查询作者信息
└── SELECT * FROM users WHERE id IN (作者 IDs)二、Prisma 嵌套查询实现
2.1 第一层:查询分类和标签
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 查询课程分类(包含标签)
async getCoursesByType() {
return this.prisma.courseTypes.findMany({
include: {
tags: true, // 包含关联的标签
},
});
}
}查询结果示例:
json
[
{
"id": 9,
"name": "推荐内容",
"order": 100,
"tags": [
{ "id": 1, "name": "Vue3 项目实战", "typeId": 9 },
{ "id": 2, "name": "React18 新特性", "typeId": 9 }
]
},
{
"id": 10,
"name": "每日一课",
"order": 200,
"tags": [
{ "id": 3, "name": "TypeScript 进阶", "typeId": 10 }
]
}
]2.2 第二层:查询标签关联的课程
typescript
// src/modules/course/course.service.ts
async getCoursesByType() {
return this.prisma.courseTypes.findMany({
include: {
tags: {
include: {
courses: true, // 包含标签关联的课程
},
},
},
});
}查询结果示例:
json
[
{
"id": 9,
"name": "推荐内容",
"tags": [
{
"id": 1,
"name": "Vue3 项目实战",
"courses": [
{ "id": 1, "courseId": 1, "tagId": 1 },
{ "id": 2, "courseId": 2, "tagId": 1 }
]
}
]
}
]2.3 第三层:查询课程详情
typescript
// src/modules/course/course.service.ts
async getCoursesByType() {
return this.prisma.courseTypes.findMany({
include: {
tags: {
include: {
courses: {
include: {
course: true, // 包含课程详情
tag: true, // 包含标签详情(可选)
},
},
},
},
},
});
}查询结果示例:
json
[
{
"id": 9,
"name": "推荐内容",
"order": 100,
"tags": [
{
"id": 1,
"name": "Vue3 项目实战",
"typeId": 9,
"courses": [
{
"id": 1,
"courseId": 1,
"tagId": 1,
"course": {
"id": 1,
"title": "Vue3 完全指南",
"authorId": 1,
"price": 99,
"students": 1000
},
"tag": {
"id": 1,
"name": "Vue3 项目实战"
}
}
]
}
]
}
]2.4 优化后的查询(移除重复字段)
typescript
// src/modules/course/course.service.ts
async getCoursesByType() {
return this.prisma.courseTypes.findMany({
include: {
tags: {
include: {
courses: {
include: {
course: true, // 包含课程详情
// tag: true, // 移除,因为已经从 tag 关联过来
},
},
},
},
},
});
}三、Controller 层实现
3.1 创建查询接口
typescript
// src/modules/course/course.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CourseService } from './course.service';
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
// 查询课程分类及关联的课程
@Get()
async getCoursesByType() {
return this.courseService.getCoursesByType();
}
}3.2 完整 Controller 实现
typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Post, Body, 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 readonly courseService: CourseService) {}
// 查询课程分类及关联的课程
@Get()
async getCoursesByType() {
return this.courseService.getCoursesByType();
}
// 创建课程
@Post()
async create(@Body() dto: CreateCourseWithTagsDto) {
if (dto.tags && dto.tags.length > 0) {
const tags = {
create: dto.tags.map(tagId => ({ tagId })),
};
return this.courseService.createCourseWithTags({
...dto,
tags,
});
} else {
return this.courseService.createCourse(dto);
}
}
// 查询课程详情
@Get(':id')
async findOne(@Param('id') id: number) {
return this.courseService.getCourseById(id);
}
}四、查询结果排序
4.1 基础排序语法
typescript
// 按 order 字段升序排列
await prisma.courseTypes.findMany({
orderBy: {
order: 'asc', // 升序
},
});
// 按 order 字段降序排列
await prisma.courseTypes.findMany({
orderBy: {
order: 'desc', // 降序
},
});4.2 嵌套查询中的排序
typescript
// src/modules/course/course.service.ts
async getCoursesByType() {
return this.prisma.courseTypes.findMany({
orderBy: {
order: 'asc', // 按分类顺序排列
},
include: {
tags: {
include: {
courses: {
include: {
course: true,
},
},
},
},
},
});
}4.3 多字段排序
typescript
// 先按 order 排序,再按 name 排序
await prisma.courseTypes.findMany({
orderBy: [
{ order: 'asc' },
{ name: 'asc' },
],
});
// 嵌套字段排序
await prisma.courseTypes.findMany({
orderBy: {
order: 'asc',
},
include: {
tags: {
orderBy: {
name: 'asc', // 标签按名称排序
},
include: {
courses: {
include: {
course: {
orderBy: {
createdAt: 'desc', // 课程按创建时间倒序
},
},
},
},
},
},
},
});4.4 排序验证
code
排序验证流程:
│
├── 初始数据
│ ├── 推荐内容:order = 100
│ ├── 每日一课:order = 200
│ └── 精品微课:order = 300
│
├── 查询结果(升序)
│ ├── 推荐内容(order = 100)
│ ├── 每日一课(order = 200)
│ └── 精品微课(order = 300)
│
├── 修改数据
│ └── 每日一课:order = 900
│
└── 查询结果(升序)
├── 推荐内容(order = 100)
├── 精品微课(order = 300)
└── 每日一课(order = 900)五、Postman 测试示例
5.1 测试一:查询分类和标签
bash
# 请求
GET /courses
# 响应
[
{
"id": 9,
"name": "推荐内容",
"order": 100,
"tags": [
{ "id": 1, "name": "Vue3 项目实战", "typeId": 9 },
{ "id": 2, "name": "React18 新特性", "typeId": 9 }
]
},
{
"id": 10,
"name": "每日一课",
"order": 200,
"tags": [
{ "id": 3, "name": "TypeScript 进阶", "typeId": 10 }
]
}
]5.2 测试二:查询标签关联的课程
bash
# 请求
GET /courses
# 响应
[
{
"id": 9,
"name": "推荐内容",
"tags": [
{
"id": 1,
"name": "Vue3 项目实战",
"courses": [
{
"id": 1,
"courseId": 1,
"tagId": 1,
"course": {
"id": 1,
"title": "Vue3 完全指南",
"authorId": 1,
"price": 99,
"students": 1000
}
}
]
}
]
}
]5.3 测试三:验证排序
bash
# 初始查询
GET /courses
# 结果:推荐内容、每日一课、精品微课
# 修改数据库:每日一课 order = 900
# 再次查询
GET /courses
# 结果:推荐内容、精品微课、每日一课六、数据结构化处理
6.1 前端所需数据结构
typescript
// 前端期望的数据结构
interface FormattedType {
id: number;
name: string;
courses: {
id: number;
title: string;
author: string;
price: number;
students: number;
cover: string;
}[];
}
// 示例
[
{
"id": 9,
"name": "推荐内容",
"courses": [
{
"id": 1,
"title": "Vue3 完全指南",
"author": "张三",
"price": 99,
"students": 1000,
"cover": "https://example.com/cover.jpg"
}
]
}
]6.2 Service 层数据结构化
typescript
// src/modules/course/course.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 查询并结构化数据
async getFormattedCoursesByType() {
const types = await this.prisma.courseTypes.findMany({
orderBy: {
order: 'asc',
},
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true, // 包含作者信息
},
},
},
},
},
},
},
});
// 数据结构化处理
return types.map(type => ({
id: type.id,
name: type.name,
order: type.order,
courses: this.extractCourses(type.tags),
}));
}
// 提取课程列表(去重)
private extractCourses(tags: any[]) {
const courseMap = new Map();
tags.forEach(tag => {
tag.courses.forEach(ct => {
if (!courseMap.has(ct.course.id)) {
courseMap.set(ct.course.id, {
id: ct.course.id,
title: ct.course.title,
author: ct.course.author?.name || '未知作者',
price: ct.course.price,
students: ct.course.students,
cover: ct.course.cover,
});
}
});
});
return Array.from(courseMap.values());
}
}6.3 Controller 层调用
typescript
// src/modules/course/course.controller.ts
@Controller('courses')
export class CourseController {
constructor(private readonly courseService: CourseService) {}
// 查询结构化的课程数据
@Get('formatted')
async getFormattedCourses() {
return this.courseService.getFormattedCoursesByType();
}
// 查询原始数据
@Get()
async getCoursesByType() {
return this.courseService.getCoursesByType();
}
}七、完整实战示例
7.1 项目结构
code
src/modules/course/
├── course.module.ts
├── course.service.ts
├── course.controller.ts
└── dto/
├── create-course.dto.ts
├── create-course-with-tags.dto.ts
└── formatted-course.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 { CreateCourseWithTagsInterface } from './dto/create-course-with-tags.dto';
@Injectable()
export class CourseService {
constructor(private prisma: PrismaService) {}
// 创建课程
async createCourse(dto: CreateCourseDto) {
return this.prisma.courses.create({
data: dto,
});
}
// 创建课程并关联标签
async createCourseWithTags(dto: CreateCourseWithTagsInterface) {
const { tags, ...courseData } = dto;
return this.prisma.courses.create({
data: {
...courseData,
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 getCoursesByType() {
return this.prisma.courseTypes.findMany({
orderBy: {
order: 'asc',
},
include: {
tags: {
include: {
courses: {
include: {
course: true,
},
},
},
},
},
});
}
// 查询课程分类(结构化数据)
async getFormattedCoursesByType() {
const types = await this.prisma.courseTypes.findMany({
orderBy: {
order: 'asc',
},
include: {
tags: {
include: {
courses: {
include: {
course: {
include: {
author: true,
},
},
},
},
},
},
},
});
return types.map(type => ({
id: type.id,
name: type.name,
order: type.order,
courses: this.extractCourses(type.tags),
}));
}
// 提取课程列表
private extractCourses(tags: any[]) {
const courseMap = new Map();
tags.forEach(tag => {
tag.courses.forEach(ct => {
if (!courseMap.has(ct.course.id)) {
courseMap.set(ct.course.id, {
id: ct.course.id,
title: ct.course.title,
author: ct.course.author?.name || '未知作者',
price: ct.course.price,
students: ct.course.students,
cover: ct.course.cover,
createdAt: ct.course.createdAt,
});
}
});
});
return Array.from(courseMap.values());
}
}7.3 完整 Controller 实现
typescript
// src/modules/course/course.controller.ts
import { Controller, Get, Post, Body, 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 readonly courseService: CourseService) {}
// 查询结构化的课程数据
@Get('formatted')
async getFormattedCourses() {
return this.courseService.getFormattedCoursesByType();
}
// 查询原始课程数据
@Get()
async getCoursesByType() {
return this.courseService.getCoursesByType();
}
// 查询课程详情
@Get(':id')
async findOne(@Param('id') id: number) {
return this.courseService.getCourseById(id);
}
// 创建课程
@Post()
async create(@Body() dto: CreateCourseWithTagsDto) {
if (dto.tags && dto.tags.length > 0) {
const tags = {
create: dto.tags.map(tagId => ({ tagId })),
};
return this.courseService.createCourseWithTags({
...dto,
tags,
});
} else {
return this.courseService.createCourse(dto);
}
}
}八、查询优化建议
8.1 查询优化策略
code
多表联合查询优化策略:
│
├── 1. 选择性查询字段
│ ├── 使用 select 代替 include
│ ├── 只查询需要的字段
│ └── 减少数据传输量
│
├── 2. 分页查询
│ ├── 使用 skip 和 take
│ ├── 避免一次性查询大量数据
│ └── 减少内存占用
│
├── 3. 索引优化
│ ├── 外键字段添加索引
│ ├── 排序字段添加索引
│ └── 频繁查询字段添加索引
│
├── 4. 缓存策略
│ ├── 使用 Redis 缓存查询结果
│ ├── 设置合理的过期时间
│ └── 数据更新时清除缓存
│
└── 5. 查询层级控制
├── 避免过深的嵌套查询
├── 深度不超过 3 层
└── 复杂查询考虑分步处理8.2 使用 select 优化查询
typescript
// 使用 select 只查询需要的字段
async getCoursesByTypeOptimized() {
return this.prisma.courseTypes.findMany({
orderBy: {
order: 'asc',
},
select: {
id: true,
name: true,
order: true,
tags: {
select: {
id: true,
name: true,
courses: {
select: {
course: {
select: {
id: true,
title: true,
price: true,
students: true,
},
},
},
},
},
},
},
});
}8.3 分页查询实现
typescript
// 分页查询
async getCoursesByTypePaginated(page: number = 1, size: number = 10) {
const skip = (page - 1) * size;
const [data, total] = await this.prisma.$transaction([
this.prisma.courseTypes.findMany({
skip,
take: size,
orderBy: {
order: 'asc',
},
include: {
tags: {
include: {
courses: {
include: {
course: true,
},
},
},
},
},
}),
this.prisma.courseTypes.count(),
]);
return {
data,
total,
page,
size,
totalPages: Math.ceil(total / size),
};
}九、常见问题与解决方案
9.1 查询结果数据冗余
问题:嵌套查询返回大量重复数据。
解决方案:
typescript
// 问题:返回重复的 tag 数据
include: {
courses: {
include: {
course: true,
tag: true, // 重复,因为已经从 tag 关联过来
},
},
}
// 解决:移除重复字段
include: {
courses: {
include: {
course: true,
// tag: true, // 移除
},
},
}9.2 排序不生效
问题:orderBy 排序不生效。
原因:orderBy 位置错误。
解决方案:
typescript
// 错误:orderBy 在 include 里面
include: {
tags: {
orderBy: { name: 'asc' }, // 错误位置
},
}
orderBy: { order: 'asc' } // 正确位置
// 正确:orderBy 在 findMany 同级
await prisma.courseTypes.findMany({
orderBy: { order: 'asc' }, // 正确位置
include: {
tags: true,
},
});9.3 数据结构化性能问题
问题:大数据量下数据结构化处理慢。
解决方案:
typescript
// 问题:使用 filter 嵌套循环
const courses = [];
tags.forEach(tag => {
tag.courses.forEach(ct => {
if (!courses.find(c => c.id === ct.course.id)) {
courses.push(ct.course);
}
});
});
// 解决:使用 Map 去重
const courseMap = new Map();
tags.forEach(tag => {
tag.courses.forEach(ct => {
if (!courseMap.has(ct.course.id)) {
courseMap.set(ct.course.id, ct.course);
}
});
});
const courses = Array.from(courseMap.values());十、最佳实践总结
10.1 查询最佳实践
code
多表联合查询最佳实践:
│
├── 1. 合理使用 include
│ ├── 只包含需要的关联数据
│ ├── 避免过深的嵌套层级
│ └── 移除重复的关联字段
│
├── 2. 排序位置正确
│ ├── orderBy 在 findMany 同级
│ ├── 嵌套排序在 include 内部
│ └── 多字段排序使用数组
│
├── 3. 数据结构化
│ ├── 在 Service 层处理
│ ├── 使用 Map 去重
│ └── 返回前端友好的格式
│
├── 4. 性能优化
│ ├── 使用 select 选择字段
│ ├── 使用分页查询
│ └── 添加必要的索引
│
└── 5. 缓存策略
├── 查询结果缓存
├── 设置合理的过期时间
└── 数据更新时清除缓存10.2 数据结构化最佳实践
code
数据结构化最佳实践:
│
├── 1. 明确前端需求
│ ├── 了解前端期望的数据结构
│ ├── 减少前端数据处理
│ └── 提高开发效率
│
├── 2. Service 层处理
│ ├── 保持 Controller 层简洁
│ ├── 业务逻辑集中在 Service
│ └── 便于测试和维护
│
├── 3. 数据去重
│ ├── 使用 Map 数据结构
│ ├── 避免重复数据
│ └── 提高查询效率
│
└── 4. 类型安全
├── 定义清晰的 DTO
├── 使用 TypeScript 类型检查
└── 避免运行时错误十一、命令速查表
11.1 Prisma 查询命令速查
| 操作 | 命令 | 说明 |
|---|---|---|
| 基础查询 | findMany() | 查询多条记录 |
| 条件查询 | findMany({ where }) | 条件查询 |
| 排序 | findMany({ orderBy }) | 排序查询 |
| 嵌套查询 | findMany({ include }) | 包含关联数据 |
| 字段选择 | findMany({ select }) | 选择字段 |
| 分页查询 | findMany({ skip, take }) | 分页查询 |
11.2 排序命令速查
| 排序方式 | 命令 | 说明 |
|---|---|---|
| 升序 | orderBy: { field: 'asc' } | 从小到大 |
| 降序 | orderBy: { field: 'desc' } | 从大到小 |
| 多字段 | orderBy: [{ f1: 'asc' }, { f2: 'desc' }] | 多字段排序 |
| 嵌套排序 | include: { relation: { orderBy } } | 关联数据排序 |
十二、学习要点总结
12.1 核心概念总结
code
NestJS 多表联合查询核心要点:
│
├── 查询链路
│ ├── CourseTypes → Tags → CourseTags → Courses
│ ├── 4 层关联关系
│ └── 使用 include 嵌套查询
│
├── 嵌套查询
│ ├── include 包含关联数据
│ ├── 支持多层嵌套
│ └── 俄罗斯套娃式查询
│
├── 排序功能
│ ├── orderBy 在 findMany 同级
│ ├── asc 升序、desc 降序
│ └── 支持多字段排序
│
├── 数据结构化
│ ├── Service 层处理
│ ├── Map 去重优化
│ └── 返回前端友好格式
│
└── 性能优化
├── 使用 select 选择字段
├── 使用分页查询
└── 添加必要的索引12.2 学习路径规划
code
学习路径规划:
│
├── 第一阶段:理解概念(1 天)
│ ├── 理解数据表关系
│ ├── 理解查询链路
│ └── 理解嵌套查询原理
│
├── 第二阶段:实践操作(2-3 天)
│ ├── 实现多表联合查询
│ ├── 实现排序功能
│ └── 实现数据结构化
│
└── 第三阶段:深入应用(持续)
├── 优化查询性能
├── 处理复杂业务场景
└── 完善错误处理12.3 重要提示
重要提示:多表联合查询是实际项目中的常见需求,理解数据表关系、掌握 Prisma include 嵌套查询、合理使用排序功能、优化查询性能,对实际项目开发非常重要!特别是要理解嵌套查询的层级关系,避免数据冗余,在 Service 层对数据进行结构化处理,返回前端友好的数据格式!