NestJS 扩展

项目配置

应用程序通常运行在不同的环境中,根据不同的环境,应使用不同的配置。最好的方法是将配置信息存储在环境变量中。
环境变量在 nodejs 中通过 process.env 来访问。
当我们需要定义大量的环境变量时,在 nodejs 中环境变量文件一般通过 .env 来命名。
Nest 为我们提供了一个开箱即用的配置模块:@nestjs/config,它包含了一个 ConfigModule 模块,其暴露的 ConfigService 可以自动加载 .env 文件。并且,它还支持我们自定义配置文件,然后通过 ConfigService 获取配置项。

安装

1
npm i --save @nestjs/config

引入

在根模块中引入:

1
2
3
4
5
6
7
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [ConfigModule.forRoot()]
})
export class AppModule {}

上面的代码将默认从项目根目录加载和解析 .env 文件,将 .env 文件中的键/值对与分配给 process.env 的环境变量合并,并将结果存储在可以通过 ConfigService 访问的私有结构中。
forRoot() 方法传入一个配置对象,包含以下配置项:

  • envFilePath:指定环境变量文件,可以是字符串或字符串数组,如 [".env.development", ".env.production"]
  • ignoreEnvFile:不加载环境变量文件
  • isGlobal:是否声明为全局模块,若为 true,其他模块可直接注入 ConfigService,否则需要引入 ConfigModule
  • load:加载配置文件
  • cache:是否缓存环境变量

自定义配置文件

环境变量只是单个的变量,如果我们想让一组相关的配置以对象的形式返回,可以通过自定义配置文件来实现。
新建 configuration.ts 文件,添加如下内容:

1
2
3
4
5
6
7
8
export default () => ({
database: {
name: "chat",
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT) || 3306,
password: process.env.DATABASE_PASSWORD
}
});

然后在引入时通过 load 配置项来加载:

1
2
3
4
5
6
7
8
9
10
import configuration from './config/configuration';

@Module({
imports: [
ConfigModule.forRoot({
load: [configuration]
})
]
})
export class AppModule {}

之后,就可以通过 ConfigService 来访问配置文件的内容了。

使用

将其作为依赖注入即可使用。

1
constructor(private configService: ConfigService) {}

我们主要通过它的 get 方法来获取环境变量或者配置文件的内容,并且它支持设置默认值。

1
2
3
4
5
// 获取环境变量
const dbUser = this.configService.get<string>('DATABASE_USER');

// 获取配置文件内容
const dbPort = this.configService.get<string>('database.port', 3306);

但是目前没有任何的类型提示,有两种方法可以解决,在下面介绍。

TS 类型提示

自定义接口

ConfigService 支持传入泛型,我们先将配置文件的内容抽象成一个接口:

1
2
3
4
5
6
7
8
interface Config {
database: {
name: string;
host: string;
port: number;
password: string;
}
}

然后将其传入 ConfigService

1
constructor(private configService: ConfigService<Config, true>) {}

第二个泛型参数依赖于第一个泛型,充当类型断言,以消除所有的 undefined 类型。
然后这样获取配置内容:

1
const port = this.configService.get("database.port", { infer: true });

infer: true 可以使 get 方法根据接口自动推断属性的类型,而且使用点表示法的 key 也能提示。
但是,这样不仅要定义一个接口,而且每次使用时都要输入许多参数。
下面这种方法可以轻松得到类型提示。

配置命名空间

使用 registerAs() 函数返回一个“命名空间”配置对象,如下所示:

1
2
3
4
5
6
export default registerAs('database', () => ({
name: "chat",
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT) || 3306,
password: process.env.DATABASE_PASSWORD
}));

database 是该命名空间的 key。
然后使用 forRoot() 方法的 load 属性加载:

1
2
3
4
5
6
7
8
9
10
import databaseConfig from './config/database.config';

@Module({
imports: [
ConfigModule.forRoot({
load: [databaseConfig]
})
]
})
export class AppModule {}

使用时,需要通过命名空间的 key 来注入依赖,这样才能从强类型中收益:

1
2
3
4
5
6
import type { ConfigType } from "@nestjs/config";

constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>
) {}

这样来获取配置内容:

1
const port = this.dbConfig.port;

