- 教程地址: v.pincman.com/courses/64.html
- 影片地址: www.bilibili.com/video/BV1Ha411Q7i...
- qq: 1849600177
- qq 群: 455820533
另,本人在找工作中,希望能有遠端工作匹配(無法去外地),有需要的老闆可以看一下我的個人介紹: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 {}
本作品採用《CC 協議》,轉載必須註明作者和本文連結