
另,本人在找工作中,希望能有远程工作匹配(无法去外地),有需要的老板可以看一下我的个人介绍: https://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((sbQuery) => { return subQuery .select('COUNT(c.id)', 'count') .from(CommentEntity, 'c') .where('c.post.id = post.id'); }, 'commentCount') // 把评论数量赋值给虚拟字段 commentCount .loadRelationCountAndMap('post.commentCount', 'post.comments'); } DTO 类与前面的CreatePostDto和UpdatePostDto写法是一样的
评论无需更新所以没有
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且存在值的时候进行验证,这样做的目的在于如果在更新时设置parent为null把当前分类设置为顶级分类,如果不传值则不改变
// 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目前评论控制器只有两个方法store和destory,分别用于新增和删除评论
分别在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 {} 1 lovepocky 2022-07-10 10:07:36 +08:00 via iPhone 这目录结构是哪学的? spring 吗? |
3 lovepocky 2022-07-10 10:28:43 +08:00 via iPhone @lichnow 多看看官方文档 |
5 oppddd 2022-08-15 09:56:09 +08:00 官方应该是一个 module 一个文件夹,对应的 controller 还有 service 在一起;你这个是把不同的业务放在了一起; |