乐闻世界logo
搜索文章和话题

TypeORM 如何实现软删除?包括软删除的配置、操作和最佳实践

2月18日 22:09

软删除是一种数据删除策略,它不会真正从数据库中删除记录,而是通过标记记录为"已删除"状态来隐藏它们。TypeORM 提供了内置的软删除支持,使得实现软删除变得简单而强大。

软删除基础概念

什么是软删除

软删除是一种数据保留策略,当删除记录时:

  • 不会真正从数据库中删除记录
  • 只是将记录标记为"已删除"状态
  • 查询时默认不包含已删除的记录
  • 可以随时恢复已删除的记录

软删除 vs 硬删除

特性软删除硬删除
数据保留保留在数据库中从数据库中删除
可恢复性可以恢复无法恢复
存储空间占用存储空间释放存储空间
查询性能需要过滤已删除记录查询性能更好
审计追踪保留删除历史无法追踪删除历史

TypeORM 软删除实现

基本软删除配置

typescript
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; // 软删除列:当记录被软删除时,此列会被设置为当前时间 @DeleteDateColumn() deletedAt: Date | null; }

使用软删除

typescript
import { DataSource } from 'typeorm'; const dataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'myapp', entities: [User], synchronize: true, }); // 软删除用户 await dataSource.manager.softRemove(user); // 或者使用 Repository await userRepository.softRemove(user); // 恢复已删除的用户 await dataSource.manager.recover(user); // 或者使用 Repository await userRepository.recover(user); // 硬删除用户(真正删除) await dataSource.manager.remove(user); // 或者使用 Repository await userRepository.remove(user);

软删除操作

软删除记录

typescript
// 方式1:使用 softRemove const user = await userRepository.findOne({ where: { id: 1 } }); await userRepository.softRemove(user); // 方式2:使用 softDelete await userRepository.softDelete(1); // 方式3:使用 QueryBuilder await userRepository .createQueryBuilder() .softDelete() .where('id = :id', { id: 1 }) .execute(); // 批量软删除 await userRepository.softDelete([1, 2, 3]);

恢复已删除记录

typescript
// 方式1:使用 recover const user = await userRepository.findOne({ where: { id: 1 }, withDeleted: true // 包含已删除的记录 }); await userRepository.recover(user); // 方式2:使用 restore await userRepository.restore(1); // 方式3:使用 QueryBuilder await userRepository .createQueryBuilder() .restore() .where('id = :id', { id: 1 }) .execute(); // 批量恢复 await userRepository.restore([1, 2, 3]);

查询已删除记录

typescript
// 查询所有记录(包括已删除的) const allUsers = await userRepository.find({ withDeleted: true }); // 只查询已删除的记录 const deletedUsers = await userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } }); // 使用 QueryBuilder 查询已删除记录 const deletedUsers = await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt IS NOT NULL') .getMany();

软删除与关系

