Javascript

nestjs搭建通用业务框架(5):数据库+配置

本文主要是介绍nestjs搭建通用业务框架(5):数据库+配置,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

这是《nestjs搭建通用业务框架》系列的第5篇进入开发具体的功能之前学习nest框架本身提供的CLI工具与规划合理的工程目录对于要实现的内容进行架构与计划这是实现健壮高可用的框架的前提。

技术整合会从三个层次进行介绍

  • 数据库 -> 配置(多环境) -> 配置验证 -> 系统日志本篇

  • 跨域 -> 错误拦截器 -> 缓存Redis

  • 数据校验 -> 日志拦截 -> 鉴权

数据库

ORM工具库

通过数据库集成库或 ORM 例如 Sequelize (recipe)和 TypeORM 以在更高的抽象级别上进行操作。

ORM对象关系映射英语Object Relational Mapping是一种程序设计技术用于实现面向对象编程语言里不同类型系统的数据之间的转换。 从效果上说它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。

应用场景

  • SQL -> DB我们写一套配置针对不同的数据库都可以方便的接入

  • DB -> SQL针对不同的数据库都可以通过抽象层进行联接

Nest 还提供了与现成的 TypeORM 与 @nestjs/typeorm 的紧密集成我们将在本章中对此进行介绍而与 @nestjs/mongoose 的紧密集成将在官方的这一章中介绍

目前主要的ORM工具库与特点

  • typeorm跨库查询事务、TS支持支持数据库MySQL, MariaDB, Postgres, CockroachDB, SQLite, MSSQL, Oracle, SAPHana, sql.js, MongoDB

  • objection TS、事务、饥饿加载、数据效验基于knexjs支持数据库Postgres**, MSSQL, MySQL, MariaDB, SQLite3, **Oracle, Amazon Redshift

  • sequelize 有非官方的中文文档目前缺少核心的维护与开发。支持PostgreSQL, MySQL, MariaDB, SQLite, MSSQL

  • prisma后起之秀(官方文档写的很不错)SQL自动合并对接GraphQL客户端、服务端+数据管理GUI支持PostgreSQL, MSSQL, MySQL, SQLite

通过上面的简单对比目前来看TypeORM是nest官方支持且推荐的可以来这里看看它的特性。

集成Postgre(MySQL)

步骤

  • 安装@nestjs/typeormtypeorm

  • 安装nodejs侧的数据库驱动程序如mysqlpg

  • 新建数据库配置文件配置数据库

  • app.module.ts引入数据库的配置文件调用TypeOrmModule.forRoot方法

  • 启动程序进行测试

安装依赖跳过数据库安装过程

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

按照上面的步骤创建文件src/config/database.config.ts

数据库配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
import { TypeOrmModuleOptions } from '@nestjs/typeorm'

export const typeOrmConfig: TypeOrmModuleOptions = {
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'postgres',
  password: 'changeme',
  database: 'demo-db',
  entities: [`${__dirname}/../entity/**/*.{js,ts}`],
  synchronize: false,
  logging: ["error"],
}

src/app.module.ts中添加TypeOrm配置

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config';


@Module({
  imports: [
    TypeOrmModule.forRoot(typeOrmConfig)
  ],
  providers: [],
})
export class AppModule {}

然后就可以使用npm run start:dev来进行调试了。

集成MongoDB

非关系型数据库MongoDB同样可以使用TypeORM官方提供了@nestjs/mongoose包所以我们来介绍两种集成方法

官方@nestjs/mongoose

安装依赖

1
npm install --save @nestjs/mongoose mongoose

配置app.module.ts

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

@Module({
  imports: [MongooseModule.forRoot('mongodb://localhost:27017/test')],
})
export class AppModule {}

使用mongoose

安装依赖

1
npm install --save mongoose

新建src/database/database.providers.ts

1
2
3
4
5
6
7
8
9
import * as mongoose from 'mongoose';

export const databaseProviders = [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: async (): Promise<typeof mongoose> =>
      await mongoose.connect('mongodb://localhost:27017/test'),
  },
];

新建src/database/database.module.ts

1
2
3
4
5
6
7
8
import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
  providers: [...databaseProviders],
  exports: [...databaseProviders],
})
export class DatabaseModule {}

配置app.module.ts

1
2
3
4
5
6
7
8
9
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module.ts';

@Module({
  imports: [
  	DatabaseModule,
  ],
})
export class AppModule {}


配置