在生产环境中的应用

前端项目的环境变量是在编译阶段直接编译进文件的,是静态的,而 nodejs 环境变量的获取是在时运行时,是动态的,因此 Nest 项目不能在打包时确定生产环境。
Nest 项目在运行和打包时不会自动设置 NODE_ENVdevelopmentproduction
@nestjs/config 不能根据 NODE_ENV 来自动加载对应的环境变量文件,如 .env.development.env.production,要实现这一点可以这样做:

1
2
3
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`
})

但是需提前设置 NODE_ENV

日志

Nest 有一个内置的基于文本的 logger,它被用来打印系统日志,我们也可以用它来打印应用程序日志。
此外,我们还可以完全自定义自己的 logger,或者在内置 logger 的基础上进行拓展。

内置 logger

1
2
3
4
5
6
7
8
9
10
11
import { Controller, Get, Logger } from "@nestjs/common";

@Controller("users")
export class UsersController {
private readonly logger = new Logger(UsersController.name);

@Get()
getUser() {
this.logger.log("get a user");
}
}

使用内置 logger 打印的日志结构为:

1
[Nest] 19096   - 2024/08/27 16:32:22   [NestFactory] get a user

按顺序依次是:Nest 标志,PID,时间戳,日志级别,上下文(打印日志的类),信息。
如果我们想要添加或减少一些字段,可以自定义 logger。

自定义 logger
完全自定义

自定义 logger 需要实现 LoggerService 接口的每个方法:

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
import { LoggerService, Injectable } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
/**
* Write a 'log' level log.
*/
log(message: any, ...optionalParams: any[]) {}

/**
* Write a 'fatal' level log.
*/
fatal(message: any, ...optionalParams: any[]) {}

/**
* Write an 'error' level log.
*/
error(message: any, ...optionalParams: any[]) {}

/**
* Write a 'warn' level log.
*/
warn(message: any, ...optionalParams: any[]) {}

/**
* Write a 'debug' level log.
*/
debug?(message: any, ...optionalParams: any[]) {}

/**
* Write a 'verbose' level log.
*/
verbose?(message: any, ...optionalParams: any[]) {}
}

这种方法较为繁琐,如果不是有特别需求,建议使用拓展内置 logger 的方法。

拓展内置 logger

通过拓展 ConsoleLogger 类并重写父类方法或新增方法来自定义 logger。
例如我们想使用内置 logger 的方法,但希望在打印日志时将其写入数据库,可以这样做:

1
2
3
4
5
6
7
8
import { ConsoleLogger } from "@nestjs/common";

export class MyLogger extends ConsoleLogger {
log(message: any, context?: string) {
// 写入数据库操作
super.log(message, context);
}
}

又比如,我们想改变日志的输出结构,可以通过其内置的一些方法来帮助我们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ConsoleLogger } from "@nestjs/common";

export class MyLogger extends ConsoleLogger {
log(message: string, path: string) {
console.log(
this.colorize("[Nest] - ", "log"),
this.getTimestamp(),
" ",
this.colorize(`[${this.context}] `, "warn"),
path,
" ",
message
);
}
}

colorize 方法可以根据日志级别为消息着色,getTimestamp 方法获取时间戳。

使用

我们可以将其作为 provider,在模块中注册,然后在需要用到的地方注入。
新建一个 logger 模块,然后创建一个 service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ConsoleLogger } from "@nestjs/common";

export class LoggerService extends ConsoleLogger {
log(message: string, path: string, context?: string) {
context && this.setContext(context);
console.log(
this.colorize("[Nest] - ", "log"),
this.getTimestamp(),
" ",
this.colorize(`[${this.context}] `, "warn"),
path,
" ",
message
);
}
}

这里使用 scopeMyLogger 指定了 transient 作用域,确保在每个注入了 MyLogger 的“消费者”中都有一个新的 MyLogger 实例,因为使用单例作用域的话,所有 MyLogger 实例的上下文都是一样的,这样做可以单独设置上下文而互不影响。

1
2
3
4
5
6
7
8
9
10
11
12
@Controller("users")
export class UsersController {
constructor(private readonly logger: LoggerService) {
this.logger.setContext(UsersController.name);
}

@Get("list")
getUsers() {
this.logger.log("get user list", "/users/list");
return [];
}
}

在 module 文件中注册并导出:

1
2
3
4
5
6
7
8
import { Module } from "@nestjs/common";
import { LoggerService } from "./logger.service";

@Module({
providers: [LoggerService],
exports: [LoggerService]
})
export class LoggerModule {}

如果希望系统日志也应用自定义 logger 的方法,可以在 app 中注册:

1
2
3
4
const app = await NestFactory.create(AppModule, {
logger: new LoggerService()
});
await app.listen(3000);

这种方式无法处理依赖注入,可使用以下方法解决:

1
2
3
4
5
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(LoggerService));
await app.listen(3000);

bufferlog 设置为 true,以确保所有日志都将被缓冲,直到连接了自定义的 logger。

使用 winston

Nest 内置 logger 只适用于开发环境,生产环境通常会使用 Winston 等专用日志模块。
安装:

1
npm i winston

winston 能够根据日志级别决定将日志打印到控制台还是保存到文件,并使用相应的日志结构。
我们需要实现 LoggerService 接口,修改 logger.service 文件:

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
70
71
import { Injectable, LoggerService as Logger } from "@nestjs/common";
import { createLogger, Logger as WinLogger, transports, format } from "winston";
import "winston-daily-rotate-file";

@Injectable()
export class LoggerService implements Logger {
private logger: WinLogger;
// 自定义格式
private customFormat = format.printf(({ level, message, timestamp }) => {
return `${level} ${timestamp} ${message}`;
});

constructor() {
this.logger = createLogger({
transports: [
// 打印到控制台
new transports.Console({
format: format.combine(
// 日志着色
format.colorize({ all: true }),
// 追加时间戳
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
this.customFormat
)
}),
// 保存到文件
new transports.DailyRotateFile({
level: "warn",
filename: "warn-%DATE%.log",
dirname: "logs/warn",
// 文件最大 20 M
maxSize: "20m",
// 保存 14 天
maxFiles: "14d",
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
this.customFormat
)
}),
// 保存到文件
new transports.DailyRotateFile({
level: "error",
filename: "error-%DATE%.log",
dirname: "logs/error",
maxSize: "20m",
maxFiles: "14d",
format: format.combine(
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
this.customFormat
)
})
]
});
}

log(message: any, path?: string) {
this.logger.info(path ? `[${path}] - ${JSON.stringify(message)}` : JSON.stringify(message));
}

error(error: Error | string) {
if (typeof error === "string") {
this.logger.error(error);
} else {
this.logger.error(`${error.message}\n${error.stack}`);
}
}

warn(message: any) {
this.logger.warn(JSON.stringify(message));
}
}

winston-daily-rotate-file 库使定义日志文件名称、大小、过期时间更加方便。通过以下方式安装:

1
npm i winston-daily-rotate-file

接下来替换系统日志:

1
2
3
4
const app = await NestFactory.create(AppModule, {
bufferLogs: true
});
app.useLogger(app.get(LoggerService));

数据库

Nest 使用数据库需要安装相应的 Node.js 驱动,如 mysql2。
ORM 可以让我们更方便的操作数据库,TypeORM 和 Sequelize 是 Node.js 常用的两个 ORM 库,Nest 也提供了与他们的集成。
这里我们使用 TypeORM 和 mysql 进行介绍。

安装

1
npm install --save @nestjs/typeorm typeorm mysql2

引入

TypeOrmModule 导入根 AppModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'test',
synchronize: false,
autoLoadEntities: true,
entities: []
})
]
})
export class AppModule {}

配置项:

  • synchronize:是否在每次应用程序启动时自动创建数据库架构。它可以将模型中的更改自动同步到数据库,但是会导致数据库中的数据丢失,不建议启用
  • autoLoadEntities:是否自动加载实体。开启后,通过 TypeOrmModule.forFeature() 注册的实体会被自动加载,否则需手动在 entities 中添加
  • entities:加载实体的数组。接受要加载的实体类和目录路径

导入后,TypeORM 的 DataSourceEntityManager 对象可在整个项目中直接注入。

由于我们的数据库配置信息一般写在环境变量里面,所以这里需要通过 ConfigService 动态的配置。

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
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigType } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import Config from "./config/config";

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`,
isGlobal: true,
load: [Config]
}),
TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigType<typeof Config>) => {
return {
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: configService.database.password,
database: "chat",
autoLoadEntities: true,
synchronize: false
};
},
inject: [Config.KEY]
})
]
})
export class AppModule {}

