news 2026/4/16 12:32:10

Nest中如何处理关联表数据,避免+1问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nest中如何处理关联表数据,避免+1问题

文章目录

  • 前言
      • 问题分析
      • 使用TypeORM的优雅处理方案
      • 使用Prisma的优雅处理方案
      • 比较与建议
    • 思考🤔:
        • 思考1
        • 思考2
      • 什么是 N+1 问题?(通俗解释)
      • 上面两种方案是如何避免 N+1 的?
        • 1. TypeORM 的方式
        • 2. Prisma 的方式
      • 对比总结表
      • 额外几种常见的 N+1 写法(要警惕)
      • 总结一句话

前言

问题分析

问题是:在NestJS框架中使用TypeORM或Prisma作为ORM时,如何处理数据库表之间的关联关系。具体场景是“英语分类表”(Category)和“英语句子表”(Sentence),其中Sentence表通过categoryId关联Category表。现在需要在查询Sentence数据时,自动返回关联的Category名称等字段,而不只是ID。目标是优雅处理,避免手动查询或冗余代码。

这是一个典型的ORM关系查询问题。我们可以利用ORM的内置特性(如关系加载)来实现“自动”包含关联数据。以下分别针对TypeORM和Prisma提供方案。假设你的数据库是关系型(如PostgreSQL或MySQL),实体已定义好。

使用TypeORM的优雅处理方案

