NestJS-系列
NestJS[一]-基础
RxJS
NestJS[二]-核心
NestJS[三]-进阶
NestJS[四]-数据库

ORM

对象关系映射器ORM(Object/Relational Mapping) 通过实例对象的语法,完成关系型数据库的操作。

ORM 把数据库映射成对象 ORM 实例教程-阮一峰

  1. 数据库的表(table) —> 类(class)
  2. 记录(record,行数据)—> 对象(object)
  3. 字段(field)—> 对象的属性(attribute)

ORM 使用对象,封装了数据库操作,不使用 SQL 语言,只面向对象编程,与数据对象直接交互,在更高的抽象层次上操作数据库。因此可以通过一个 ORM 框架,操作多种数据库。让开发者更专注业务逻辑的处理。

TypeORM

TypeORM 是TS生态中最成熟的ORM,TypeORM中文文档

Nest 提供了与它的紧密集成库 @nestjs/typeorm文档

安装:npm i -S @nestjs/typeorm typeorm

还需要安装node侧的数据库驱动程序,如 mysql2、pg、sqlite3 等

安装:npm i -S mysql2

连接数据库

TypeOrmModule全局动态模块,用于注册数据库连接,并进行一些配置。

forRoot() 方法支持 TypeORMDataSource 构造函数暴露的所有配置属性,并且还有一些额外的配置属性。

基本配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Module({
imports: [
TypeOrmModule.forRoot({
type: "mysql", //数据库类型
username: "root", //账号
password: "123456", //密码
host: "localhost", //host
port: 3306, //
database: "test", //库名
entities: [__dirname + '/**/*.entity{.ts,.js}'], //实体文件
synchronize:true, //synchronize字段代表是否自动将实体类同步到数据库
// 下面三个是Nest提供的额外配置
retryDelay:500, //重试连接数据库间隔
retryAttempts:10,//重试连接数据库的次数
autoLoadEntities:true, //如果为true,将自动加载实体 forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中
cache: true, //启用查询缓存
// cache: {
// duration: 30000, //默认缓存时间为 1000 毫秒,可以传入数字指定缓存时间
// }
}),
],
})
export class AppModule {}

使用 forRootAsync() 异步注册方式,注入 ConfigService 进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => {
const config = configService.get<Configuration>(configToken).database;
return {
type: config.type, //数据库类型
username: config.username, //账号
password: config.password, //密码
host: config.host, //host
port: config.port, //
database: config.name, //库名
// ......其他配置
} as TypeOrmModuleOptions;
},
inject: [ConfigService],
}),

配置完成后,TypeORMRepository 对象将可在整个项目中 通过 @InjectRepository() 注入(无需导入任何模块)

注册实体

forRoot()entities 选项中统一注册实体文件,支持静态 glob 路径和 entities 数组

1
2
entities: [__dirname + '/**/*.entity{.ts,.js}'], // 静态 glob 路径数组
entities: [User], // 实体类数组

也可以开启 autoLoadEntities 自动加载实体,并使用 forFeature() 方法在不同模块中注册实体,这些局部注册的实体都将自动添加到配置对象的实体数组中。

1
2
3
4
//自动加载实体,forFeature()方法注册的每个实体都将自动添加到配置对象的实体数组中
autoLoadEntities: true,
// 在其它模块中使用 forFeature() 注册实体
TypeOrmModule.forFeature([entity])

实体

TypeORM中,实体是由 @Entity() 注释类,用于映射数据库表。文档

提供了 @Column() 等实体属性装饰器,用于定义列等数据库表的结构和信息。

1
2
3
4
5
6
7
8
9
10
import {Entity,Column,PrimaryGeneratedColumn} from 'typeorm'
@Entity()
export class Guard {
// 自增主键列
@PrimaryGeneratedColumn()
id:number
// 普通列
@Column()
name:string
}

开发环境开启 synchronize 配置项,TypeORM 会根据实体的定义自动创建数据库表,且在每次应用启动时都会检查实体类和表的同步状态,如果不同步则会自动更新表结构。