或者可以使用 useClass 简化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import Config from "./config/config";

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`,
isGlobal: true,
load: [Config]
}),
TypeOrmModule.forRootAsync({
useClass: TypeORMConfigService
})
]
})
export class AppModule {}

TypeORMConfigService 需实现 TypeOrmOptionsFactorycreateTypeOrmOptions 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Injectable, Inject } from "@nestjs/common";
import { ConfigType } from "@nestjs/config";
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from "@nestjs/typeorm";
import Config from "../config/config";

@Injectable()
export class TypeORMConfigService implements TypeOrmOptionsFactory {
constructor(@Inject(Config.KEY) private readonly configService: ConfigType<typeof Config>) {}

createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: "mysql",
host: "localhost",
port: 3306,
username: this.configService.database.username,
password: this.configService.database.password,
database: "chat",
autoLoadEntities: true,
synchronize: false
};
}
}

使用

创建实体

实体是一个映射到数据库表的类,更多内容请看 https://typeorm.io/entities
定义一个 User 实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
age: number;

@Column()
sex: "male" | "female";
}
注册实体

在 user 模块中注册:

1
2
3
4
5
6
7
8
9
10
11
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from "./user.entity";

@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService]
})
export class UsersModule {}
操作数据库
EntityManager

EntityManager 可以管理任何实体,就像一个存放实体存储库集合的地方。

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
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(private readonly usersManager: EntityManager) {}

findAll(): Promise<User[]> {
return this.usersManager.find(User);
}

findOne(id: number): Promise<User | null> {
return this.usersManager.findOneBy(User, { id });
}

async add(user: UserInterface) {
await this.usersManager.insert(User, {
name: user.name,
age: user.age,
sex: user.sex
});
}

async update(user: UserInterface) {
// 更新 id 为 1 的用户的名字
await this.usersManager.update(User, 1, { name: user.name });
// 更新 name 为 Silence 的用户的名字
await this.usersManager.update(User, { name: "Silence" }, { name: user.name });
}

async remove(id: number): Promise<void> {
// 根据 id 删除用户
await this.usersManager.delete(User, id);
// 根据 name 删除用户
await this.usersManager.delete(User, { name: "Silence" });
}
}

更多用法见 https://typeorm.io/entity-manager-api

Repository

RepositoryEntityManager 类似,但它的操作仅限于具体的实体。

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
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>
) {}

findAll(): Promise<User[]> {
return this.usersRepository.find();
}

findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}

async add(user: UserInterface) {
await this.usersRepository.insert({
name: user.name,
age: user.age,
sex: user.sex
});
}

async update(user: UserInterface) {
// 更新 id 为 1 的用户的名字
await this.usersRepository.update(1, { name: user.name });
// 更新 name 为 Silence 的用户的名字
await this.usersRepository.update({ name: "Silence" }, { name: user.name });
}

async remove(id: number): Promise<void> {
// 根据 id 删除用户
await this.usersRepository.delete(id);
// 根据 name 删除用户
await this.usersRepository.delete({ name: "Silence" });
}
}

更多用法见 https://typeorm.io/repository-api

DataSource

我们可以从 DataSource 对象中获取 EntityManagerRepository

1
2
3
4
5
6
7
8
9
10
11
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(private readonly dataSource: DataSource) {
this.usersRepository = dataSource.getRepository(User);
this.usersManager = dataSource.manager;
}
}

迁移

迁移提供了一种增量式更新数据库模式的方法,使其与应用程序的数据模型保持同步,同时保留数据库中的现有数据。为了生成、运行和恢复迁移,TypeORM 提供了一个专用的 CLI。
迁移只是一个带有 SQL 查询的文件,用于更新数据库模式并将新的更改应用于现有数据库。

准备

在进行迁移之前,需要准备一个 data source 文件。在 src 目录下新建 data-source.ts 文件,在其中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { DataSource } from "typeorm";

export default new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "145726",
database: "chat",
synchronize: false,
logging: true,
entities: ["./src/**/*.entity{.ts,.js}"],
migrations: ["./src/migration/*{.ts,.js}"]
});
  • entities:实体文件的位置
  • migrations:迁移文件所在位置,运行迁移时会自动加载

由于 typeorm 生成的迁移文件是 ts 类型,而运行迁移的指令只能作用于 js 文件,因此需要将 ts 转译为 js。
全局安装 ts-node

1
npm i ts-node -g
生成迁移

生成迁移是指根据模型文件自动生成迁移文件。在创建和改动模型文件后,我们想将其同步到数据库,可以生成迁移文件,再运行即可。
生成迁移文件指令:

1
npx typeorm migration:generate [migration-path]/[migration-name] -d [data-source-path]

如:

1
npx typeorm migration:generate ./src/migration/user -d ./src/data-source.ts

这里的路径都使用相对路径
应和配置项 migrations 中的路径一致

此命令会在 migration 目录下生成以 {}-user.ts 命名的迁移文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { MigrationInterface, QueryRunner } from "typeorm";

export class Index1725500554297 implements MigrationInterface {
name = 'Index1725500554297'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`order\` ADD CONSTRAINT \`FK_caabe91507b3379c7ba73637b84\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`user\``);
}
}

