如何在NestJS中使用sequelize-typescript递归获取所有子节点
在NestJS中用sequelize-typescript递归获取全层级分类
问题根源
你遇到的栈溢出是因为默认作用域中递归引用了自身的关联,导致Sequelize无限循环解析关联配置;而nested: true仅能获取2层,是因为Sequelize默认的嵌套深度限制。下面提供三种可行的解决方案,按需选择:
方案1:手动递归查询(小数据量首选)
直接通过代码递归查询每一层子分类,逻辑简单易懂,适合分类层级不多、数据量较小的场景。
代码实现
- 分类模型(确保已定义父子关联):
// category.model.ts import { Table, Column, Model, DataType, HasMany, BelongsTo } from 'sequelize-typescript'; @Table({ tableName: 'categories' }) export class Category extends Model { @Column({ type: DataType.INTEGER, primaryKey: true, autoIncrement: true }) id: number; @Column({ type: DataType.STRING, allowNull: false }) name: string; @Column({ type: DataType.INTEGER, allowNull: true }) parentId: number; @BelongsTo(() => Category, 'parentId') parent: Category; @HasMany(() => Category, 'parentId') children: Category[]; }
- Service层递归方法:
// category.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Category } from './category.model'; @Injectable() export class CategoryService { constructor(@InjectModel(Category) private categoryModel: typeof Category) {} async getFullChildCategories(parentId: number): Promise<Category[]> { // 查询当前父ID下的直接子分类 const children = await this.categoryModel.findAll({ where: { parentId }, attributes: ['id', 'name', 'parentId'], // 按需选择字段,减少数据传输 }); // 递归遍历每个子分类,查询其子分类 for (const child of children) { child['children'] = await this.getFullChildCategories(child.id); } return children; } }
方案2:Sequelize动态递归Include(中等数据量)
通过手动构建递归的Include配置,替代默认作用域的关联,避免循环引用导致的栈溢出,同时可自定义递归深度。
代码实现
在Service层编写递归Include生成函数,然后用于查询:
// category.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Category } from './category.model'; @Injectable() export class CategoryService { constructor(@InjectModel(Category) private categoryModel: typeof Category) {} // 生成指定深度的递归Include配置 private buildRecursiveInclude(depth: number = 10): any { if (depth <= 0) return []; return { model: Category, as: 'children', required: false, // 允许无子分类的节点存在 include: [this.buildRecursiveInclude(depth - 1)], }; } async getCategoriesWithAllChildren(parentId: number) { return this.categoryModel.findAll({ where: { parentId }, include: [this.buildRecursiveInclude()], // 默认递归10层,可按需调整 }); } }
注意事项
- 避免在默认作用域中配置递归关联,否则仍会触发循环引用栈溢出。
- 可根据业务实际情况调整
depth参数,防止不必要的深层查询。
方案3:数据库递归查询(大数据量推荐)
利用数据库的递归CTE(Common Table Expression)特性,在数据库层面一次性查询所有层级数据,性能最优,适合分类层级深、数据量大的场景(需PostgreSQL 9.4+ / MySQL 8.0+)。
代码实现
// category.service.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { Category } from './category.model'; @Injectable() export class CategoryService { constructor(@InjectModel(Category) private categoryModel: typeof Category) {} async getRecursiveCategories(parentId: number): Promise<any[]> { // 递归CTE查询语句 const query = ` WITH RECURSIVE category_tree AS ( SELECT id, name, parentId, 1 AS level FROM categories WHERE parentId = :parentId UNION ALL SELECT c.id, c.name, c.parentId, ct.level + 1 FROM categories c JOIN category_tree ct ON c.parentId = ct.id ) SELECT * FROM category_tree ORDER BY level, id; `; // 执行原生查询 const [flatResults] = await this.categoryModel.sequelize.query(query, { replacements: { parentId }, type: this.categoryModel.sequelize.QueryTypes.SELECT, }); // 将扁平结果转换为树形结构 return this.convertFlatToTree(flatResults); } // 辅助函数:扁平数组转树形结构 private convertFlatToTree(categories: any[]): any[] { const categoryMap = new Map(); const rootCategories: any[] = []; // 先将所有分类存入Map,便于快速查找 categories.forEach(cat => { categoryMap.set(cat.id, { ...cat, children: [] }); }); // 构建树形关系 categories.forEach(cat => { const parent = categoryMap.get(cat.parentId); if (parent) { parent.children.push(categoryMap.get(cat.id)); } else { rootCategories.push(categoryMap.get(cat.id)); } }); return rootCategories; } }
内容的提问来源于stack exchange,提问作者Saad
相关产品推荐
相关产品推荐