1
2
3
synchronize: true, //synchronize字段代表是否自动将实体类同步到数据库
// 设置为true,表示每次应用启动时都会检查实体类和数据库表的同步状态,如果不同步则会自动更新数据库表结构
// 生产环境应设置为false,避免自动更新数据库表结构,否则可能会丢失生产数据。

实体属性装饰器

常用实体属性装饰器:

  1. @Entity() 声明实体
  2. @PrimaryColumn() 主键列
  3. @PrimaryGeneratedColumn() 自增主键列
  4. @Column() 普通列
  5. @Generated() 生成列,能自动生成值,如UUID等。
  6. @CreateDateColumn() *特殊列,自动为实体插入创建时间。
  7. @UpdateDateColumn() *特殊列,每次save时自动更新时间。
  8. @DeleteDateColumn *特殊列,软删除标记列,初始值为null,软删除时记录删除时间。
  9. @VersionColumn() *特殊列,每次save时自动增长实体版本(增量编号)

*特殊列的值将根据内置规则自动设置,无需手动赋值。

user实体 src\db\entities\user.entity.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
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
desc: string;
@CreateDateColumn({ type: 'timestamp' })
create_time: Date;
@UpdateDateColumn({ type: 'timestamp' })
update_time: Date;
@DeleteDateColumn({ type: 'timestamp' })
delete_time: Date;
}

Column选项

@Column() 装饰器可以接受ColumnOptions选项,用于定义列的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type: ColumnType - 列类型。https://typeorm.bootcss.com/entities#列类型
name: string - 数据库表中的列名。默认情况下,列名称是从属性的名称生成的。 你也可以通过指定自己的名称来更改它。
length: number - 列类型的长度。 例如,如果要创建varchar(150)类型,请指定列类型和长度选项。
width: number - 列类型的显示范围。 仅用于MySQL integer types
onUpdate: string - ON UPDATE触发器。 仅用于 MySQL.
nullable: boolean - 在数据库中使列NULL或NOT NULL。 默认情况下,列是nullable:false。
update: boolean - 指示"save"操作是否更新列值。如果为false,则只能在第一次插入对象时编写该值。 默认值为"true"。
select: boolean - 定义在进行查询时是否默认隐藏此列。 设置为false时,列数据不会显示标准查询。 默认情况下,列是select:true
default: string - 添加数据库级列的DEFAULT值。
primary: boolean - 将列标记为主要列。 使用方式和@ PrimaryColumn相同。
unique: boolean - 将列标记为唯一列(创建唯一约束)。
comment: string - 数据库列备注,并非所有数据库类型都支持。
precision: number - 十进制(精确数字)列的精度(仅适用于十进制列),这是为值存储的最大位数。仅用于某些列类型。
scale: number - 十进制(精确数字)列的比例(仅适用于十进制列),表示小数点右侧的位数,且不得大于精度。 仅用于某些列类型。
zerofill: boolean - 将ZEROFILL属性设置为数字列。 仅在 MySQL 中使用。 如果是true,MySQL 会自动将UNSIGNED属性添加到此列。
unsigned: boolean - 将UNSIGNED属性设置为数字列。 仅在 MySQL 中使用。
charset: string - 定义列字符集。 并非所有数据库类型都支持。
collation: string - 定义列排序规则。
enum: string[]|AnyEnum - 在enum列类型中使用,以指定允许的枚举值列表。 你也可以指定数组或指定枚举类。
asExpression: string - 生成的列表达式。 仅在MySQL中使用。
generatedType: "VIRTUAL"|"STORED" - 生成的列类型。 仅在MySQL中使用。
hstoreType: "object"|"string" -返回HSTORE列类型。 以字符串或对象的形式返回值。 仅在Postgres>)中使用。
array: boolean - 用于可以是数组的 postgres 列类型(例如 int [])
transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType } - 用于将任意类型EntityType的属性编组为数据库支持的类型DatabaseType。