up 方法中的代码用于执行迁移(migration:run),down 方法中的代码用于回复上次迁移(migration:revert)。
每次生成迁移都会生成一个只记录此次更改的迁移文件,因此,如果我们希望迁移文件能够根据模型归类,可以在每次更改完一个模型后去生成迁移,并将迁移文件生成到与模型对应的目录中。
修改 data source 文件:

1
2
3
4
5
import { DataSource } from "typeorm";

export default new DataSource({
migrations: ["./src/migration/**/*{.ts,.js}"]
});

比如更改了 User 模型,执行以下命令:

1
npx typeorm migration:generate ./src/migration/user/user -d ./src/data-source.ts
创建迁移
1
npx typeorm migration:create [migration-path]/[migration-name]

生成的迁移文件中的 updown 方法为空,需要手动添加 sql 查询。

运行迁移
1
npx typeorm-ts-node-commonjs migration:run -- -d [data-source-path]

此命令将执行所有挂起的迁移,并按时间戳顺序运行它们。这意味着所有迁移文件的 up 方法中编写的 sql 查询都将被执行。就这样!现在您的数据库模式已经是最新的了。

恢复迁移
1
npx typeorm-ts-node-commonjs migration:revert -- -d [data-source-path]

此命令将在最近执行的迁移中执行 down 方法。如果需要恢复多个迁移,则必须多次调用此命令。

