共计 6695 个字符,预计需要花费 17 分钟才能阅读完成。
- 视频地址: 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 类与后面的 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 {}