研究了一天,该踩的坑一个不少,Typeorm用起来不太顺手。
业务分析
关于文章的增删改查:
这是我画的ER图:
实体之间关系:
文章与标签是多对多,一个文章下可以有多个标签,一个标签也可以对应多个文章
文章与用户是一对多,用户可以有多个文章,一篇文章只有一个作者(用户)
基本步骤:
创建和更新时:用户(user)提交文章(article),解析出所包含的标签(tag),标签和用户关联到文章上。
查询时:查询所有文章,并将关联标签输出
删除时(软删除):删除指定文章,并将关联标签删除
实体定义文件
文章
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import { User } from '@user/entities/user.entity'; import { Tag } from 'src/tag/entities/tag.entity'; import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, ManyToMany, JoinTable, JoinColumn, DeleteDateColumn, } from 'typeorm'; @Entity() export class Article { @PrimaryGeneratedColumn() id: string;
@Column({ length: 255 }) title: string;
@Column({ default: '', length: 255 }) content: string;
@ManyToOne((type) => User, (user) => user.articles) user: User;
@DeleteDateColumn() deleteTime: Date;
@ManyToMany((type) => Tag, (tag) => tag.articles, { cascade: true, onDelete: 'CASCADE', onUpdate: 'CASCADE', }) @JoinTable({ name: 'article_tag', joinColumn: { name: 'tag', referencedColumnName: 'id', }, inverseJoinColumn: { name: 'article', referencedColumnName: 'id', }, }) tags: Tag[];
@Column() createTime: string;
@Column({ default: '' }) updateTime: string; }
|
标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { Article } from 'src/article/entities/article.entity'; import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, DeleteDateColumn, Index, } from 'typeorm';
@Entity() export class Tag { @PrimaryGeneratedColumn() id?: number;
@Column() @Index({ unique: true }) content: string;
@Column({ default: false }) is_topics: boolean;
@ManyToMany((type) => Article, (article) => article.tags) articles: Article[];
@DeleteDateColumn() deleteTime: Date; }
|
用户
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { Article } from './../../article/entities/article.entity'; import { Entity, OneToMany, PrimaryGeneratedColumn, } from 'typeorm';
@Entity() export class User { @OneToMany((type) => Article, (article) => article.user) articles: Article; }
|
看起来也挺简单哈,写吧!
创建(Create)
在创建时,需要判断前端传入的标签存不存在,如果存在则保持不变,不存在则添加入库。
区分标签的方法就是看内容,因为标签不可能重复,所以我设置了唯一索引(见标签实体定义)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import { UserService } from './../user/user.service'; import { TagService } from './../tag/tag.service'; import { Repository } from 'typeorm'; import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; import { Article } from './entities/article.entity'; import { InjectRepository } from '@nestjs/typeorm'; import * as _ from 'lodash';
@Injectable() export class ArticleService { constructor( @InjectRepository(Article) private readonly repository: Repository<Article>, private readonly tagService: TagService, private readonly userService: UserService, ) {} async create(userId: string, createArticleDto: CreateArticleDto) { const articleDO = { title: createArticleDto.title, content: createArticleDto.content, createTime: new Date().toString(), updateTime: new Date().toString(), user: await this.userService.findUser(userId), tags: [], }; const article = this.repository.create(articleDO); if (!_.isEmpty(createArticleDto.tags)) { const existTags = await this.tagService.findExistTags( createArticleDto.tags, ); const recivedTags = createArticleDto.tags.map((content) => ({ content })); const beInsertTags = _.xorBy(recivedTags, existTags, 'content'); const beInsertTagEntities = beInsertTags.map((tag) => this.tagService.create(tag)); article.tags = _.uniqBy( _.concat(beInsertTagEntities, existTags), 'content', ); }
return await this.repository.save(article); } }
|
如果前端有传入标签(tags)的话,我要判断他是否已经在数据库了,用_.xorBy
,这个是取对称差集(symmetric_difference),而不是差集(difference)!
关于介绍差集和对称差集的文章:python 并集union, 交集intersection, 差集difference, 对称差集symmetric_difference_Python学习者的技术博客_51CTO博客
另外某lodash中文文档将_.xor
翻译成差集,害我怀疑学的假数学!
读取(Retrieve)和 删除(Delete)
读取和删除都挺简单的,放在一起了
在 article.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import { UserService } from './../user/user.service'; import { TagService } from './../tag/tag.service'; import { Repository } from 'typeorm'; import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { CreateArticleDto } from './dto/create-article.dto'; import { UpdateArticleDto } from './dto/update-article.dto'; import { Article } from './entities/article.entity'; import { InjectRepository } from '@nestjs/typeorm'; import * as _ from 'lodash';
@Injectable() export class ArticleService { constructor( @InjectRepository(Article) private readonly repository: Repository<Article>, private readonly tagService: TagService, private readonly userService: UserService, ) {} findAll(user: string) { return this.repository.find({ where: { user: { id: user } }, relations: ['user', 'tags'], }); } async remove(id: string) { const article = await this.repository.findOne({ where: { id }, relations: ['tags'], }); if (_.isEmpty(article)) { throw new UnprocessableEntityException('文章不存在!'); } return this.repository.softRemove(article); } }
|
更新(Update)
更新这里,我想了一中午
关于存在即保留,不存在即添加,将所有情况列了出来(不止下面这些)
觉得麻烦,遂谷歌,查出一些我知道的方法,例如upsert
、replace
,这些方法都存在一些问题,虽然可以插入,由于我太菜了,批量插入之后只会返回最后一个插入成功的id,所以我不知道怎么去将标签关联到文章。
以上两种方法我选择放弃,然后偶尔看到某仓库写法很巧妙,我照猫画虎的写了一个,忘记是哪个仓库了,记得是咖啡店后台管理系统来着。。。
在article.service.ts
:
1 2 3 4 5 6 7 8 9 10 11 12
| async update(id: string, updateArticleDto: UpdateArticleDto) { const tags = await this.tagService.insert(updateArticleDto.tags); const articleDO: Partial<Article> = { id: id, title: updateArticleDto.title, content: updateArticleDto.content, updateTime: new Date().toString(), tags, }; return await this.repository.save(articleDO); }
|
继续看 tag.service.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import { ArticleService } from './../article/article.service'; import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { CreateTagDto } from './pojo/create-tag.dto'; import { UpdateTagDto } from './pojo/update-tag.dto'; import { Tag } from './entities/tag.entity'; import * as _ from 'lodash'; @Injectable() export class TagService { constructor( @InjectRepository(Tag) private repository: Repository<Tag>, ) {} create(createTagDto: CreateTagDto) { return this.repository.create(createTagDto); } async findOrCreate(tag: string) { let tagEntity = await this.repository.findOne({ where: { content: tag }, }); if (!tagEntity) { tagEntity = await this.repository.save({ content: tag }); } return tagEntity; }
async insert(tags: string[]) { const tagEntities = []; for (const tag of tags) { const tagEntity = await this.findOrCreate(tag); await this.repository.save(tagEntity); tagEntities.push(tagEntity); } return tagEntities; } }
|
这里 findOrCreate
方法一定会返回一个数据库实体,而 repository.save
一定会返回创建好后的对象(无论存在冲突或正常插入),这样我就可以平稳的拿到所有插入后的实体,然后附加到文章关系上,也不用担心对新旧标签插入还是删除。
总结
今天一个增删改查写了一中午,其实这个存在即更新,不存在即插入,这个问题之前工作遇到过,当时是手动diff的,也不知道有replace、upsert
这些。
感觉自己死脑筋,跳不出思维定势出来,有些问题也不止有一种解法,换个切入点或许效率更高。