TypeORM

实体/模型类

TypeORM 的实体是一个通过 @Entity() 装饰的类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Entity, Column, PrimaryGeneratedColumn, JoinColumn, Generated } from "typeorm";
import type { User } from "../users/user.entity";

@Entity({ name: "order" })
export class Order {
@PrimaryGeneratedColumn()
id: number;

@Column()
@Generated("uuid")
no: string;

@Column({ type: "float" })
price: number;

@Column({ type: "int" })
count: number;

@JoinColumn({ name: "user_id", referencedColumnName: "uid" })
user: User;
}
  • @Entity():定义数据表。其参数的 name 属性指定表名称,默认为类名的小写
  • @PrimaryGeneratedColumn():定义自增长主键
  • @Column():定义数据表中普通的键,其参数信息见 https://www.typeorm.net/decorator-reference#column
  • @JoinColumn():定义外键。其参数的 name 属性指定表中外键的名称,referencedColumnName 属性指定关联的主表的键
  • @Generated():指定键的值自动生成,如 uuid

但是现在会有两个问题:
(1)通过 TypeORM 查询 order 表中的一条数据,其结果是如下形式:

1
2
3
4
5
6
{
"id": 1,
"no": "2aedac0d-aa59-443f-a5fa-b84f366c1c45",
"price": 9.9,
"count": 10
}

这里并没有外键 user_id 的值。
(2)通过 TypeORM 查询 order 表中 user_id"1" 的数据,代码如下:

