关于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 {}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理