乐趣区

关于typescript:Nestjs最佳实践教程3模型关联与树形嵌套

  • 视频地址: https://www.bilibili.com/vide…

有问题请扫描视频中 qq 群二维码交换

另,自己在找工作中,心愿能有近程工作匹配(无奈去当地),有须要的老板能够看一下我的集体介绍:pincman.com/about

学习指标

这次教程在上一节的根底上实现一个简略的 CMS 零碎, 实现如下性能

  • 文章与分类多对多关联
  • 文章与评论一对多关联
  • 分类与评论的树形有限级嵌套

文件构造

这次的更改集中于 ContentModule 模块, 编写好之后的目录构造如下

src/modules/content
├── content.module.ts
├── controllers
│   ├── category.controller.ts
│   ├── comment.controller.ts
│   ├── index.ts
│   └── post.controller.ts
├── dtos
│   ├── create-category.dto.ts
│   ├── create-comment.dto.ts
│   ├── create-post.dto.ts
│   ├── index.ts
│   ├── update-category.dto.ts
│   └── update-post.dto.ts
├── entities
│   ├── category.entity.ts
│   ├── comment.entity.ts
│   ├── index.ts
│   └── post.entity.ts
├── repositories
│   ├── category.repository.ts
│   ├── comment.repository.ts
│   ├── index.ts
│   └── post.repository.ts
└── services
    ├── category.service.ts
    ├── comment.service.ts
    ├── index.ts
    └── post.service.ts
cd src/modules/content && \
touch controllers/category.controller.ts \
controllers/comment.controller.ts \
dtos/create-category.dto.ts \
dtos/create-comment.dto.ts \
dtos/update-category.dto.ts \
entities/category.entity.ts \
entities/comment.entity.ts \
repositories/category.repository.ts \
services/category.service.ts \
services/comment.service.ts \
&& cd ../../../

利用编码

编码流程与上一节一样,entity->repository->dto->service->controller, 最初注册

模型类

模型关联

别离创立分类模型 (CategoryEntity) 和评论模型 (CommentEntity), 并和PostEntity 进行关联

分类模型

// src/modules/content/entities/category.entity.ts
@Entity('content_categories')
export class CategoryEntity extends BaseEntity {
  ...
    // 分类与文章多对多关联
    @ManyToMany((type) => PostEntity, (post) => post.categories)
    posts!: PostEntity[];}

评论模型

// src/modules/content/entities/comment.entity.ts
@Entity('content_comments')
export class CommentEntity extends BaseEntity {
  ...
   // 评论与文章多对一, 并触发 `CASCADE`
    @ManyToOne(() => PostEntity, (post) => post.comments, {
        nullable: false,
        onDelete: 'CASCADE',
        onUpdate: 'CASCADE',
    })
    post!: PostEntity;
}

文章模型

@Entity('content_posts')
export class PostEntity extends BaseEntity {
    // 评论数量
    // 虚构字段, 在 Repository 中通过 QueryBuilder 设置
    commentCount!: number;

    // 文章与分类反向多对多关联
    @ManyToMany((type) => CategoryEntity, (category) => category.posts, {cascade: true,})
    @JoinTable()
    categories!: CategoryEntity[];
    // 文章与评论一对多关联
    @OneToMany(() => CommentEntity, (comment) => comment.post, {cascade: true,})
    comments!: CommentEntity[];}

树形嵌套

评论模型与分类模型的树形嵌套实现基本一致, 惟一的区别在于在删除父分类时子分类不会删除而是晋升为顶级分类, 而删除评论则连带删除其后辈评论

typeorm 有三种计划实现树形嵌套模型, 咱们应用综合来说最好用的一种, 即物理门路(Materialized Path), 起因在于 Adjacency list 的毛病是无奈一次加载整个树, 而 closure 则无奈主动触发Cascade

// src/modules/content/entities/category.entity.ts
@Entity('content_categories')
// 物理门路嵌套树须要应用 `@Tree` 装璜器并以 'materialized-path' 作为参数传入
@Tree('materialized-path')
export class CategoryEntity extends BaseEntity {
  ...
    // 子分类
    @TreeChildren({cascade: true})
    children!: CategoryEntity[];
    // 父分类
    @TreeParent({onDelete: 'SET NULL'})
    parent?: CategoryEntity | null;
}
    
// src/modules/content/entities/comment.entity.ts
@Entity('content_comments')
@Tree('materialized-path')
export class CommentEntity extends BaseEntity {
    ...
    @TreeChildren({cascade: true})
    children!: CommentEntity[];
    @TreeParent({onDelete: 'CASCADE'})
    parent?: CommentEntity | null;
}

存储类

创立一个空的 CategoryRepository 用于操作 CategoryEntity 模型

留神 : 树形的存储类必须通过getTreeRepository 获取或者通过 getCustomRepository 加载一个继承自 TreeRepository 的类来获取

在 [nestjs][] 中注入树形模型的存储库应用以下办法

  • 应用该模型的存储库类是继承自 TreeRepository 类的自定义类, 则间接注入即可
  • 如果没有存储库类就须要在注入的应用 TreeRepository<Entity> 作为类型提醒

为了简略,CommentRepository临时不须要创立, 间接注入服务即可

// src/modules/content/repositories/category.repository.ts
@EntityRepository(CategoryEntity)
export class CategoryRepository extends TreeRepository<CategoryEntity> {}