级联软删除

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date | null; @OneToMany(() => Post, post => post.author, { cascade: true, // 级联操作 }) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @DeleteDateColumn() deletedAt: Date | null; @ManyToOne(() => User, user => user.posts) author: User; } // 软删除用户时,级联软删除所有文章 const user = await userRepository.findOne({ where: { id: 1 }, relations: ['posts'] }); await userRepository.softRemove(user);

查询关联数据时处理软删除

typescript
// 查询用户及其未删除的文章 const users = await userRepository.find({ relations: ['posts'], where: { deletedAt: IsNull() // 只查询未删除的用户 } }); // 使用 QueryBuilder 查询 const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.deletedAt IS NULL') .where('user.deletedAt IS NULL') .getMany();

高级软删除用法

自定义软删除列

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; // 使用自定义列名和类型 @DeleteDateColumn({ name: 'deleted_at', type: 'timestamp', nullable: true }) deletedAt: Date | null; // 或者使用布尔标记 @Column({ name: 'is_deleted', type: 'boolean', default: false }) isDeleted: boolean; @Column({ name: 'deleted_by', type: 'int', nullable: true }) deletedBy: number | null; }

软删除审计

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date | null; @Column({ nullable: true }) deletedBy: number | null; // 删除操作者 ID @Column({ type: 'text', nullable: true }) deleteReason: string | null; // 删除原因 } // 软删除时记录审计信息 async function softDeleteWithAudit(userId: number, deletedBy: number, reason: string) { const user = await userRepository.findOne({ where: { id: userId } }); if (!user) { throw new Error('User not found'); } user.deletedAt = new Date(); user.deletedBy = deletedBy; user.deleteReason = reason; await userRepository.save(user); }

软删除订阅者

typescript
import { EntitySubscriberInterface, EventSubscriber, SoftRemoveEvent } from 'typeorm'; @EventSubscriber() export class UserSubscriber implements EntitySubscriberInterface<User> { listenTo() { return User; } async beforeSoftRemove(event: SoftRemoveEvent<User>) { console.log('Before soft remove user:', event.entity); // 检查是否可以软删除 if (event.entity.hasActiveOrders()) { throw new Error('Cannot delete user with active orders'); } } async afterSoftRemove(event: SoftRemoveEvent<User>) { console.log('After soft remove user:', event.entity); // 记录审计日志 await this.logAudit('SOFT_DELETE', event.entity); // 发送通知 await this.sendNotification(event.entity); } async beforeRecover(event: any) { console.log('Before recover user:', event.entity); // 检查是否可以恢复 if (event.entity.emailConflict()) { throw new Error('Cannot recover user due to email conflict'); } } async afterRecover(event: any) { console.log('After recover user:', event.entity); // 记录恢复日志 await this.logAudit('RECOVER', event.entity); } private async logAudit(action: string, user: User) { // 记录审计日志的逻辑 console.log(`Audit log: ${action} user ${user.id}`); } private async sendNotification(user: User) { // 发送通知的逻辑 console.log(`Sending notification for user ${user.id}`); } }

软删除与查询构建器

在 QueryBuilder 中使用软删除

typescript
// 查询未删除的记录(默认行为) const users = await userRepository .createQueryBuilder('user') .getMany(); // 查询所有记录(包括已删除的) const allUsers = await userRepository .createQueryBuilder('user') .withDeleted() .getMany(); // 只查询已删除的记录 const deletedUsers = await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt IS NOT NULL') .getMany(); // 软删除记录 await userRepository .createQueryBuilder('user') .softDelete() .where('user.id = :id', { id: 1 }) .execute(); // 恢复记录 await userRepository .createQueryBuilder('user') .restore() .where('user.id = :id', { id: 1 }) .execute();

复杂查询中的软删除处理

typescript
// 查询用户及其未删除的文章 const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post', 'post.deletedAt IS NULL') .where('user.deletedAt IS NULL') .getMany(); // 统计未删除和已删除的用户数量 const [activeCount, deletedCount] = await Promise.all([ userRepository.count({ where: { deletedAt: IsNull() } }), userRepository.count({ withDeleted: true, where: { deletedAt: Not(IsNull()) } }) ]); console.log(`Active users: ${activeCount}, Deleted users: ${deletedCount}`);

软删除最佳实践

1. 合理使用软删除

typescript
// ✅ 好的做法:对重要数据使用软删除 @Entity() export class User { @DeleteDateColumn() deletedAt: Date | null; } @Entity() export class Order { @DeleteDateColumn() deletedAt: Date | null; } // ✅ 好的做法:对临时数据使用硬删除 @Entity() export class TempFile { // 不使用软删除,直接硬删除 } // ❌ 不好的做法:对所有数据都使用软删除 // 这会导致数据库膨胀和查询性能下降

2. 定期清理已删除记录

typescript
// 定期清理超过一定时间的已删除记录 async function cleanupOldDeletedRecords() { const retentionDays = 90; // 保留 90 天 const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); // 真正删除超过保留期的记录 await userRepository .createQueryBuilder('user') .withDeleted() .where('user.deletedAt < :cutoffDate', { cutoffDate }) .delete() .execute(); } // 使用定时任务定期执行 // setInterval(cleanupOldDeletedRecords, 24 * 60 * 60 * 1000); // 每天执行一次

3. 处理唯一约束冲突

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column({ unique: true }) email: string; @DeleteDateColumn() deletedAt: Date | null; } // 软删除用户后,可能需要恢复 async function recoverUser(userId: number) { const user = await userRepository.findOne({ where: { id: userId }, withDeleted: true }); if (!user) { throw new Error('User not found'); } // 检查邮箱是否已被使用 const existingUser = await userRepository.findOne({ where: { email: user.email } }); if (existingUser) { throw new Error('Email already in use'); } await userRepository.recover(user); }

4. 软删除与外键约束

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @DeleteDateColumn() deletedAt: Date | null; @OneToMany(() => Post, post => post.author) posts: Post[]; } @Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @DeleteDateColumn() deletedAt: Date | null; @ManyToOne(() => User, user => user.posts) @JoinColumn({ onDelete: 'SET NULL' }) // 软删除时设置为 NULL author: User | null; }

5. 软删除与索引

typescript
@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() @Index() // 为软删除列添加索引,提高查询性能 deletedAt: Date | null; } // 或者使用复合索引 @Entity() @Index(['deletedAt', 'createdAt']) // 复合索引 export class User { @DeleteDateColumn() deletedAt: Date | null; @CreateDateColumn() createdAt: Date; }

软删除性能优化

1. 使用索引优化查询

typescript
@Entity() export class User { @DeleteDateColumn() @Index('IDX_USER_DELETED_AT') // 为软删除列添加索引 deletedAt: Date | null; } // 查询未删除的用户时会使用索引 const users = await userRepository.find({ where: { deletedAt: IsNull() } });

2. 避免过度使用 withDeleted

typescript
// ❌ 不好的做法:总是查询所有记录 const users = await userRepository.find({ withDeleted: true }); // ✅ 好的做法:只在需要时查询已删除记录 const users = await userRepository.find(); const deletedUsers = await userRepository.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });

3. 使用 QueryBuilder 优化复杂查询

typescript
// ✅ 好的做法:使用 QueryBuilder 优化查询 const users = await userRepository .createQueryBuilder('user') .where('user.deletedAt IS NULL') .orderBy('user.createdAt', 'DESC') .limit(10) .getMany();

TypeORM 的软删除功能提供了强大的数据管理能力,合理使用软删除可以保护重要数据,提供审计追踪,同时保持应用的灵活性。但需要注意性能影响和定期清理策略。

标签:TypeORM