TypeORM支持实体间的关系定义(如@ManyToOne),并通过relations或eager loading来自动加载关联数据。在NestJS中,这通常在Service层实现。

  1. 实体定义

    • Category实体(src/entities/category.entity.ts):
      import{Entity,PrimaryGeneratedColumn,Column,OneToMany}from'typeorm';import{Sentence}from'./sentence.entity';@Entity()exportclassCategory{@PrimaryGeneratedColumn()id:number;@Column()name:string;// 分类名称@OneToMany(()=>Sentence,(sentence)=>sentence.category)sentences:Sentence[];// 可选,反向关系}
    • Sentence实体(src/entities/sentence.entity.ts):
      import{Entity,PrimaryGeneratedColumn,Column,ManyToOne}from'typeorm';import{Category}from'./category.entity';@Entity()exportclassSentence{@PrimaryGeneratedColumn()id:number;@Column()sentence:string;@ManyToOne(()=>Category,(category)=>category.sentences)category:Category;// 关联Category实体(非ID)}

    注意:这里使用@ManyToOne定义关系,category字段是实体类型,而非简单ID。这允许TypeORM自动处理join。

  2. 在Service中查询(src/sentences/sentences.service.ts):

    • 使用find方法时指定relations选项,自动加载category:
      import{Injectable}from'@nestjs/common';import{InjectRepository}from'@nestjs/typeorm';import{Repository}from'typeorm';import{Sentence}from'./entities/sentence.entity';@Injectable()exportclassSentencesService{constructor(@InjectRepository(Sentence)privatesentencesRepository:Repository<Sentence>,){}asyncfindAll():Promise<Sentence[]>{returnthis.sentencesRepository.find({relations:['category'],// 自动加载category,包括name等字段});}asyncfindOne(id:number):Promise<Sentence>{returnthis.sentencesRepository.findOne({where:{id},relations:['category'],// 只加载需要的关联});}}
  3. 在Controller中返回(src/sentences/sentences.controller.ts):

    • 直接返回Service的结果,响应会自动包含category.name:
      import{Controller,Get,Param}from'@nestjs/common';import{SentencesService}from'./sentences.service';@Controller('sentences')exportclassSentencesController{constructor(privatereadonlysentencesService:SentencesService){}@Get()findAll(){returnthis.sentencesService.findAll();}@Get(':id')findOne(@Param('id')id:string){returnthis.sentencesService.findOne(+id);}}
  4. 优雅点

    • 如果总是需要category,可以在实体中设置eager: true(@ManyToOne(…, { eager: true })),这样find时无需指定relations,但小心性能(适合小表)。
    • 使用QueryBuilder自定义查询:this.sentencesRepository.createQueryBuilder('sentence').leftJoinAndSelect('sentence.category', 'category').getMany();– 这更灵活,支持select特定字段(如只选category.name)。
    • 避免N+1问题:使用relations或join加载,而不是循环手动查询。
    • DTO转换:如果需要自定义响应格式,用class-transformer将实体转为DTO,只暴露必要字段。

使用Prisma的优雅处理方案

Prisma的Schema定义更声明式,使用include选项自动加载关系,性能更好(默认lazy loading)。

  1. Prisma Schema定义(prisma/schema.prisma):

    model Category { id Int @id @default(autoincrement()) name String // 分类名称 sentences Sentence[] // 反向关系 } model Sentence { id Int @id @default(autoincrement()) sentence String categoryId Int category Category @relation(fields: [categoryId], references: [id]) // 关联 }
    • 生成迁移和client:npx prisma generatenpx prisma migrate dev
  2. 在Service中查询(src/sentences/sentences.service.ts):

    • 使用findManyfindUnique时指定include
      import{Injectable}from'@nestjs/common';import{PrismaService}from'../prisma/prisma.service';// 假设你有PrismaService@Injectable()exportclassSentencesService{constructor(privateprisma:PrismaService){}asyncfindAll(){returnthis.prisma.sentence.findMany({include:{category:true,// 自动包含category的所有字段,如name},});}asyncfindOne(id:number){returnthis.prisma.sentence.findUnique({where:{id},include:{category:true,},});}}
  3. 在Controller中返回

    • 同TypeORM,直接返回Service结果,响应会嵌套category.name。
  4. 优雅点

    • 自定义select:include: { category: { select: { name: true } } }– 只返回name,避免多余数据。
    • Prisma的类型安全:自动生成TypeScript类型,确保编译时检查。
    • 批量查询:Prisma优化了N+1问题,使用DataLoader内部机制。
    • 如果需要复杂过滤,用where结合include。

比较与建议

  • TypeORM vs Prisma
    • TypeORM更灵活(支持自定义SQL),但配置稍繁琐。适合已有实体模型的项目。
    • Prisma更现代、类型安全,查询语法简洁。推荐新项目使用Prisma,因为它内置优化且易维护。
  • 通用优雅实践(NestJS中):
    • 使用DTO(class-validator + class-transformer)转换响应,避免暴露内部实体。
    • 错误处理:添加try-catch或全局interceptor。
    • 性能:对于大表,用分页(skip/take in TypeORM, take/skip in Prisma)和缓存(Redis)。
    • 测试:用Jest写单元测试,模拟ORM查询。

思考🤔:

思考1

选择 LEFT JOIN 是因为:

✅ 保证句子数据不丢失
✅ 容错性好(分类删除不影响句子显示)
✅ 便于发现数据问题
✅ 符合"以句子为主"的业务逻辑

思考2

N+1 查询问题是 ORM(对象关系映射)中最常见、也最容易被忽略的性能问题之一,尤其在使用 TypeORM、Prisma、Hibernate、Entity Framework 等工具时特别常见。

什么是 N+1 问题?(通俗解释)

假设你有下面这样的场景:

你想查 100 条英语句子(Sentence),并且希望每条句子都带上它的分类名称(Category.name)。

最“天真”的写法(会产生 N+1 问题)大概长这样:

// 伪代码 - 非常危险的写法constsentences=awaitsentenceRepository.find();// 查 100 条句子 → 1 次查询for(constsentenceofsentences){// 每条句子都去查一次 categoryconstcategory=awaitcategoryRepository.findOne({where:{id:sentence.categoryId}});sentence.categoryName=category.name;}

实际执行的 SQL 次数

1 次查询 → 找出 100 条句子 + 100 次查询 → 每条句子单独查一次 category = 总共 101 次数据库查询

这就是著名的N+1 问题

  • 1 = 查主表(Sentence)的查询
  • N = 主表有多少条记录,就额外发多少次查询去查关联表(Category)

当数据量变大(比如 1000 条、1 万条),性能会急剧下降,甚至几秒钟都出不来结果。

上面两种方案是如何避免 N+1 的?

1. TypeORM 的方式
// 推荐写法awaitsentenceRepository.find({relations:['category']});

.createQueryBuilder('sentence').leftJoinAndSelect('sentence.category','category').getMany();

实际产生的 SQL(大致):

SELECTsentence.*,category.*FROMsentenceLEFTJOINcategoryONsentence.categoryId=category.id

只发 1~2 次查询(通常只有 1 次),就把所有需要的 category 数据一起查出来了。

这就是预先加载(eager loading / join fetching)的方式,通过一次 JOIN 操作就把关联数据全部取回,避免了循环里重复查询。

2. Prisma 的方式
awaitprisma.sentence.findMany({include:{category:true}});

Prisma 内部实际也会生成类似 JOIN 的查询(大多数情况下是一次查询):

SELECT"Sentence".*,"Category".*FROM"Sentence"LEFTJOIN"Category"ON"Sentence"."categoryId"="Category"."id"

结果:同样只用 1 次查询就把所有 category 数据带回来了。

对比总结表

方式查询次数是否有 N+1性能(数据量大时)推荐场景
循环里单独查 category1 + N极差千万不要用
TypeORM relations / join通常 1 次很好推荐
Prisma include通常 1 次很好强烈推荐
实体上设置 eager: true通常 1 次好(但全局)小表、总是需要的情况
手动写 JOIN SQL1 次最好极致性能要求时

额外几种常见的 N+1 写法(要警惕)

// 危险写法 1 - 隐形的 N+1sentences.forEach(s=>console.log(s.category.name));// 如果 category 没加载,就会触发懒加载// 危险写法 2 - 嵌套循环for(constsentenceofsentences){for(consttagofsentence.tags){// 如果 tags 没预加载// ...}}

总结一句话

N+1 问题本质是:本来可以用一次 JOIN 解决的事情,被你拆成了 N+1 次单独查询。

TypeORM 的relations/leftJoinAndSelect和 Prisma 的include都是通过一次 JOIN把关联数据提前加载回来,从而把查询次数从1+N降到1(或极少数情况 2),这就是它们能优雅避免 N+1 的核心原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/15 18:02:13

救命神器10个AI论文网站,研究生高效写作必备!

救命神器10个AI论文网站&#xff0c;研究生高效写作必备&#xff01; AI 工具助力论文写作&#xff0c;高效提分不是梦 在研究生阶段&#xff0c;论文写作是每一位学生必须面对的挑战。无论是开题报告、文献综述&#xff0c;还是最终的毕业论文&#xff0c;都需要大量的时间与精…

作者头像 李华
网站建设 2026/4/10 18:02:16

DeepSeek V4新突破:编程能力全面升级,或将超越GPT与Claude

DeepSeek将于2月中旬推出主打编程能力的新一代AI模型V4&#xff0c;据内部测试&#xff0c;其代码任务表现可能超越Claude和GPT系列&#xff0c;并在处理超长代码提示方面有突破性进展&#xff0c;这对开发者处理复杂项目大有裨益。恰逢中国春节发布&#xff0c;网友调侃DeepSe…

作者头像 李华
网站建设 2026/4/16 8:58:39

基于遗传算法的5B70铝合金铣削加工多目标参数优化附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f447; 关注我领取海量matlab电子书和数学建模资料 &#x1f34…

作者头像 李华
网站建设 2026/4/16 11:01:44

SpringBoot邮件发送功能模版

获取授权码 邮件发送需要准备的信息&#xff1a; 你想要使用的来发送邮件的邮箱的 SMTP 授权码&#xff0c;注意是授权码&#xff0c;不是登录邮箱的密码 1.如果你想要用163邮箱来发送测试邮件 需要获得163邮箱的 SMTP 授权码&#xff1a; 打开163邮箱官网 在顶部的设置 …

作者头像 李华
网站建设 2026/4/16 10:38:43

圆度误差的神经网络评定及测量不确定度研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。&#x1f34e; 往期回顾关注个人主页&#xff1a;Matlab科研工作室&#x1f447; 关注我领取海量matlab电子书和数学建模资料 &#x1f34…

作者头像 李华