批改 PostRepository 增加 buildBaseQuery 用于服务查问, 代码如下

// src/modules/content/repositories/post.repository.ts
buildBaseQuery() {return this.createQueryBuilder('post')
            // 退出分类关联
            .leftJoinAndSelect('post.categories', 'categories')
            // 建设子查问用于查问评论数量
            .addSelect((subQuery) => {
                return subQuery
                    .select('COUNT(c.id)', 'count')
                    .from(CommentEntity, 'c')
                    .where('c.post.id = post.id');
            }, 'commentCount')
            // 把评论数量赋值给虚构字段 commentCount
            .loadRelationCountAndMap('post.commentCount', 'post.comments');
    }

DTO 验证

DTO 类与后面的 CreatePostDtoUpdatePostDto写法是一样的

评论无需更新所以没有 update 的 DTO

  • create-category.dto.ts用于新建分类
  • update-category.dto.ts用于更新分类
  • create-comment.dto.ts用于增加评论

在代码中能够看到我这里对分类和评论的 DTO 增加了一个 parent 字段用于在创立和更新时设置他们的父级

@Transform装璜器是用于转换数据的, 基于 class-transformer 这个类库实现, 此处的作用在于把申请中传入的值为 null 字符串的 parent 的值转换成实在的 null 类型

@ValidateIf的作用在于 只在申请的 parent 字段不为 null 且存在值的时候进行验证 , 这样做的目标在于如果在更新时设置parentnull把以后分类设置为顶级分类, 如果不传值则不扭转

// src/modules/content/dtos/create-category.dto.ts
    @IsUUID(undefined, { always: true, message: '父分类 ID 格局不正确'})
    @ValidateIf((p) => p.parent !== null && p.parent)
    @IsOptional({always: true})
    @Transform(({value}) => (value === 'null' ? null : value))
    parent?: string;

CreatePostDto 中增加分类 IDS 验证

// src/modules/content/dtos/create-post.dto.ts
   @IsUUID(undefined, { each: true, always: true, message: '分类 ID 格局谬误'})
   @IsOptional({always: true})
   categories?: string[];

CreateCommentDto 中增加一个文章 ID 验证

// src/modules/content/dtos/create-comment.dto.ts
    @IsUUID(undefined, { message: '文章 ID 格局谬误'})
    @IsDefined({message: '评论文章 ID 必须指定'})
    post!: string;

服务类

Category/Comment

服务的编写根本与 PostService 统一, 咱们新增了以下几个服务

  • CategoryService用于分类操作
  • CommentService用于评论操作

分类服务通过 TreeRepository 自带的 findTrees 办法可间接查问出树形构造的数据, 然而此办法无奈增加查问条件和排序等, 所以后续章节咱们须要本人增加这些

// src/modules/content/services/category.service.ts
export class CategoryService {
    constructor(
        private entityManager: EntityManager,
        private categoryRepository: CategoryRepository,
    ) {}

    async findTrees() {return this.categoryRepository.findTrees();
    }
    ...

getParent办法用于依据申请的 parent 字段的 ID 值获取分类和评论下的父级

protected async getParent(id?: string) {
        let parent: CommentEntity | undefined;
        if (id !== undefined) {if (id === null) return null;
            parent = await this.commentRepository.findOne(id);
            if (!parent) {throw new NotFoundException(`Parent comment ${id} not exists!`);
            }
        }
        return parent;
    }

PostService

当初为了读取和操作文章与分类和评论的关联, 应用 QueryBuilder 来构建查询器

在此之前, 在 core/types(新增) 中定义一个用于额定传入查问回调参数的办法类型

// src/core/types.ts

/**
 * 为 query 增加查问的回调函数接口
 */
export type QueryHook<Entity> = (hookQuery: SelectQueryBuilder<Entity>,) => Promise<SelectQueryBuilder<Entity>>;

PostService更改

对于评论的嵌套展现在后续教程会从新定义一个新的专用接口来实现

  • create时通过 findByIds 为新增文章出查问关联的分类
  • update时通过 addAndRemove 更新文章关联的分类
  • 查问时通过 .buildBaseQuery().leftJoinAndSelect 为文章数据增加上关联的评论

控制器

新增两个控制器, 别离用于解决分类和评论的申请操作

CategoryContoller

办法与 PostController 一样,index,show,store,update,destory

临时间接用 findTrees 查问出树形列表即可

export class CategoryController {
  ...
    @Get()
    async index() {return this.categoryService.findTrees();
    }
}

CommentController

目前评论控制器只有两个办法 storedestory, 别离用于新增和删除评论

注册代码

别离在 entities,repositories,dtos,services,controllers 等目录的 index.ts 文件中导出新增代码以给 ContentModule 进行注册

const entities = Object.values(EntityMaps);
const repositories = Object.values(RepositoryMaps);
const dtos = Object.values(DtoMaps);
const services = Object.values(ServiceMaps);
const controllers = Object.values(ControllerMaps);
@Module({
    imports: [TypeOrmModule.forFeature(entities),
        // 注册自定义 Repository
        CoreModule.forRepository(repositories),
    ],
    controllers,
    providers: [...dtos, ...services],
    exports: [
        // 导出自定义 Repository, 以供其它模块应用
        CoreModule.forRepository(repositories),
        ...services,
    ],
})
export class ContentModule {}
退出移动版