如何利用typeorm+mysql设计博客系统(二)

发表于 2020-02-20
更新于 2024-05-23
分类于 技术专栏
阅读量 409
字数统计 5761

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、博客系统的功能点分析

一套简易的博客系统需要实现的功能点有以下几个:

  1. 可以新建博客,然后可以根据slug进行博客的更新;
  2. 可以根据创建时间的顺序分页获取博客列表
  3. 可以根据标签ID分页获取与其相关的博客列表(按照创建时间顺序)
  4. 可以根据分类ID分页获取与其相关的博客列表(按照创建时间顺序)
  5. 可以根据归档ID分页获取与其相关的博客列表(按照创建时间顺序)
  6. 根据slug获取博客详情
  7. 可以获取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()
}

至此,整套简易的博客系统便开发完成,上述代码不涉及任何框架,大家有需要的话可以直接扣过去使用,当然前提是使用大致一样的表 结构。

新的豆米博客已经上线了,欢迎大家多多访问,提提意见~

参考

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 如何利用typeorm+mysql设计博客系统(二) 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github