注意:大多数列选项都是特定于 RDBMS 的,并且在MongoDB中不可用。

操作实体-Repositories

配置好 TypeOrmModule 并注册了实体后,使用 @InjectRepository() 注入 Repository 存储库类。

每个实体都有自己的 Repository 存储库,可以处理其实体的所有操作。通过调用 Repository 的方法,实现对数据库的增删改查。

src\db\db.service.ts
1
2
3
4
5
6
7
8
9
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class DbService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
) {}
}

CURD

通过实体存储库 Repository 实现CURD,Repository API

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
1. `manager` 存储库使用的 `EntityManager`
2. `metadata` 存储库管理的实体的 `EntityMetadata`
3. `queryRunner` EntityManager 使用的查询器。仅在 EntityManager 的事务实例中使用。
4. `target` 此存储库管理的目标实体类。仅在 EntityManager 的事务实例中使用。
5. `createQueryBuilder()` 创建用于构建 SQL 查询的查询构建器。详见[QueryBuilder](https://typeorm.bootcss.com/select-query-builder)
6. `hasId()` 检查是否定义了给定实体的主列属性。
7. `getId()` 获取给定实体的主列属性值。复合主键返回的值将是一个具有主列名称和值的对象。
8. `create()` 创建一个新的实体实例。可选接收具有实体属性的对象,将写入新创建的实体实例。相当于 `new User()` 后再往实例上添加属性。
9. `merge()` 将多个实体合并为一个实体。
10. `preload()` 将给定的实体与数据库中的实体进行比较,并返回一个补全了缺失的属性新的实体。
11. `save()` 保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。
12. `insert()` 插入新实体或实体数组。
13. `update()` 通过给定的更新选项或实体 ID 部分更新实体。
14. `upsert()``save()` 类似,但不包含级联、关系和其他操作。
15. `remove()` 删除给定的实体或实体数组。
16. `delete()` 根据实体id、id数组或给定的条件删除实体。
17. `softDelete()` 软删除,参数同 `delete()`
18. `softRemove()` 软删除,参数同 `remove()`
19. `recover()` 传入实体或实体数组,恢复软删除的实体。
20. `restore()` 根据实体id、id数组或给定的 `FindOptionsWhere` 条件,恢复软删除的实体。
21. `increment()` 增加符合条件的实体某些列值。
22. `decrement()` 减少符合条件的实体某些列值。
23. `find()` 查询匹配Find选项的实体,适用于大多数查询场景。
24. `findAndCount()` 查询匹配Find选项实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skip 和 take 选项)。
25. `findOneById()`ID 查询实体。
26. `findByIds()`ID 查询多个实体。
27. `findOne()` 查询匹配Find选项的第一个实体。
28. `findOneOrFail()` 查询匹配Find选项的第一个实体。如果没有匹配,则 Rejects 一个 promise。
29. `exist()` 检查是否存在匹配Find选项的实体。
30. `count()` 符合指定条件的实体数量。对分页很有用。
31. `query()` 执行原始 SQL 语句。
32. `clear()` 清除给定表中的所有数据。

创建/插入

  1. create() 创建一个新的实体实例。可选接收具有实体属性的对象,将写入新创建的实体实例。相当于实例化User类后再添加属性。
  2. save() 保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。
  3. insert() 插入新实体或实体数组。返回 InsertResult 对象,包含插入的实体的标识符和生成的映射。
  4. upsert()save() 类似,但不包含级联、关系和其他操作。
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
async create(createDbDto: CreateDbDto) {
const user = this.userRepository.create({
name: createDbDto.name,
desc: createDbDto.desc,
}); // 创建一个实体
const result = await this.userRepository.save(user); // 保存到数据库
// const result = await this.userRepository.insert(user); // 插入到数据库
return result;
}
// save返回保存后的实体值
// {
// "name": "user2",
// "desc": "这是一个用户2",
// "id": 15,
// "create_time": "2024-02-20T05:10:52.740Z",
// "update_time": "2024-02-20T05:10:52.740Z"
// }

