# TypeORM
在项目中,我们使用的数据库驱动为 MySQL
Midway
官方也提供了 TypeORM 组件 (opens new window),相关的使用案例都挺好的,对 TypeORM
还不是很熟悉的同学建议先看 Midway
提供的相关案列
对于 Midway
文档没有中没有提到的内容,可以查阅以下文档
中文文档(github) (opens new window) 、官方文档 (opens new window)
# 实体类
# 何为实体类
可以简单的理解为实体类就是数据库表结构的一种描述
- 如果不好理解,可以简单理解为前端的
Virtual DOM
,Virtual DOM
它就是一个JS
对象,是用 通过JS
语言去描述一个DOM
的结构
- 如果不好理解,可以简单理解为前端的
每一个实体类就对应着数据库中的一个表,
- 通常实体类的名称与数据库表的名称是一一对应的,添加
@Entity()
注解时,默认就会通过实体类的名称去匹配数据库中表 - 当然也可以去做自定义,
@Entiry("user_info")
,那么此时User
这个实体类对应的表就是user_info
这张表
- 通常实体类的名称与数据库表的名称是一一对应的,添加
实体类中的每一个属性对应着数据库中的一个
column
(列),同样也对应着装饰器@Column
# 数据库建表规范
感兴趣的同学可以查阅 阿里巴巴 Java 开发手册 (opens new window)
# 常用操作
基本的增删改查就不写了,就写两个我觉得还比较有意思的
# Brackets 的使用
# 获取当月需要展示的所有事项
这个页面的数据是怎么查出来的呢?
前端传一个 startTime: 2022-09-25
和 endTime:2022-11-05
。获取这一时间区间的所有的事项;
后端只需要判断事项的startTime
和 endTime
有没有在前端传递的这个时间区间内就好了;
tips: 关于 find
的使用文档在这 Find 选项 (opens new window)
const eventList = await this.eventModel.find({
where: [
{
userId,
startTime: Between(startTime, endTime),
},
{
userId,
endTime: Between(startTime, endTime),
},
],
});
2
3
4
5
6
7
8
9
10
11
12
恩,看起来好像没啥大问题,但这个世界上还存在跨月这种事项,这种情况下,后端的这个查询就懵了。。。
比如说,上面的这个事项,它的开始时间为 09月02日 结束时间为 11月11日。当时间在 9 月 和 11月 目前的查询条件还能适用,但是如果时间到了 10
月呢 ?恩,匹配不上了!
为了解决这个问题,我们还需要添加一个判断条件,事项的开始/结束时间 是否 小于/大于 区间时间。
在这里我们使用 Brackets
这个特性,它可以将 复杂的 WHERE
表达式添加到现有的WHERE
中。
const eventList = await this.eventModel
.createQueryBuilder('eventInfo')
.where('eventInfo.userId =:userId', { userId })
.andWhere(
new Brackets(qb => {
qb.where({
startTime: Between(startTime, endTime),
})
.orWhere({
endTime: Between(startTime, endTime),
})
.orWhere({
startTime: LessThan(startTime),
endTime: MoreThan(endTime),
});
})
)
.getMany();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 事务的使用
# 清单列表的拖拽排序
需求是这样的,用户可以拖拽清单列表进行排序
做之前我们先看了下语雀 和 Apifox
对于这种需求的时候接口是怎么设计的
语雀是如果你把一个知识库拖拽到第 1 位,order_num
直接传 0 ,拖到第几位,order_num
就传n - 1
Apifox
是传 目标 id 和 放置 id 另外加一个偏移量,向上为负数、向下为正数。
恩,看完之后还是有点懵,就搜到了以下两篇文章
https://www.jianshu.com/p/9ee708e43ebf
https://www.cnblogs.com/xjnotxj/p/12744348.html
于是,就把接口设计成了这样
sourceIndex` 代表原来的位置,`moveIndex` 代表目标的位置,`id` 代表拖拽的清单 `id
在设计数据库时,我们也添加了一个 sortIndex
字段,代表它在页面中的展示顺序
@Column({ name: 'sort_index', comment: '展示顺序' })
sortIndex: number;
2
最后在这里列一下后端处理的代码
- 判断该事项是向上拖拽还是向下拖拽
const isUp = sourceIndex > moveIndex;
从数据库中按
sortIndex
升序查出souceIndex
至moveIndex
区间的数据。遍历该区间数据
- 对于当前拖拽的清单,直接将
sortIndex
更新为moveIndex
- 其它的清单如果是向上拖拽,那么将
sortIndex
加 1,向下拖拽,将sortIndex
减 1
- 对于当前拖拽的清单,直接将
开启事务
所谓的事务,通俗点来说这些操作 要么都执行,要么都不执行。
在拖拽中,我们更新一个区间数据的展示顺序,同样也需要满足上面要求,区间内的清单顺序都修改成功才算成功,有一个失败那么就全部回滚
关于事务的更多的使用案例见 文档 (opens new window)
// 'default' 是在 config.default.ts 文件中的 typeorm 对象中定义的,因为可能有的业务中会涉及多数据源
const dataSource = this.dataSourceManager.getDataSource('default');
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
try {
// 开启事务
await queryRunner.startTransaction();
const folderList = await queryRunner.manager
.createQueryBuilder(FolderInfo, 'folder')
.where(
'folder.userId = :userId AND folder.sortIndex BETWEEN :startIndex AND :endIndex',
{
userId,
...startEndIndex,
}
)
.orderBy('sort_index', 'ASC')
.getMany();
for (let index = 0; index < folderList.length; index++) {
const item = folderList[index];
// 直接修改拖拽源的 sortIndex
if (item.id === id) {
await queryRunner.manager.update(FolderInfo, id, {
sortIndex: moveIndex,
});
} else {
await queryRunner.manager.update(FolderInfo, item.id, {
sortIndex: isUp ? item.sortIndex + 1 : item.sortIndex - 1,
});
}
}
// 提交事务
await queryRunner.commitTransaction();
} catch (error) {
// 出错就回滚
await queryRunner.rollbackTransaction();
} finally {
// 不管成功失败,手动释放 runner
await queryRunner.release();
}
// 返回最新的数据
return this.getFolderList();
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
# Redis
Midway Redis 组件文档 (opens new window)
Redis
内容挺多的,八股文也挺多的。但我们在项目中,只是用 Redis
存了短信验证码,其它的业务数据并没有往 Redis
放,从功能上来讲只用到了它的冰山一角。像缓存击穿、缓存雪崩、缓存穿透这一类的处理一律都没有。
关于 Redis
的学习推荐一个感觉还不错的 视频 (opens new window) ,虽然是Java
版本但个人觉得实战部分还不错,毕竟我们主要是学方案,至于语言不都是互通的嘛
# 短信验证码的存和取
# 封装的工具函数
// 获取常用的 redis 存储时间,1分钟、1天、2天,现在至当天结束
const getRedisExpireInfo = (): RedisExpireType => {
const currentDay = dayjs().format('YYYY-MM-DD');
return {
oneMinute: 60,
onDay: 60 * 60 * 24,
twoDay: 60 * 60 * 24 * 2,
// 距离当天的结束时间
endDay: dayjs(`${currentDay} 23:59:59`).diff(dayjs(), 's'),
};
};
// 业务直接调用此函数,设置 redis 的 key、value、存储时间
const setRedisInfo = async (
service: RedisService,
key: string,
value: string,
expire: keyof RedisExpireType
): Promise<boolean> => {
const expireInfo = getRedisExpireInfo();
try {
const isOk = await service.set(key, value, 'EX', expireInfo[expire]);
return isOk === 'OK';
} catch (error) {
return false;
}
};
// 根据业务属性定义相关的枚举
export enum UserKeyEnum {
'SMS_REDIS_KEY' = 'web:user:smsCode',
'USER_LOGIN_KEY' = 'web:user:login',
}
// 取
const redisCode = await this.redisService.get(
`${UserKeyEnum.SMS_REDIS_KEY}${mobile}`
);
// 存
const redisKey = `${UserKeyEnum.SMS_REDIS_KEY}${mobile}`;
// 设置60秒有效期
const isSave = await setRedisInfo(
this.redisService,
redisKey,
code,
'oneMinute'
);
if (!isSave) {
this.ctx.logger.error(
`redis验证码保存失败: redisKey: ${redisKey}, value: ${code}`
);
throw new DefaultError('验证码发送失败');
}
return '验证码发送成功';
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