学习目标

  • 批量删除数据
  • 软删除与回收站功能
  • 恢复软删除的数据

应用编码

核心代码

BaseDataService

添加一个枚举来确定查询类型

// src/core/constants.ts
export enum QueryTrashMode {
    ALL = 'all', // 包含已软删除和未软删除的数据
    ONLY = 'only', // 只包含软删除的数据
    NONE = 'none', // 只包含未软删除的数据
}

新增(修改)8个方法,分别用于删除和恢复数据

已经被软删除后的数据再次删除则直接硬删除

  • getListQuery: 修改后为普通模型添加软删除查询支持
  • list: 修改后为树形模型添加软删除查询支持
  • delete: 修改后使其支持软删除
  • deleteList: 根据ID列表批量删除数据,并支持软删除,返回结果为删除后重新查询的数据列表
  • deletePaginate: 根据ID列表批量删除数据,并支持软删除,返回结果为删除数据后重新查询的分页结果
  • restore: 恢复软删除后的数据
  • restoreList:根据ID列表批量恢复数据,返回结果为恢复后重新查询的数据列表
  • restorePaginate: 根据ID列表批量恢复数据,返回结果为恢复数据后重新查询的分页结果

属性及方法如下

  • params: 数据列表查询参数
  • trash参数: 是否软删除
  • callback: 额外的query构建回调函数
  • pageOptions: 分页设置参数
// src/core/base/data.service.ts
export abstract class BaseDataService<...> {
    ...
    // 是否启用软删除功能                  
    protected enable_trash: boolean = false;
    // 查询列表数据
    async list(params?: P, callback?: QueryHook): Promise {
        if (this.repository instanceof BaseTreeRepository) {
            const trashed = params ? params.trashed : undefined;
            const nparams = params ?? {};
            const tree = await this.repository.findTree(
                trashed
                    ? {
                          ...nparams,
                          withTrashed: true,
                      }
                    : nparams,
            );

            const data = await this.repository.toFlatTrees(tree);
            if (trashed === QueryTrashMode.ONLY) {
                return data.filter((i) => (i as any).deletedAt);
            }
            if (trashed === QueryTrashMode.NONE) {
                return data.filter((i) => !(i as any).deletedAt);
            }
            return data;
        }
       ...
    }
    // 获取查询数据列表的 QueryBuilder
    protected async getListQuery(
        query: SelectQueryBuilder,
        params: P,
        callback?: QueryHook,
    ) {
        const { trashed } = params;
        // 是否查询回收站
        if (trashed === QueryTrashMode.ALL || trashed === QueryTrashMode.ONLY) {
            query.withDeleted();
            if (trashed === QueryTrashMode.ONLY) {
                const trashCond = `post.deletedAt is not null`;
                query.where(trashCond);
            }
        }
        if (callback) return callback(query);
        return query;
    }
    // 删除数据
    async delete(id: string, trash = true): Promise

    // 批量删除数据
    async deleteList(
        data: string[],
        params?: P,
        trash?: boolean,
        callback?: QueryHook,
    ):Promise

    // 批量删除数据(分页)
    async deletePaginate(
        data: string[],
        pageOptions: PaginateDto,
        params?: P,
        trash?: boolean,
        callback?: QueryHook,
    ):Promise>

    // 恢复回收站中的数据
    async restore(id: string, callback?: QueryHook): Promise 

    // 批量恢复回收站中的数据
    async restoreList(data: string[], params?: P, callback?: QueryHook) {
        for (const id of data) {
            await this.restore(id);
        }
        return this.list(params, callback);
    }:Promise

    // 批量恢复回收站中的数据(分页)
    async restorePaginate(
        data: string[],
        pageOptions: PaginateDto,
        params?: any,
        callback?: QueryHook,
    ):Promise>
}

BaseSubscriber

修改SubcriberSetting

//src/core/types.ts
export type SubcriberSetting = {
    trash?: boolean;
    tree?: boolean;
};

为读取后的数据添加一个trashed(回收站)属性来确定该数据是否已被软删除

// src/core/base/subscriber.ts
...
async afterLoad(entity: any) {
    // 是否启用软删除
    if (this.setting.trash) entity.trashed = !!entity.deletedAt;
    // 是否启用树形
    if (this.setting.tree && !entity.level) entity.level = 0;
}

BaseTreeRepository

为树形数据查询添加软删除支持

修改查询参数类型

// src/core/types.ts
export type TreeQueryParam = {
    getQuery?: (query: SelectQueryBuilder) => SelectQueryBuilder;
    orderBy?: OrderQueryType;
    withTrashed?: boolean;
};

修改findTree,findRts,createDtsQueryBuilder,createAtsQueryBuilder以使其支持软删除

// src/core/base/tree.repository.ts
export abstract class BaseTreeRepository<
    E extends ObjectLiteral,
