NestJS+MongoDB三层层级模型的级联删除最佳实践
解决NestJS+MongoDB三级模型级联删除的最佳架构方案
针对你遇到的Calendar→Event→Invitation三级关联级联删除问题,结合NestJS的模块设计原则和MongoDB特性,以下是三种无循环依赖、模块解耦的通用解决方案,适配不同场景需求:
方案1:Mongoose预删除钩子(简单场景首选)
利用Mongoose的pre('remove')钩子,将级联删除逻辑内聚到模型层,业务层无需跨模块调用,彻底避免循环依赖。
实现步骤:
- 为每个模型添加预删除钩子:
- Calendar模型钩子:删除关联Event
- Event模型钩子:删除关联Invitation
// calendar.schema.ts import { Schema } from 'mongoose'; import { Injectable, InjectModel } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Event } from '../event/event.schema'; @Injectable() export class CalendarSchemaFactory { constructor(@InjectModel(Event.name) private eventModel: Model<Event>) {} createSchema(): Schema { const schema = new Schema({ name: { type: String, required: true }, // 其他日历字段 }); // 预删除钩子:删除关联事件 schema.pre('remove', async function (next) { const calendar = this as any; await this.eventModel.deleteMany({ calendarId: calendar._id }); next(); }); return schema; } }
// event.schema.ts import { Schema } from 'mongoose'; import { Injectable, InjectModel } from '@nestjs/common'; import { Model } from 'mongoose'; import { Invitation } from '../invitation/invitation.schema'; @Injectable() export class EventSchemaFactory { constructor(@InjectModel(Invitation.name) private invitationModel: Model<Invitation>) {} createSchema(): Schema { const schema = new Schema({ title: { type: String, required: true }, calendarId: { type: Schema.Types.ObjectId, ref: 'Calendar' }, // 其他事件字段 }); // 预删除钩子:删除关联邀请 schema.pre('remove', async function (next) { const event = this as any; await this.invitationModel.deleteMany({ eventId: event._id }); next(); }); return schema; } }
- 在模块中注册带钩子的Schema:
// calendar.module.ts import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Calendar } from './calendar.schema'; import { CalendarSchemaFactory } from './calendar.schema'; import { CalendarService } from './calendar.service'; @Module({ imports: [ MongooseModule.forFeatureAsync([ { name: Calendar.name, useFactory: (factory: CalendarSchemaFactory) => factory.createSchema(), inject: [CalendarSchemaFactory], }, ]), ], providers: [CalendarSchemaFactory, CalendarService], }) export class CalendarModule {}
- 修改Service删除逻辑,触发钩子:
// calendar.service.ts async deleteCalendar(id: string) { const calendar = await this.calendarModel.findById(id); if (!calendar) throw new NotFoundException('日历不存在'); await calendar.remove(); // 触发pre('remove')钩子,自动级联删除 return { message: '日历及关联事件、邀请已删除' }; }
优点:代码简洁,逻辑内聚,模块分离清晰;缺点:仅适用于纯数据删除场景,复杂业务逻辑(如删除后通知用户)难以扩展。
方案2:NestJS领域事件(中大型项目首选)
通过NestJS内置的EventEmitter实现模块间解耦通信,Calendar删除后发布事件,Event/Invitation模块监听事件并处理级联删除,彻底避免循环依赖。
实现步骤:
- 定义事件类:
// events/calendar-deleted.event.ts export class CalendarDeletedEvent { constructor(public readonly calendarId: string) {} } // events/events-deleted.event.ts export class EventsDeletedEvent { constructor(public readonly eventIds: string[]) {} }
- CalendarService发布事件:
// calendar.service.ts import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class CalendarService { constructor( private readonly calendarModel: Model<Calendar>, private eventEmitter: EventEmitter2, ) {} async deleteCalendar(id: string) { const calendar = await this.calendarModel.findByIdAndDelete(id); if (!calendar) throw new NotFoundException('日历不存在'); // 发布日历删除事件 this.eventEmitter.emit('calendar.deleted', new CalendarDeletedEvent(id)); return { message: '日历已删除' }; } }
- Event模块订阅事件并删除关联事件:
// event/subscribers/calendar-deleted.subscriber.ts import { OnEvent } from '@nestjs/event-emitter'; import { Injectable } from '@nestjs/common'; import { EventService } from '../event.service'; import { CalendarDeletedEvent } from '../../events/calendar-deleted.event'; import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class CalendarDeletedSubscriber { constructor( private eventService: EventService, private eventEmitter: EventEmitter2, ) {} @OnEvent('calendar.deleted') async handleCalendarDeleted(event: CalendarDeletedEvent) { const events = await this.eventService.findByCalendarId(event.calendarId); const eventIds = events.map(e => e._id.toString()); await this.eventService.deleteByIds(eventIds); // 发布事件删除事件 this.eventEmitter.emit('events.deleted', new EventsDeletedEvent(eventIds)); } }
- Invitation模块订阅事件并删除关联邀请:
// invitation/subscribers/events-deleted.subscriber.ts import { OnEvent } from '@nestjs/event-emitter'; import { Injectable } from '@nestjs/common'; import { InvitationService } from '../invitation.service'; import { EventsDeletedEvent } from '../../events/events-deleted.event'; @Injectable() export class EventsDeletedSubscriber { constructor(private invitationService: InvitationService) {} @OnEvent('events.deleted') async handleEventsDeleted(event: EventsDeletedEvent) { await this.invitationService.deleteByEventIds(event.eventIds); } }
- 在模块中注册订阅者:
// event.module.ts @Module({ imports: [MongooseModule.forFeature([{ name: Event.name, schema: EventSchema }])], providers: [EventService, CalendarDeletedSubscriber], }) export class EventModule {}
优点:模块解耦彻底,扩展性强,可轻松添加删除后的业务逻辑(如发送通知、记录日志);缺点:需定义事件和订阅者,代码量略多。
方案3:共享数据访问层(DAL)
创建统一的DataAccessModule,集中管理所有模型的数据操作逻辑,业务模块仅依赖DAL,避免模块间直接依赖。
实现步骤:
- 创建DataAccessModule:
// data-access/data-access.module.ts import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Calendar, CalendarSchema } from '../calendar/calendar.schema'; import { Event, EventSchema } from '../event/event.schema'; import { Invitation, InvitationSchema } from '../invitation/invitation.schema'; import { CalendarRepository } from './calendar.repository'; import { EventRepository } from './event.repository'; import { InvitationRepository } from './invitation.repository'; @Module({ imports: [ MongooseModule.forFeature([ { name: Calendar.name, schema: CalendarSchema }, { name: Event.name, schema: EventSchema }, { name: Invitation.name, schema: InvitationSchema }, ]), ], providers: [CalendarRepository, EventRepository, InvitationRepository], exports: [CalendarRepository, EventRepository, InvitationRepository], }) export class DataAccessModule {}
- 在Repository中实现级联删除逻辑:
// data-access/calendar.repository.ts import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Calendar } from '../calendar/calendar.schema'; import { EventRepository } from './event.repository'; @Injectable() export class CalendarRepository { constructor( @InjectModel(Calendar.name) private calendarModel: Model<Calendar>, private eventRepository: EventRepository, ) {} async delete(id: string): Promise<void> { // 先删除关联事件(内部自动删除关联邀请) await this.eventRepository.deleteByCalendarId(id); // 再删除日历 await this.calendarModel.findByIdAndDelete(id); } }
- 业务模块依赖DataAccessModule:
// calendar.module.ts @Module({ imports: [DataAccessModule], providers: [CalendarService], }) export class CalendarModule {}
优点:数据操作逻辑集中,业务模块专注于业务逻辑,便于统一维护;缺点:DAL模块会随模型增多而变大,但仍比单模块更可控。
方案选择建议
- 小型项目/纯数据删除场景:选Mongoose预删除钩子,快速实现,代码简洁。
- 中大型项目/需扩展业务逻辑:选领域事件,解耦彻底,扩展性强。
- 已有统一数据访问规划:选共享DataAccessModule,统一管理数据操作。
内容的提问来源于stack exchange,提问作者physicalattraction
相关产品推荐
相关产品推荐

