如何利用typeorm+mysql设计博客系统(二)
2、typeorm设计数据库
我们的博客系统包含以下表:
- article: 该表存储每一篇博文,包含博文标题、正文、博文状态、发布时间、更新时间等数据
- category: 该表存储所有的分类,与article是一个一对多的关系。
- archive: 该表存储所有的归档时间,与article是一个一对多的关系
- tags: 该表存储所有的标签,与article是一个多对多的关系
- user: 该表存储的所有用户,与article是一个一对多的关系
- reader: 该表存储的是每一篇文章的阅读数据
表与表之间的的图形化ERD(entity relation diagram)图如下:
最主要的Article表的具体的typeorm实现如下:
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, ManyToMany, JoinTable, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { User } from './user';
import { Tag } from './tag';
import { Archive } from './archive';
import { Category } from './category';
export enum ArticleStatus {
DRAFT = "draft",
PUBLISHED = "published",
}
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number;
// 文章标题
@Column({
type: "varchar",
length: 200,
nullable: false
})
title: string;
// 文章阅读量
@Column('bigint')
pv: number;
// 博文主体,markdown格式,注意这里的类型必须是Longtext,否则保存不了一篇正常的文章。
@Column('longtext')
content: string;
// 博文链接
@Column('varchar')
@Index({unique: true})
slug: string;
// 博文摘要,这次需要自己填写,不再自动从文章采集字符了,那样实现不准确
@Column({
type: "varchar",
length: 200,
nullable: false
})
digest: string;
// 博文状态,默认草稿中
@Column({
type: "enum",
enum: ArticleStatus,
default: ArticleStatus.DRAFT
})
articleStatus: ArticleStatus;
// 文章首页图片链接
@Column({
type: "varchar",
length: 200,
nullable: false
})
illustration: string;
// 文章作者
@ManyToOne(type => User, user => user.articles)
author: User;
// 一篇文章可以包含多个tag,所以这里是多对多的关系
@ManyToMany(type => Tag, tag => tag.articles)
@JoinTable()
tags: Tag[];
// 以日为单位
@Column({
type: "varchar",
length: 200,
nullable: false
})
fullArchiveTime: string;
// 文章的归档时间, 月份为单位
@ManyToOne(type => Archive, archive => archive.articles)
archiveTime: Archive;
// 文章的分类
@ManyToOne(type => Category, category => category.articles)
category: Category;
@CreateDateColumn()
public readonly createdAt!: Date;
@UpdateDateColumn()
public readonly updatedAt!: Date;
}
更多代码参考:传送门
3、博客系统的功能点分析
一套简易的博客系统需要实现的功能点有以下几个:
- 可以新建博客,然后可以根据slug进行博客的更新;
- 可以根据创建时间的顺序分页获取博客列表
- 可以根据标签ID分页获取与其相关的博客列表(按照创建时间顺序)
- 可以根据分类ID分页获取与其相关的博客列表(按照创建时间顺序)
- 可以根据归档ID分页获取与其相关的博客列表(按照创建时间顺序)
- 根据slug获取博客详情
- 可以获取pv最高的前五篇文章作为热门文章
从上面的功能点我们可以将其归纳为三个接口:
- 新建或者更新博客接口:对功能点1的实现
- 根据各种条件分页获取博客列表:对功能点2、3、4、5、7的实现
- 获取博客详情:对功能点6的实现
因此从上面来看,最复杂的应该是博客列表的获取了,在继续讲解博客列表的获取之前,我们来说说typeorm初学者的一些坑。
4、typeorm填坑指南
4.1、repo.save
此次并没有用到repo.create
这个方法,因为从官网文档上看,save
具备了createOrUpdate
的功能了,create
这个好理解,但是save是如何实现update的呢?官网是这么解释的:
Saves a given entity or array of entities. If the entity already exist in the database, it is updated. If the entity does not exist in the database, it is inserted.
如果该entity存在于数据库,那么就更新。那么问题来了,entity怎么判定存在呢?答案就是你必须指定id或者index的字段,可以参考 这个问题的回答:stackoverflow。
刚开始创建博客的时候,因为归档时间这个字段我是想着可以使用save来实现的,如果文章的归档时间不存在,那么创建,如果存在则不
做任何操作,因此设计Archive
这个表的时候,给archiveTime加索引了:
@Column({
type: "varchar",
length: 200,
nullable: false
})
@Index({ unique: true })
archiveTime: string;
然后创建博文的时候写Archive的代码是:
const instance = new Archive()
instance.archiveTime = archiveTime
await archiveRepo.save(instance);
当archiveTime一样的话,以为保存成功,结果不是,直接给你报错:
所以save这个会存在duplicate key
的错误,因此,还是改成了提前先查找是否有该条数据,没有的话才save。
这是save遇到的一个坑。也由此可见,即使你的archiveTime一样,在save的实现上仍然认为是一个新的记录,所以用Insert的语法。
3.2、repo.update
第二个坑是以为调用update
犯法可以更新博文(包括其附属的分类、标签关系)就万事大吉了,结果又报错了:unknown column field 'aid' in 'field list'
,这种让人抓不到头脑的错误,想想自己没有aid
这个字段啊?后面调试typeorm的代码,发现是错在了tag这个多对多的关系上。aid
这个字段其实就是其关联表article_tags_tag
中的字段。
一开始我是设计joinTable的时候指定中间表的字段为aid的,但是自己却忘掉了!
后面搜了搜谷歌,发现update这个方法不适用于这种关系的更新,更新这种数据只能使用save方法,具体解释在:传送 门
5、博文列表获取的代码设计
为了精简博客列表的设计,可以支持普通查询,也可以支持根据标签id、分类id、归档id查询,于是设计了fetchArticleList
这个方法,
别的查询都比较常规,主要是tag这个的查询,普通的方法不能支持这种查询,只有使用queryBuilder
了,最后的实现是:
if (condition.queryTag) {
// 多对多的关系比较特殊,find不能满足需求
let orderField = 'article.createdAt';
let orderDef: "ASC" | "DESC" = 'DESC';
if (order) {
// 排序字段仅支持1个字段
Object.keys(order).forEach(item => {
orderField = `article.${item}`
orderDef = order[item]
})
}
let queryBuilder = repo.createQueryBuilder('article')
if (condition.articleStatus) {
queryBuilder.where('article.articleStatus = :status', { status: condition.articleStatus })
}
// 使用 Inner JOIN的语法查询包含指定标签ID的所有文章
[list, count] = await queryBuilder
.innerJoin('article.tags', 'tag', 'tag.id IN (:...tagId)', { tagId: condition.queryTag })
.skip((currentPage - 1) * (pageSize ? pageSize : PAGE_SIZE))
.take(pageSize? pageSize : PAGE_SIZE)
.orderBy(orderField, orderDef)
.innerJoinAndSelect('article.tags', 'tags') // 级联取出对应的表结构
.innerJoinAndSelect('article.category', 'category')
.innerJoinAndSelect('article.archiveTime', 'archiveTime')
.innerJoinAndSelect('article.author', 'author')
.getManyAndCount()
}
至此,整套简易的博客系统便开发完成,上述代码不涉及任何框架,大家有需要的话可以直接扣过去使用,当然前提是使用大致一样的表 结构。
新的豆米博客已经上线了,欢迎大家多多访问,提提意见~
参考
公众号关注一波~
![微信公众号](https://blogimage.5udou.cn/homePage/%E5%85%AC%E4%BC%97%E5%8F%B7-%E7%99%BD%E8%89%B2%E7%89%88.png?x-oss-process=style/addWaterMarkBottom)
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 如何利用typeorm+mysql设计博客系统(二) 的内容有疑问,请在下面的评论系统中留言,谢谢。