// insert返回InsertResult
// {
// "identifiers": [
// {
// "id": 14
// }
// ],
// "generatedMaps": [
// {
// "id": 14,
// "create_time": "2024-02-20T05:09:07.306Z",
// "update_time": "2024-02-20T05:09:07.306Z"
// }
// ],
// "raw": {
// "fieldCount": 0,
// "affectedRows": 1,
// "insertId": 14,
// "info": "",
// "serverStatus": 2,
// "warningStatus": 0,
// "changedRows": 0
// }
// }

查询

Repository 提供了多种查询方法,通常返回实体或实体数组。

  1. find() 查询匹配Find选项的实体,适用于大多数查询场景。
  2. findAndCount() 查询匹配Find选项实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skip 和 take 选项)。
  3. findOneById() 按 ID 查询实体。
  4. findByIds() 按 ID 查询多个实体。
  5. findOne() 查询匹配Find选项的第一个实体。
  6. findOneOrFail() 查询匹配Find选项的第一个实体。如果没有匹配,则 Rejects 一个 promise。
  7. exist() 检查是否存在匹配Find选项的实体。
  8. count() 符合指定条件的实体数量。对分页很有用。
  9. query() 执行原始 SQL 语句。

大部分查询方法能接收 Find 选项,用于指定查询条件、排序、分页等。
一些查询方法还有 find*By() 版本,传入 FindOptionsWhere 直接根据查询条件查询实体。如 findBy()findOneBy() 等。

常用 Find 选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
userRepository.find({
select: ["firstName", "lastName"], // 指定要选择的列
relations: ["profile", "photos", "videos"], // 指定要加载的关系
where: { // 指定查询条件
firstName: "Timber",
lastName: "Saw"
},
order: { // 指定排序
name: "ASC", // 升序
id: "DESC" // 降序
},
// 分页
skip: 5, // 跳过前5个
take: 10, // 取10个
cache: true // 缓存查询结果,需要在TypeOrmModule配置中启用查询缓存
// cache: 60000 // 默认缓存时间为 1000 毫秒,可以传入数字指定缓存时间
});

TypeORM 还提供了许多内置运算符,可用于创建更复杂的查询。部分运算符与 SQL 同名的关键字差不多。

  1. Not() 不等于
  2. LessThan() 小于
  3. LessThanOrEqual() 小于等于
  4. MoreThan() 大于
  5. MoreThanOrEqual() 大于等于
  6. Equal() 等于
  7. Like() 模糊查询,支持 SQL 通配符
  8. ILIKE() 不区分大小写的模糊查询,也支持通配符
  9. Between() 在两个值之间
  10. In() 在给定的值数组中
  11. Any() 在给定的值数组中的任意一个,通常配合其它运算符使用
  12. IsNull() 为空
  13. Raw() 原始 SQL 语句

FindAll查询案例

支持搜索、分页、排序

DTO src\db\dto\find-all.dto.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Transform } from 'class-transformer';
import { IsInt, IsOptional, IsString, Min } from 'class-validator';

export class FindAllDto {
@IsString()
@IsOptional()
keyword?: string;
@IsInt()
@IsOptional()
@Min(1)
@Transform((p) => parseInt(p.value, 10))
@RequireOtherFields('size') // 当传入了page时,必须同时传入size
page?: number;
@IsInt()
@IsOptional()
@Min(0)
@Transform((p) => parseInt(p.value, 10))
size?: number;
}
控制器 src\db\db.controller.ts
1
2
3
4
5
import { FindAllDto } from './dto/find-all.dto';
@Get()
findAll(@Query() query: FindAllDto) {
return this.dbService.findAll(query);
}
服务 src\db\db.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
async findAll(options: FindAllDto) {
const result = await this.userRepository.find({
where: {
name: options.keyword ? Like(`%${options.keyword}%`) : undefined,
},
order: {
id: 'ASC',
},
skip: options.page && options.size && (options.page - 1) * options.size,
take: options.size,
});
const total = await this.userRepository.count();
return {
users: result,
total,
};
}
// {
// "users": [
// {
// "id": 1,
// "name": "fsfdf",
// "desc": "这是46456户",
// "create_time": "2024-02-19T13:35:28.264Z",
// "update_time": "2024-02-20T06:45:50.000Z",
// "delete_time": null
// },
// {
// "id": 3,
// "name": "user1",
// "desc": "这是一个用户",
// "create_time": "2024-02-19T13:35:38.280Z",
// "update_time": "2024-02-19T13:36:19.896Z",
// "delete_time": null
// }
// ],
// "total": 16
// }

