NestJS中DTO与非1:1匹配关联实体的转换最佳实践
NestJS中DTO与多对多关联实体非1:1匹配的转换方案
1. 手动转换(适合新手快速理解)
直接在服务层处理DTO到实体的转换逻辑,把bundles数组转成questionToCategories需要的中间表实体数组。
先给出实体定义(方便对照):
// category.entity.ts @Entity() export class Category { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => QuestionToCategory, qtc => qtc.category) questionToCategories: QuestionToCategory[]; } // question.entity.ts @Entity() export class Question { @PrimaryGeneratedColumn() id: number; @Column() content: string; @OneToMany(() => QuestionToCategory, qtc => qtc.question, { cascade: true }) questionToCategories: QuestionToCategory[]; } // question-to-category.entity.ts @Entity() export class QuestionToCategory { @PrimaryGeneratedColumn() id: number; @ManyToOne(() => Question, question => question.questionToCategories) question: Question; @ManyToOne(() => Category, category => category.questionToCategories) category: Category; }
CreateQuestionDto定义:
// create-question.dto.ts export class CreateQuestionDto { @IsString() content: string; @IsArray() @IsNumber({}, { each: true }) bundles: number[]; }
服务层转换代码:
// question.service.ts @Injectable() export class QuestionService { constructor( @InjectRepository(Question) private questionRepo: Repository<Question>, @InjectRepository(Category) private categoryRepo: Repository<Category>, ) {} async create(createQuestionDto: CreateQuestionDto) { // 创建Question基础实体 const question = this.questionRepo.create({ content: createQuestionDto.content, }); // 转换bundles为QuestionToCategory数组 const questionToCategories = await Promise.all( createQuestionDto.bundles.map(async categoryId => { const category = await this.categoryRepo.findOneBy({ id: categoryId }); if (!category) { throw new NotFoundException(`Category ${categoryId} not found`); } const qtc = new QuestionToCategory(); qtc.category = category; qtc.question = question; return qtc; }), ); // 关联并保存 question.questionToCategories = questionToCategories; return this.questionRepo.save(question); } }
注意:需给Question实体的questionToCategories设置{ cascade: true },保存Question时会自动同步中间表数据。
2. 使用class-transformer自定义转换器(更优雅的方式)
通过自定义转换器让DTO转实体时自动处理关联字段,减少服务层重复代码。
创建转换器:
// category-to-qtc.transformer.ts import { Transform } from 'class-transformer'; import { Injectable, NotFoundException } from '@nestjs/common'; import { Category } from './category.entity'; import { QuestionToCategory } from './question-to-category.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; @Injectable() export class CategoryIdsToQtcTransformer { constructor( @InjectRepository(Category) private categoryRepo: Repository<Category>, ) {} @Transform(async (value: number[]) => { if (!value || !Array.isArray(value)) return []; const categories = await this.categoryRepo.findByIds(value); // 校验分类ID是否存在 const existingIds = categories.map(c => c.id); const missingIds = value.filter(id => !existingIds.includes(id)); if (missingIds.length > 0) { throw new NotFoundException(`Categories ${missingIds.join(', ')} not found`); } return categories.map(category => { const qtc = new QuestionToCategory(); qtc.category = category; return qtc; }); }, { toClassOnly: true }) transform!: number[]; }
服务层使用转换器:
// question.service.ts import { plainToInstance } from 'class-transformer'; @Injectable() export class QuestionService { constructor( @InjectRepository(Question) private questionRepo: Repository<Question>, private readonly categoryIdsTransformer: CategoryIdsToQtcTransformer, ) {} async create(createQuestionDto: CreateQuestionDto) { const questionData = { content: createQuestionDto.content, questionToCategories: createQuestionDto.bundles, }; // 用转换器自动处理分类ID数组 const question = plainToInstance(Question, questionData, { transformers: [this.categoryIdsTransformer], }); return this.questionRepo.save(question); } }
记得在模块中将转换器注册为提供者。
3. 利用TypeORM原生多对多(无中间表额外字段时)
如果中间表仅存关联关系,无需显式定义QuestionToCategory实体,直接用TypeORM的@ManyToMany关联更简洁:
修改实体定义:
// question.entity.ts @Entity() export class Question { @PrimaryGeneratedColumn() id: number; @Column() content: string; @ManyToMany(() => Category, category => category.questions) @JoinTable({ name: 'question_to_category', joinColumn: { name: 'question_id', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'category_id', referencedColumnName: 'id' }, }) categories: Category[]; } // category.entity.ts @Entity() export class Category { @PrimaryGeneratedColumn() id: number; @Column() name: string; @ManyToMany(() => Question, question => question.categories) questions: Question[]; }
服务层简化代码:
async create(createQuestionDto: CreateQuestionDto) { const categories = await this.categoryRepo.findByIds(createQuestionDto.bundles); const question = this.questionRepo.create({ content: createQuestionDto.content, categories, }); return this.questionRepo.save(question); }
最佳实践总结
- 中间表有额外业务字段时,优先选手动转换或class-transformer转换器,保证代码可读性;
- 中间表仅存关联关系时,用TypeORM原生
@ManyToMany,减少冗余代码; - 所有场景都要校验参数合法性(比如检查分类ID是否存在),避免无效数据入库;
- 合理使用TypeORM的
cascade选项,简化关联数据的保存逻辑。
内容的提问来源于stack exchange,提问作者Burh Moment
相关产品推荐
相关产品推荐