应用程序通常在不同的环境中运行根据环境DevelopmentProduction的不同应该使用不同的配置设置。

两种方法

  • 使用@nestjs/config来实现对.envkey=value对进行解析

  • 使用config库解析yaml格式的文件

官方@nestjs/config

最简单的用法

1
npm i --save @nestjs/config

配置src/app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

然后创建.env文件

1
2
DATABASE_USER=test
DATABASE_PASSWORD=test123

下面来使用src/app.controller.ts中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private configService: ConfigService,
  ) {}

  @Get()
  getHello(): string {
    const dbUser = this.configService.get<string>('DATABASE_USER');
    console.log(dbUser); // 这里来测试
    return this.appService.getHello();
  }
}

如果访问localhost:3000即可以看到

1
2
3
4
5
6
7
8
[Nest] 14039   - 2021/03/13 下午9:43:54   [NestFactory] Starting Nest application...
[Nest] 14039   - 2021/03/13 下午9:43:54   [InstanceLoader] ConfigHostModule dependencies initialized +95ms
[Nest] 14039   - 2021/03/13 下午9:43:54   [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 14039   - 2021/03/13 下午9:43:54   [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 14039   - 2021/03/13 下午9:43:54   [RoutesResolver] AppController {}: +7ms
[Nest] 14039   - 2021/03/13 下午9:43:54   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 14039   - 2021/03/13 下午9:43:54   [NestApplication] Nest application successfully started +2ms
test

进阶玩法

http://img4.sycdn.imooc.com/605c821c0001bc0710700312.jpg

从这里点进去我们发现ConfigModuleOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ConfigFactory } from './config-factory.interface';
export interface ConfigModuleOptions {
    cache?: boolean;
    isGlobal?: boolean;
    ignoreEnvFile?: boolean;
    ignoreEnvVars?: boolean;
    envFilePath?: string | string[];
    encoding?: string;
    validate?: (config: Record<string, any>) => Record<string, any>;
    validationSchema?: any;
    validationOptions?: Record<string, any>;
    load?: Array<ConfigFactory>;
    expandVariables?: boolean;
}

所支持的参数。

我们可以利用envFilePath配合NODE_ENV来在不同的启动命令的时候使用不同的配置。

1
npm i cross-env

然后添加两个文件.env.development.env.production比如.env.production

1
2
DATABASE_USER=test1
DATABASE_PASSWORD=test123321

下面修改scripts

1
"start:prod": "cross-env NODE_ENV=production node dist/main",

可以设置app.module.ts中默认是development

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';

const envPath = `.env.${process.env.NODE_ENV || 'development'}`;
console.log(' ~ file: app.module.ts ~ line 7 ~ envPath', envPath);

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envPath,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

同样大家可以启动了测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜ npm run start:prod

> nestjs-common-template@0.0.1 start:prod /Users/macos/Projects/nestjs/nestjs-common-template
> cross-env NODE_ENV=production node dist/main ~ file: app.module.ts ~ line 7 ~ envPath .env.production
[Nest] 14977   - 2021/03/13 下午11:10:13   [NestFactory] Starting Nest application...
[Nest] 14977   - 2021/03/13 下午11:10:13   [InstanceLoader] ConfigHostModule dependencies initialized +34ms
[Nest] 14977   - 2021/03/13 下午11:10:13   [InstanceLoader] ConfigModule dependencies initialized +1ms
[Nest] 14977   - 2021/03/13 下午11:10:13   [InstanceLoader] AppModule dependencies initialized +1ms
[Nest] 14977   - 2021/03/13 下午11:10:13   [RoutesResolver] AppController {}: +6ms
[Nest] 14977   - 2021/03/13 下午11:10:13   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 14977   - 2021/03/13 下午11:10:13   [NestApplication] Nest application successfully started +3ms
test1

上面打印的test1正是我们设置在.env.production中的内容。


解析yaml格式的配置

步骤

 下载js-yaml@types/js-yaml

1
2
npm i js-yaml
npm i -D @types/js-yaml

  创建配置config.yml

1
2
3
4
5
6
7
8
9
10
11
12
http:
  host: 'localhost'
  port: 8080

db:
  postgres:
    url: 'localhost'
    port: 5432
    database: 'yaml-db'

  sqlite:
    database: 'sqlite.db'

配置自定义文件configuration.ts

1
2
3
4
5
6
7
8
9
10
11
import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import { join } from 'path';

const YAML_CONFIG_FILENAME = 'config.yml';
const filePath = join(__dirname, YAML_CONFIG_FILENAME);

export default () => {
  return yaml.load(readFileSync(filePath, 'utf8'));
};
  调用forRoot中的load方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import Configuration from './config/configuration'; // 这里调整

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [Configuration], // load方法
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 修改app.controller.ts中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Controller, Get } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppService } from './app.service';
import { DatabaseConfig } from './interface';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private configService: ConfigService,
  ) {}

  @Get()
  getHello(): string {
    const db = this.configService.get<DatabaseConfig>('db');
    console.log(db);
    return this.appService.getHello();
  }
}