更新/修改

  1. update() 通过给定的更新选项或实体 ID 部分更新实体。
  2. save() 保存(插入)给定实体或实体数组,返回保存后的实体值。若主键已存在,则会更新该实体,否则插入一个新实体。
  3. upsert()save() 类似,但不包含级联、关系和其他操作。
1
2
3
4
5
6
7
8
9
async update(id: number, updateDbDto: UpdateDbDto) {
const result = await this.userRepository.update(id, updateDbDto);
return result;
}
// {
// "generatedMaps": [],
// "raw": [],
// "affected": 1
// }

若需要条件更新,需要与查询方法配合使用,获取需要更新的实体,使用 merge() 合并实体,再调用 save()upsert()

1
2
3
4
5
6
7
async update(id: number, updateDbDto: UpdateDbDto) {
// const result = await this.userRepository.update(id, updateDbDto);
const oldUser = await this.userRepository.findOneBy({ id });
const newUser = this.userRepository.merge(oldUser, updateDbDto);
const result = await this.userRepository.save(newUser);
return result;
}

删除

  1. delete() 根据实体id、id数组或给定的 FindOptionsWhere 条件删除实体。
  2. remove() 删除给定的实体或实体数组。
  3. softDelete() 软删除,参数同 delete()
  4. softRemove() 软删除,参数同 remove()
1
2
3
4
5
6
7
8
async remove(id: number) {
const result = await this.userRepository.delete(id);
return result;
}
// {
// "raw": [],
// "affected": 1
// }

在实际业务中,可能需要软删除,即不真正删除数据,而是标记为已删除。
使用 @DeleteDateColumn() 声明标记删除时间的列。就可以通过 soft*() 进行软删除。

1
2
3
4
5
6
7
8
9
async remove(id: number) {
const result = await this.userRepository.softDelete(id);
return result;
}
// {
// "generatedMaps": [],
// "raw": [],
// "affected": 1
// }

恢复软删除

  1. recover() 传入实体或实体数组,恢复软删除的实体。
  2. restore() 根据实体id、id数组或给定的 FindOptionsWhere 条件,恢复软删除的实体。

恢复软删除的实体,即将标记删除时间的列置为null。

1
2
3
4
5
6
7
8
9
async restore(id: number) {
const result = await this.userRepository.restore(id);
return result;
}
// {
// "generatedMaps": [],
// "raw": [],
// "affected": 1
// }

完整案例

控制器 src\db\db.controller.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe } from '@nestjs/common';
import { DbService } from './db.service';
import { CreateDbDto } from './dto/create-db.dto';
import { UpdateDbDto } from './dto/update-db.dto';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { FindAllDto } from './dto/find-all.dto';

@Controller('db')
@ApiTags('db')
export class DbController {
constructor(private readonly dbService: DbService) {}

@Post()
@ApiOperation({
summary: '创建',
description: '创建一个用户',
})
create(@Body() createDbDto: CreateDbDto) {
return this.dbService.create(createDbDto);
}

@Get()
@ApiOperation({
summary: '查询所有',
description: '查询所有用户',
})
findAll(@Query() query: FindAllDto) {
return this.dbService.findAll(query);
}

@Get(':id')
@ApiOperation({
summary: '查询单个',
description: '根据id查询单个用户',
})
findOne(@Param('id', ParseIntPipe) id: number) {
return this.dbService.findOne(id);
}

@Patch(':id')
@ApiOperation({
summary: '更新',
description: '根据id更新用户',
})
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateDbDto: UpdateDbDto,
) {
return this.dbService.update(id, updateDbDto);
}

