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

MySQL+NestJS+TypeORM用户注销时保留cat表删其他关联数据方案

NestJS+TypeORM+MySQL 下可扩展的用户注销数据清理方案

核心思路

放弃硬编码逐表写删除语句的逻辑,利用TypeORM本身维护的实体关联元数据、或MySQL系统表自动识别所有和User存在关联的表,通过配置排除需要保留数据的表(如Cat表),自动生成并执行删除逻辑,后续新增/删除关联表时无需修改核心清理代码。


前置注意事项

不要给所有外键统一配置ON DELETE CASCADE,会无差别删除Cat表的关联数据,不符合需求。可以单独给Cat表的userId外键配置ON DELETE SET NULL,删除用户时Cat表数据保留,仅将关联用户字段置空;其他需要随用户删除的关联表不要配置级联删除规则,统一由业务代码控制清理逻辑。


推荐实现:TypeORM元数据驱动自动清理

这个方案完全基于TypeORM的实体定义能力,不需要额外维护表名列表,新增关联实体时只要不加入保留列表就会被自动清理。

  1. 核心实现代码(可直接作为NestJS服务注入使用)
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { User } from './entities/user.entity';
import { Cat } from './entities/cat.entity';

@Injectable()
export class UserCleanupService {
  // 配置用户删除时需要保留数据的实体,后续有其他要保留的表直接加到这个数组即可
  private readonly retainedEntities = [Cat];

  constructor(@InjectConnection() private readonly connection: Connection) {}

  async deleteUserAndRelatedData(userId: number): Promise<void> {
    const queryRunner = this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const userMeta = this.connection.getMetadata(User);
      // 筛选所有和User关联、且不在保留列表的实体
      const entitiesNeedClean = userMeta.relations
        .map(rel => rel.inverseEntityMetadata)
        .filter(
          entityMeta =>
            !this.retainedEntities.some(
              retained => entityMeta.target === retained
            )
        );

      // 逐表执行删除
      for (const entityMeta of entitiesNeedClean) {
        // 自动识别实体上关联User的外键字段名,不需要硬编码userId
        const userForeignKey = entityMeta.relations.find(
          rel => rel.inverseEntityMetadata.target === User
        )?.foreignKeys[0]?.columns[0]?.databaseName;

        if (!userForeignKey) continue;

        await queryRunner.manager
          .createQueryBuilder()
          .delete()
          .from(entityMeta.tableName)
          .where(`${userForeignKey} = :userId`, { userId })
          .execute();
      }

      // 所有关联数据清理完成后,删除User主表记录
      await queryRunner.manager.delete(User, userId);

      await queryRunner.commitTransaction();
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}
  1. 扩展性优化
    如果后续有特殊清理需求的表(比如不需要物理删除、只做数据脱敏/软删除),可以自定义实体装饰器标记清理策略,遍历实体时识别装饰器走对应逻辑即可,不需要修改核心清理流程。

备选实现:基于MySQL系统表动态获取关联表

如果项目中部分表没有通过TypeORM配置关联关系,可以直接查询MySQL自带的information_schema系统表,自动获取所有关联User的表,排除保留表后执行删除:

-- 查询所有通过userId外键关联User表、且需要清理的表
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
  REFERENCED_TABLE_NAME = 'user'
  AND REFERENCED_COLUMN_NAME = 'id'
  AND COLUMN_NAME = 'userId'
  AND TABLE_NAME != 'cat';

代码中拿到查询返回的表名列表后,循环拼接删除语句执行即可,注意同样要放在事务中保证数据一致性。


关键注意点

  • 所有清理操作必须包裹在数据库事务中,避免中途报错导致部分数据删除、部分保留的脏数据问题
  • 保留实体列表直接引用实体类,不要硬编码表名字符串,避免后续修改表名/实体名时出现配置遗漏
  • 大用户量场景下如果关联表数据量过大,建议把删除逻辑改成分批异步删除,避免长事务锁表影响线上业务

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 09:57:29