定义src/interface.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface DatabaseConfig {
  postgres: PostgresConfig;
  sqlite: SqliteConfig;
}

export interface PostgresConfig {
  url: string;
  port: number;
  database: string;
}

export interface SqliteConfig {
  database: string;
}

最后测试

1
2
3
4
5
6
7
8
9
10
11
[Nest] 16960   - 2021/03/13 下午11:34:00   [NestFactory] Starting Nest application...
[Nest] 16960   - 2021/03/13 下午11:34:00   [InstanceLoader] ConfigHostModule dependencies initialized +30ms
[Nest] 16960   - 2021/03/13 下午11:34:00   [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 16960   - 2021/03/13 下午11:34:00   [InstanceLoader] AppModule dependencies initialized +0ms
[Nest] 16960   - 2021/03/13 下午11:34:00   [RoutesResolver] AppController {}: +4ms
[Nest] 16960   - 2021/03/13 下午11:34:00   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 16960   - 2021/03/13 下午11:34:00   [NestApplication] Nest application successfully started +1ms
{
  postgres: { url: 'localhost', port: 5432, database: 'yaml-db' },
  sqlite: { database: 'sqlite.db' }
}

写到这里应该够用了代码可以查看本次提交。


使用config库解析

步骤

 安装第三方包config

1
2
npm i config -S
npm i cross-env -D

新建 配置文件config/default.json同样还可以建立development.jsonproduction.json

1
2
3
4
5
{
  "server": {
    "happy": "my default value"
  }
}

development.json:

1
2
3
4
5
6
7
8
{
  "server": {
    "port": 3001,
    "host": "localhost",
    "username": "test",
    "password": "test"
  }
}
production.json:
1
2
3
4
5
6
7
8
{
  "server": {
    "port": 3002,
    "host": "localhost",
    "username": "prod",
    "password": "prod"
  }
}
app.controller.ts中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import * as config from 'config';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    const server = config.get('server');
    console.log(server);
    return this.appService.getHello();
  }
}
配置脚本
1
2
"start:dev": "cross-env NODE_ENV=development nest start --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
运行结果
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
➜ npm run start:dev
[Nest] 34580   - 2021/03/14 上午12:50:42   [NestFactory] Starting Nest application...
[Nest] 34580   - 2021/03/14 上午12:50:42   [InstanceLoader] AppModule dependencies initialized +34ms
[Nest] 34580   - 2021/03/14 上午12:50:42   [RoutesResolver] AppController {}: +6ms
[Nest] 34580   - 2021/03/14 上午12:50:42   [RouterExplorer] Mapped {, GET} route +3ms
[Nest] 34580   - 2021/03/14 上午12:50:42   [NestApplication] Nest application successfully started +2ms
{
  happy: 'my default value',
  port: 3001,
  host: 'localhost',
  username: 'test',
  password: 'test'
}


➜ npm run start:prod

> nestjs-common-template@0.0.1 start:prod /Users/macos/Projects/nestjs/nestjs-common-template
> cross-env NODE_ENV=production node dist/main

[Nest] 34400   - 2021/03/14 上午12:50:03   [NestFactory] Starting Nest application...
[Nest] 34400   - 2021/03/14 上午12:50:03   [InstanceLoader] AppModule dependencies initialized +71ms
[Nest] 34400   - 2021/03/14 上午12:50:03   [RoutesResolver] AppController {}: +6ms
[Nest] 34400   - 2021/03/14 上午12:50:03   [RouterExplorer] Mapped {, GET} route +2ms
[Nest] 34400   - 2021/03/14 上午12:50:03   [NestApplication] Nest application successfully started +2ms
{
  happy: 'my default value',
  port: 3002,
  host: 'localhost',
  username: 'prod',
  password: 'prod'
}

附上代码地址


配置验证