@Delete(':id')
@ApiOperation({
summary: '删除',
description: '根据id删除用户',
})
remove(@Param('id', ParseIntPipe) id: number) {
return this.dbService.remove(id);
}

@Patch('restore/:id')
@ApiOperation({
summary: '恢复',
description: '根据id恢复用户',
})
restore(@Param('id', ParseIntPipe) id: number) {
return this.dbService.restore(id);
}
}
服务 src\db\db.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { Injectable } from '@nestjs/common';
import { CreateDbDto } from './dto/create-db.dto';
import { UpdateDbDto } from './dto/update-db.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { ILike, Like, Repository } from 'typeorm';
import { FindAllDto } from './dto/find-all.dto';

@Injectable()
export class DbService {
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
) {}

async create(createDbDto: CreateDbDto) {
const user = this.userRepository.create({
name: createDbDto.name,
desc: createDbDto.desc,
}); // 创建一个实体
// const result = await this.userRepository.save(user); // 保存到数据库
const result = await this.userRepository.insert(user); // 插入到数据库
return result;
}

async findAll(options: FindAllDto) {
const result = await this.userRepository.find({
where: {
name: options.keyword ? Like(`%${options.keyword}%`) : undefined,
},
order: {
id: 'ASC',
},
skip: options.page && options.size && (options.page - 1) * options.size,
take: options.size,
});
return result;
}

async findOne(id: number) {
const result = this.userRepository.findOneBy({ id });
return result;
}

async update(id: number, updateDbDto: UpdateDbDto) {
// const result = await this.userRepository.update(id, updateDbDto);
const oldUser = await this.userRepository.findOneBy({ id });
const newUser = this.userRepository.merge(oldUser, updateDbDto);
const result = await this.userRepository.save(newUser);
return result;
}

async remove(id: number) {
const result = await this.userRepository.softDelete(id);
return result;
}

async restore(id: number) {
const result = await this.userRepository.restore(id);
return result;
}
}

关系

实际业务中,经常有多个实体之间的关系,如用户和角色、用户和订单等。反映到数据库中,就是表与表之间的关联。在 MySQL 中通过主键和外键来建立表格之间的关系。

TypeORM 支持多种关系,如一对一、一对多、多对多等。文档

关系装饰器:

  1. @OneToOne() 一对一
  2. @OneToMany() 一对多
  3. @ManyToOne() 多对一
  4. @ManyToMany() 多对多

参数:

  1. 第一个参数是一个函数,返回一个实体类,表示关联的实体。如 @OneToOne(() => Config)
  2. 第二个可选参数是一个函数,返回一个实体类的属性,表示关联的实体的属性,用于实现双向关系。如 @OneToOne(() => Config, (config) => config.user)
  3. 第三个可选参数是关系选项

关系选项:文档

  1. eager: boolean 如果为 true,则在此实体上使用 find*(),将始终加载此关系。
  2. cascade: boolean 如果为 true,则将插入相关对象并在数据库中更新。
  3. onDelete: "RESTRICT"|"CASCADE"|"SET NULL" 指定删除引用对象时外键的行为方式
  4. primary: boolean 指示此关系的列是否为主列。
  5. nullable: boolean 指示此关系的列是否可为空。默认为true。
  6. orphanedRowAction: "nullify" | "delete" 将子行从其父行中删除后,确定该子行是孤立的(默认值)还是删除的。

设置外键、中间表:
@JoinColumn() 设置外键,用于一对一、多对一关系。@JoinTable() 用于多对多关系,自动生成一个中间表

