You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS+MongoDB三层层级模型的级联删除最佳实践

解决NestJS+MongoDB三级模型级联删除的最佳架构方案

针对你遇到的Calendar→Event→Invitation三级关联级联删除问题,结合NestJS的模块设计原则和MongoDB特性,以下是三种无循环依赖、模块解耦的通用解决方案,适配不同场景需求:


方案1:Mongoose预删除钩子(简单场景首选)

利用Mongoose的pre('remove')钩子,将级联删除逻辑内聚到模型层,业务层无需跨模块调用,彻底避免循环依赖。

实现步骤:

  1. 为每个模型添加预删除钩子:
    • 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;
  }
}
  1. 在模块中注册带钩子的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 {}
  1. 修改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模块监听事件并处理级联删除,彻底避免循环依赖。

实现步骤:

  1. 定义事件类:
// 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[]) {}
}
  1. 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: '日历已删除' };
  }
}
  1. 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));
  }
}
  1. 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);
  }
}
  1. 在模块中注册订阅者:
// event.module.ts
@Module({
  imports: [MongooseModule.forFeature([{ name: Event.name, schema: EventSchema }])],
  providers: [EventService, CalendarDeletedSubscriber],
})
export class EventModule {}

优点:模块解耦彻底,扩展性强,可轻松添加删除后的业务逻辑(如发送通知、记录日志);缺点:需定义事件和订阅者,代码量略多。


方案3:共享数据访问层(DAL)

创建统一的DataAccessModule,集中管理所有模型的数据操作逻辑,业务模块仅依赖DAL,避免模块间直接依赖。

实现步骤:

  1. 创建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 {}
  1. 在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);
  }
}
  1. 业务模块依赖DataAccessModule:
// calendar.module.ts
@Module({
  imports: [DataAccessModule],
  providers: [CalendarService],
})
export class CalendarModule {}

优点:数据操作逻辑集中,业务模块专注于业务逻辑,便于统一维护;缺点:DAL模块会随模型增多而变大,但仍比单模块更可控。


方案选择建议

  • 小型项目/纯数据删除场景:选Mongoose预删除钩子,快速实现,代码简洁。
  • 中大型项目/需扩展业务逻辑:选领域事件,解耦彻底,扩展性强。
  • 已有统一数据访问规划:选共享DataAccessModule,统一管理数据操作。

内容的提问来源于stack exchange,提问作者physicalattraction

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 14:08:09