1
2
const user = await this.userRepository.findOneBy({ id: "1" });
this.orderRepository.findBy({ user: user });

我们需要传递整个 User 对象,而不能直接通过 id 查询。
要解决这两个问题,我们需要在 Order 实体中额外添加一个属性:

1
2
@Column({ name: "user_id" })
userId: string;

@Column 的 name 属性必须与表中外键键名一致。
修改之后不需要迁移,第一个问题即刻解决,第二个问题现在可以这样写:

1
this.orderRepository.findBy({ user_id: "1" });
关系

表之间的关系是通过外键来实现的,通过为外键添加不同的约束可实现不同的关系:一对一、一对多/多对一、多对多。
在 TypeORM 中,可以通过 @OneToOne@ManyToOne/@OneToMany@ManyToMany 装饰器建立不同的关系,而且,这些装饰器提供了联合查询和级联查询的方法,我们只需要在查询时指定 relations 字段即可。

一对一

假如有两个实体 Husband 和 Wife,一个 husband 只能有一个 wife,一个 wife 也只能有一个 husband,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm";
import type { Wife } from "../orders/wife.entity";

@Entity()
export class Husband {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
age: number;

@OneToOne("Wife", (wife: Wife) => wife.husband)
wife: Wife;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn } from "typeorm";
import type { Husband } from "../users/husband.entity";

@Entity()
export class Wife {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
age: number;

@OneToOne("Husband", (husband: Husband) => husband.wife)
@JoinColumn({ name: "husband_id", referencedColumnName: "id" })
huaband: Husband;
}

@OneToOne 如果只在外键一侧定义,那么使用关系查询时 wife 对象中会包含 husband 对象,但是无法从 husband 查询到 wife,此时为单向关系。若在主表一侧也定义 @OneToOne,那么就可以从 huaband 查询到 wife 的信息,是双向关系。

一对多/多对一

假如有两个实体 User 和 Photo,一个 user 可以有多个 photo,一个 photo 只能有一个 user,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from "typeorm";
import type { Order } from "../orders/order.entity";

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column()
age: number;

@OneToMany("Order", (order: Order) => order.user)
orders: Order[];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from "typeorm";
import type { User } from "../users/user.entity";

@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;

@Column({ type: "float" })
price: number;

@Column({ type: "int" })
count: number;

@ManyToOne("User", (user: User) => user.orders)
@JoinColumn({ name: "user_id", referencedColumnName: "uid" })
user: User;
}

@ManyToOne 可单独使用,@OneToMany 必须与 @ManyToOne 一起使用。

多对多

多对多关系可以分解为主表对从表的一对多关系和从表对主表的一对多关系。主表对从表的一对多关系很容易实现,而由于主键的唯一性,从表对主表的一对多关系无法直接实现,而是需要借助中间表来实现。
中间表存放关联主表的外键和关联从表的外键,这样,主表的主键就可以以外键的形式在中间表中重复存在,从表对主表的一对多关系得以实现。
假如有两个实体 Category 和 Question,一个 category 可以有多个 question,一个 question 也可以有多个 category,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@ManyToMany("Question", (question) => question.categories)
questions: Question[]
}
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
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from "typeorm"
import type { Category } from "./Category"

@Entity()
export class Question {
@PrimaryGeneratedColumn()
id: number

@Column()
title: string

@Column()
text: string

@ManyToMany("Category", (category) => category.questions)
@JoinTable({
name: "question_categories",
joinColumn: {
name: "question",
referencedColumnName: "id",
foreignKeyConstraintName: "fk_question_categories_questionId"
},
inverseJoinColumn: {
name: "category",
referencedColumnName: "id",
foreignKeyConstraintName: "fk_question_categories_categoryId"
}
})
categories: Category[]
}

@JoinTable 会生成一个特殊表“junction”(中间表),由 TypeORM 自动创建,是一个特殊的单独的表,其中列引用相关实体。
我们可以更改生成的“junction”表的名称、表中的列名以及外键的相关信息。
category 表和 question 表中均无外键,多对多关系完全由中间表实现,因此 joinColumninverseJoinColumn 无特定指向。

查询

https://typeorm.io/find-options