配置验证主要是指在应用程序启动时如果没有提供所需的环境变量或不符合某些验证规则就会抛出一个异常。@nestjs/config包实现了两种不同的方式来实现这一点。

  • Joi内置验证器。通过Joi你可以定义一个对象模式并根据它验证JavaScript对象

  • 一个自定义的validate()函数它将环境变量作为输入

Joi用法

特别说明

  • 最新版本的joi需要你运行Node v12或更高版本。旧版本的node请安装v16.1.8。这主要是因为在v17.0.2发布后在构建的时候会出现错误。更多信息请参考其17.0.0发布说明点击这里。

  • joi最好配合官方的@nestjs/config进行使用

步骤

安装依赖

1
npm install --save joi

定义验证Schema

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
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import * as Joi from 'joi';
import { ConfigModule } from '@nestjs/config';

const envPath = `.env.${process.env.NODE_ENV || 'development'}`;

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envPath,
      // 这里多了一个属性validationSchema
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_USER: Joi.string().required()
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

 验证测试

配置错误脚本

1
"start:dev": "cross-env NODE_ENV=development PORT=toimc nest start --watch",
配置正确的脚本
1
"start:dev": "cross-env NODE_ENV=development PORT=3000 nest start --watch",
测试命令
1
npm run start:dev
 错误的提示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[下午7:33:38] Found 0 errors. Watching for file changes.

/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61
                throw new Error(`Config validation error: ${error.message}`);
                ^

Error: Config validation error: "PORT" must be a number
    at Function.forRoot (/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61:23)
    at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/app.module.js:21:35)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/main.js:4:22)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
或者修改.env.development中的配置信息
1
2
DATABASE_USER=
DATABASE_PASSWORD=test123
错误提示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61
                throw new Error(`Config validation error: ${error.message}`);
                ^

Error: Config validation error: "DATABASE_USER" is not allowed to be empty
    at Function.forRoot (/Users/macos/Projects/nestjs/nestjs-common-template/node_modules/_@nestjs_config@0.6.3@@nestjs/config/dist/config.module.js:61:23)
    at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/app.module.js:21:35)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/Users/macos/Projects/nestjs/nestjs-common-template/dist/main.js:4:22)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)

结论使用Joi可以很方便对传入应用程序的参数进行验证可以限制传入的数据类型。

除了上面写的验证以外还可以加入以下属性来验证输入的命令参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envPath,
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test', 'provision')
          .default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_USER: Joi.string().required()
      }),
      validationOptions: { // 这里加
        allowUnknown: false,
        abortEarly: true,
      },
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

@nestjs/config包使用的默认设置是

  • allowUnknown控制是否允许在环境变量中使用未知键。默认为true

  • abortEarly如果为true则在第一个错误时停止验证如果为false则返回所有错误。默认值为false。

注意上面的Joi的用法

  • 主要是校验process.env传入的参数

  • 主要是校验envFilePath初次加载的时候的参数

使用class-validator

步骤

 安装依赖class-validatorclass-transformer

1
npm i class-validator class-transformer

 配置效验文件src/env.validation.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
import { plainToClass } from 'class-transformer';
import { IsEnum, IsNumber, validateSync } from 'class-validator';

enum Environment {
  Development = "development",
  Production = "production"
}

class EnvironmentVariables {
  @IsEnum(Environment)
  NODE_ENV: Environment;

  @IsNumber()
  PORT: number;
}

export function validate(config: Record<string, unknown>) {
  const validatedConfig = plainToClass(
    EnvironmentVariables,
    config,
    { enableImplicitConversion: true },
  );
  const errors = validateSync(validatedConfig, { skipMissingProperties: false });

  if (errors.length > 0) {
    throw new Error(errors.toString());
  }
  return validatedConfig;
}
调整app.module.ts文件
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 { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { validate } from './env.validation';

const envPath = `.env.${process.env.NODE_ENV || 'development'}`;

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envPath,
      validate,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

与使用Joi验证结果一致。


小结

  • 使用第三方的包config可以方便的读取配置信息但是校验却需要在读取的位置来加对于不需要验证而需要全局使用的配置项可以使用这种方式

  • 官方的@nestjs/config可以方便的导入.env的文件同时结合js-yaml也可以导入yaml格式的配置。

    配置灵活而且可以配合验证工具Joi进行参数的验证推荐

    自定义的校验第三方包class-validator这里只是冰山一角后面在学习数据验证的时候还会使用到它





这篇关于nestjs搭建通用业务框架(5):数据库+配置的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!