在nestjs中对文章和标签CRUD
孙泽辉 Lv5

研究了一天,该踩的坑一个不少,Typeorm用起来不太顺手。

业务分析

关于文章的增删改查:

image

这是我画的ER图:

image

实体之间关系:

文章与标签是多对多,一个文章下可以有多个标签,一个标签也可以对应多个文章

文章与用户是一对多,用户可以有多个文章,一篇文章只有一个作者(用户)

基本步骤:

创建和更新时:用户(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;

// 关联user和tag;
@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;

// 对应多个 article
@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)) {
// 文章所属tag,存在则保留,不存在即添加
const existTags = await this.tagService.findExistTags(
createArticleDto.tags,
);
const recivedTags = createArticleDto.tags.map((content) => ({ content }));
const beInsertTags = _.xorBy(recivedTags, existTags, 'content');
// 将需要添加到数据库的tag添加到article的tags中
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 { Transactional, OrmContext } from '@malagu/typeorm/lib/node';
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)

更新这里,我想了一中午

关于存在即保留,不存在即添加,将所有情况列了出来(不止下面这些)

image

觉得麻烦,遂谷歌,查出一些我知道的方法,例如upsertreplace,这些方法都存在一些问题,虽然可以插入,由于我太菜了,批量插入之后只会返回最后一个插入成功的id,所以我不知道怎么去将标签关联到文章。

以上两种方法我选择放弃,然后偶尔看到某仓库写法很巧妙,我照猫画虎的写了一个,忘记是哪个仓库了,记得是咖啡店后台管理系统来着。。。

article.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
async update(id: string, updateArticleDto: UpdateArticleDto) {
// 这里调用 tagService 的插入方法了
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;
}

// 插入 tag
async insert(tags: string[]) {
const tagEntities = [];
for (const tag of tags) {
// 统一 save 处理
const tagEntity = await this.findOrCreate(tag);
await this.repository.save(tagEntity);
tagEntities.push(tagEntity);
}
return tagEntities;
}
}

这里 findOrCreate 方法一定会返回一个数据库实体,而 repository.save 一定会返回创建好后的对象(无论存在冲突或正常插入),这样我就可以平稳的拿到所有插入后的实体,然后附加到文章关系上,也不用担心对新旧标签插入还是删除。

总结

今天一个增删改查写了一中午,其实这个存在即更新,不存在即插入,这个问题之前工作遇到过,当时是手动diff的,也不知道有replace、upsert这些。

感觉自己死脑筋,跳不出思维定势出来,有些问题也不止有一种解法,换个切入点或许效率更高。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Total words 85.5k