关系可以是单向的和双向的。单向是仅在一侧有关系装饰器。双向是两侧都有的关系装饰器。但注意 @JoinColumn() 只在关系的一侧且拥有外键的表上使用,另一条关系则称为反向关系。

在一对多、多对一关系中,@JoinColumn() 添加至”一”的一侧。

一对一

UserConfig 实体为例,一个用户只有一个配置,一个配置只属于一个用户。文档

1、先实现 User 单向关联 Config

  1. User 中新增一个 config 属性,通过 @JoinColumn() 设置为外键。
  2. 在使用 @OneToOne() 声明关系,指定关联的实体。
src\db\entities\user.entity.ts
1
2
3
4
5
export class User {
@OneToOne(() => Config)
@JoinColumn()
config: Config;
}

完成单向一对一关系后,User 表中就会新增一个 configId 外键,用于存放 Config 的主键(id)。

可以发现,单向的关系仅需处理主实体,不需要在被关联的实体中做任何处理。

2、再完成反向关系,实现双向的一对一关系。

  1. 在被关联的 Config 中,新增 user 属性(但不使用@Column()注释为列),使用 @OneToOne() 声明关系,指定关联的实体,以及第二个参数指定关联的实体的属性。
  2. 在主实体 User 中,也需要向 @OneToOne() 传入第二个参数指定关联的实体的属性。
src\db\entities\config.entity.ts
1
2
3
4
5
6
7
8
export class Config {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string; // 配置名
@OneToOne(() => User, (user) => user.id)
user: User;
}
src\db\entities\user.entity.ts
1
2
3
4
5
export class User {
@OneToOne(() => Config, (config) => config.id, { cascade: true })
@JoinColumn()
config: Config;
}

接口案例

添加用户配置的接口:

src\db\db.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
async setConfig(params: AddConfigDto) {
// 获取需要添加配置的用户
const user = await this.userRepository.findOneBy({ id: params.userId });
// 创建一个配置实体
const config = this.configRepository.create({
name: params.config.name,
});
// 若是开启了级联保存,可以省略此步,会自动保存到数据库
await this.configRepository.insert(config); // 插入配置实体
user.config = config; // 关联用户和配置
const result = await this.userRepository.save(user); // 保存到数据库
return result;
}
// {
// "id": 1,
// "name": "用户1",
// "desc": "这是一个用户",
// "create_time": "2024-02-19T13:35:28.264Z",
// "update_time": "2024-02-20T14:03:13.000Z",
// "delete_time": null,
// "config": {
// "name": "配置2",
// "id": 2
// }
// }

查询时,需要指定 relations 选项,以加载关联的实体,值为数组,元素是实现关联的属性名。

src\db\db.service.ts
1
2
3
4
5
6
const result = this.userRepository.findOne({
relations: ['config'],
where: {
id,
},
});

一对多/多对一

UserCategory 实体为例,一个分类下有多个用户,一个用户只属于一个分类。文档

有了一对一关系的基础,下面直接实现双向的一对多/多对一关系。

src\db\entities\user.entity.ts
1
2
3
4
5
export class User {
@ManyToOne(() => Category, (category) => category.users, { cascade: true })
@JoinColumn()
category: Category;
}
src\db\entities\category.entity.ts
1
2
3
4
5
6
7
8
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string; // Category名称
@OneToMany(() => User, (user) => user.category)
users: User[];
}

完成后,User 表中新增了一个 categoryId 外键,用于存放 Category 的主键(id)。

@JoinColumn() 通常与 @ManyToOne() 在主实体中一起使用,用于指定外键。
实际上 @JoinColumn() 可以完全省略,除非需要自定义关联列(外键)在数据库中的名称,因为在多对一关系中,外键总是在”一”的一侧,TypeORM 会自动添加外键。

多对多

UserTag 实体为例,一个用户有多个标签,一个标签可分配给多个用户。文档

TypeORM 会为多对多关系自动创建一个中间表,用于表示两个实体的关联关系,中间表有两个外键,分别指向两个实体的主键