> extends TreeRepository {
  ...
  async findTree(params: TreeQueryParam = {}): Promise {
        params.withTrashed = params.withTrashed ?? false;
        ...
  }
  findRts(params: TreeQueryParam = {}): Promise {
        const { getQuery, orderBy, withTrashed } = params;
        ...
        if (withTrashed) query.withDeleted();
        return query.getMany();
  }
  createDtsQueryBuilder(
        alias: string,
        closureTableAlias: string,
        entity: E,
        params: TreeQueryParam = {},
    ): SelectQueryBuilder {
        const { getQuery, orderBy, withTrashed } = params;
        ...
        return withTrashed ? query.withDeleted() : query;
   }
   createAtsQueryBuilder(
        alias: string,
        closureTableAlias: string,
        entity: E,
        params: TreeQueryParam = {},
    ): SelectQueryBuilder {
        const { getQuery, withTrashed, orderBy } = params;
        ...
        return withTrashed ? query.withDeleted() : query;
   }
}

修改应用

模型

CategoryEntityPostEntity添加deteledAt字段

// src/modules/content/entities/category.entity.ts
export class CategoryEntity extends BaseEntity {
  ...
    @Expose()
    @Type(() => Date)
    @CreateDateColumn({
        comment: '创建时间',
    })
    createdAt!: Date;

    @Expose()
    trashed!: boolean;
}

观察者

CategorySubscriberPostSubscriber添加软删除设置,以CategorySubscriber为例,如下

// src/modules/content/subscribers/category.subscriber.ts
@EventSubscriber()
export class CategorySubscriber extends BaseSubscriber {
    ...
    protected setting: SubcriberSetting = {
        tree: true,
        trash: true,
    };
}

服务

CategoryServicePostService启用软删除功能

CommentService无需启用

// src/modules/content/services/category.service.ts
export class CategoryService extends BaseDataService {
    ...
    protected enable_trash = true;
}

DTO

添加软删除,批量删除等相关的数据验证

category.dto.tspost.dto.ts是一致的,由于comment没有软删除,所以只要批量删除验证即可.以category.dto.ts为例,代码如下

// src/modules/content/dtos/category.dto.ts
// 删除分类数据验证
@Injectable()
@DtoValidation()
export class DeleteCategoryDto {
    // 是否软删除
    @IsBoolean()
    @IsOptional()
    @Transform(({ value }) => tBoolean(value))
    trash?: boolean;
}

// 分类批量删除数据验证
@Injectable()
@DtoValidation()
export class DeleteCategoryMultiDto extends DeleteCategoryDto {
    @IsUUID(undefined, {
        each: true,
        message: '分类ID格式错误',
    })
    @IsDefined({
        each: true,
        message: '分类ID必须指定',
    })
    categories: string[] = [];
}
// 分类批量恢复数据验证
@Injectable()
@DtoValidation()
export class RestoreCategoryMultiDto extends PickType(DeleteCategoryMultiDto, [
    'categories',
] as const) {}

为需要启用软删除的Query查询添加trashed可选验证,然后在category.dto.tspost.dto.tsquery中添加字段

// src/modules/content/dtos/category.dto.ts
export class QueryCategoryDto implements PaginateDto {
    ...
    @IsEnum(QueryTrashMode)
    @IsOptional()
    trashed?: QueryTrashMode;
}

控制器

CategoryControllerPostController修改原来的删除方法,使其支持软删除查询,并添加批量删除,数据恢复,批量恢复功能,而同样的,CommentController因为不需要软删除功能,所以添加一个批量删除方法即可,以CategoryController为例,代码如下

// src/modules/content/controllers/category.controller.ts
export class CategoryController {
    ...
    // 删除分类
    @Delete(':category')
    @SerializeOptions({ groups: ['category-detail'] })
    async destroy(
        @Body()
        { trash }: DeleteCategoryDto,
        @Param('category', new ParseUUIDPipe())
        category: string,
    ) {
        return this.categoryService.delete(category, trash);
    }

    // 批量删除分类
    @Delete()
    @SerializeOptions({ groups: ['category-list'] })
    async destroyMulti(
        @Query()
        { page, limit }: QueryCategoryDto,
        @Body()
        { trash, categories }: DeleteCategoryMultiDto,
    )

    // 恢复分类
    @Patch('restore/:category')
    @SerializeOptions({ groups: ['category-detail'] })
    async resore(
        @Param('category', new ParseUUIDPipe())
        category: string,
    )

    // 批量恢复分类
    @Patch('restore')
    @SerializeOptions({ groups: ['category-list'] })
    async restoreMulti(
        @Query()
        { page, limit }: QueryCategoryDto,
        @Body()
        { categories }: DeleteCategoryMultiDto,
    )
}