@JoinTable()@ManyToMany() 关系所必需的,可以放在关系的任意一侧。用于生成中间表。

下面直接实现双向的多对多关系。

src\db\entities\user.entity.ts
1
2
3
4
5
export class User {
@ManyToMany(() => Tag, (tag) => tag.users, { cascade: true }) // 只能一侧设置级联
@JoinTable()
tags: Tag[];
}
src\db\entities\tag.entity.ts
1
2
3
4
5
6
7
8
export class Tag {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string; // tag名称
@ManyToMany(() => User, (user) => user.tags)
users: User[];
}

默认自动生成了名为 user_tags_tag 的中间表,包含两个外键,分别指向 UserTag 的主键。

接口案例

先实现个创建新标签的接口。

src\db\db.service.ts
1
2
3
4
5
6
7
async createTag(createTagDto: CreateTagDto) {
const tag = this.tagRepository.create({
...createTagDto,
}); // 创建一个标签实体
const result = await this.tagRepository.save(tag); // 保存到数据库
return result;
}

再实现给用户添加标签的接口。

src\db\db.service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async addTag(addTagDto: AddTagDto) {
// 获取用户和标签
const user = await this.userRepository.findOne({
where: { id: addTagDto.userId },
relations: ['tags'],
});
const tag = await this.tagRepository.findOne({
where: { id: addTagDto.tagId },
relations: ['users'],
});
// 因为启用了级联,所以仅需操作一侧即可
user.tags.push(tag); // 关联用户和标签
const result = await this.userRepository.save(user); // 保存到数据库
return result;
}

多次添加后,在 user_tags_tag 中间表中,就可以看到用户和标签的,通过各自主键关联的多对多关系。

事务

事务是数据库操作的一种机制,用于保证一组操作的原子性,要么全部成功,要么全部失败。

四大特性:

  1. 原子性:事务包含的各项操作,是一个不可分割的工作单位,要么全部成功,要么全部失败。任何一项出错都会导致整个事务的失败,同时其它已经执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。
  2. 一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。即数据库从一个一致性状态转换到另一个一致性状态。
  3. 隔离性:并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。
  4. 持久性:事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。

EntityManager

实体管理器 EntityManager 可以操作任何实体,就像是存放实体存储库的集合的地方

EntityManager APIRepository 的类似,但 EntityManager 需要指定操作的实体存储库。

通过任意存储库的 manager 属性可以获取 EntityManager

在 TypeORM 中,使用 EntityManager.transaction() 来创建并处理事务。
该方法接收一个回调函数,参数为 EntityManager,所有事务内的CURD等操作,都必须通过 EntityManager 在回调中执行。

1
2
3
4
5
this.mangerRepository.manager.transaction(
async (manager) => {
// 通过 manager 事务内的操作
}
);

转账案例

转账分为两步,先从A账户扣款,再给B账户加款。需要事务保证两步操作的原子性,若加款失败,扣款也要回滚。

DTO
1
2
3
4
5
6
7
8
9
export class CreateMangerDto {
name: string; // 用户
money: number; // 金额
}
export class TransferMoneyDto {
fromId: number; // 发起人
toId: number; // 接收人
money: number; // 转账金额
}
src\manger\manger.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
async transferMoney(transferMoneyDto: TransferMoneyDto) {
try {
await this.mangerRepository.manager.transaction(async (manager) => {
const from = await manager.findOneBy(Manger, {
id: transferMoneyDto.fromId,
});
const to = await manager.findOneBy(Manger, {
id: transferMoneyDto.toId,
});
if (from.money < transferMoneyDto.money) {
throw new HttpException('余额不足', 400);
}
// 进行转账
from.money -= transferMoneyDto.money;
to.money += transferMoneyDto.money;
await manager.save(from);
await manager.save(to);
});
} catch (e) {
return {
message: e.message,
};
}
return {
message: '转账成功',
};
}