Skip to main content

Nestjs通用配置-数据库配置

· 9 min read
Zhouxiaoxiao

开始

我们以上篇课程 Nestjs 通用配置-基础进阶 2 的代码为基础,进行后续的学习。

本篇课程我们将学习如何通过 ORM 工具 typeOrm 对数据进行配置,然后建立一个 Coffee 实体类,使用 @nestjsx/crud 快速发布 CURD 接口并通过 @nestjs/swagger 对接口进行测试。

下面是我们需要安装的扩展包

pnpm add @nestjs/typeorm typeorm mysql2  ---typeorm包和Mysql数据库驱动mysql2
pnpm add @nestjsx/crud class-transformer class-validator @nestjsx/crud-typeorm ---crud包和校验扩展包
pnpm add @nestjs/swagger swagger-ui-express nestjs-knife4 ---swagger包和对应的UI扩展包
pnpm add nestjs-pino pino-http pino ---pino日志扩展包
pnpm add -D pino-pretty ---开发环境日志显示格式化扩展包

安装数据库

方法一(推荐)

建议安装小皮面板,这是个一键安装服务环境的软件,支持 linux,windows,mac 多环境,具体可以去官网看看。

方法二

使用remotemysql.com设置数据库,这是个免费的在线数据库网站,优点是不用占用本地资源,缺点是有点慢。

安装完成后,我们新建两个数据库 testdb-devtestdb-prod,并将数据库相关配置更新到配置文件 config.development.ymlconfig.production.yml。(注意:我们这里暂时都使用 mysql1 的配置)

配置数据库

新建配置文件 database.config.ts

为了从配置文件读取数据配置信息,我们使用 TypeOrmModule.forRootAsync 方法进行数据库配置,我们从 configService 中获取对应数据库配置信息,创建 TypeOrmModuleOptions 类型对象并利用此对象,初始化对象 TypeOrmModuleAsyncOptions

提示

对象 TypeOrmModuleOptionsTypeOrmModuleAsyncOptions 都是 TypeOrmModule.forRootAsync 方法所需要的参数。

初始化完成后,我们在 app.module.ts 中注册 TypeOrmModule

database.config.ts
export default class TypeOrmConfig {
static getOrmConfig(configService: ConfigService): TypeOrmModuleOptions {
return {
type: "mysql", //数据库类型
host: configService.get < string > "db.mysql1.dbHost", //数据库地址
port: configService.get < number > "db.mysql1.dbPort" || 3306, //端口,默认3306
username: configService.get < string > "db.mysql1.dbUser", //用户名
password: configService.get < string > "db.mysql1.dbPwd", //密码
database: configService.get < string > "db.mysql1.dbBase", //数据库名称
retryDelay: 500, // 重试连接数据库间隔
retryAttempts: 10, // 允许重连次数
entities: [__dirname + "/../**/*.entity{.ts,.js}"], //扫描根目录下的所有entity文件
synchronize:
configService.get < boolean > "db.mysql1.synchronize" || false, //是否自动同步数据结构到数据库, 这个参数正式环境一定要设置成false,默认 false
};
}
}

export const typeOrmConfigAsync: TypeOrmModuleAsyncOptions = {
imports: [ConfigModule],
useFactory: (configService: ConfigService): TypeOrmModuleOptions =>
TypeOrmConfig.getOrmConfig(configService),
inject: [ConfigService],
};
app.module.ts
...
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
TypeOrmModule.forRootAsync(typeOrmConfigAsync),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

重新启动应用程序,如果能看到如下提示,则证明数据库配置成功了。

[Nest][InstanceLoader] TypeOrmCoreModule dependencies initialized +257ms
[Nest][InstanceLoader] TypeOrmModule dependencies initialized +1ms

新建目录 configuration 将文件 configuration.tsdatabase.config.ts 都放入其中,以后这个目录就存放所有配置相关的文件。

新建 Coffee 实体组件

Coffee 实体类里面我们暂时只存放两个字段 咖啡名称主键ID

我们使用 nest 命令新建 Coffee 实体类相关文件,这样做的好处是可以将信息自动注册对应的 Moudle。

使用终端进入到 src 目录执行如下命令:

nest g mo Coffee --- 新建module
nest g s Coffee --- 新建service
nest g co Coffee --- 新建controller

手工新建 Coffee.entity.ts文件

app.module.ts
@Entity()
export class Coffee {
@PrimaryGeneratedColumn()
@Exclude()
id: number;

@ApiProperty({
type: String,
description: "咖啡名称",
})
@Column()
@IsNotEmpty()
coffeeName: string;
}

新建 CRUD 接口

修改 CoffeeService 使其继承 TypeOrmCrudService,并在构造方法中自动注入 Repository<Coffee>

coffee.service.ts
@Injectable()
export class CoffeeService extends TypeOrmCrudService<Coffee> {
constructor(@InjectRepository(Coffee) repo: Repository<Coffee>) {
super(repo);
}
}

修改 CoffeeModule ,将 Coffee 实体注册到 Module 上来。

coffee.module.ts
@Module({
controllers: [CoffeeController],
providers: [CoffeeService],
imports: [TypeOrmModule.forFeature([Coffee])],
exports: [CoffeeService],
})
export class CoffeeModule {}

修改 CoffeeController,使其继承自 CrudController,增加 @Crud装饰器。

coffee.module.ts
@Crud({
model: {
type: Coffee,
},
})
@Controller('coffee')
@ApiTags('coffee')
@UseInterceptors(ClassSerializerInterceptor)
export class CoffeeController implements CrudController<Coffee> {
constructor(public service: CoffeeService) {}

@ApiOperation({
summary: 'Get all coffee',
description: '使用find()方法,获取所有咖啡数据',
})
@Get('/all-coffee')
getAllCoffees() {
return this.service.find();
}
}

完成以上工作后,基本的 CRUD 接口就完成了,这是因为使用了 @nestjsx/crud相关扩展包,会自动生成对应代码。

配置日志

configuration 目录中新建配置文件 logger.config.ts

logger.config.ts
const passUrl = new Set(['/health', '/graphql']);

export const loggerOptions: Params = {
pinoHttp: [
{
timestamp: () => `,"time":"${new Date(Date.now()).toISOString()}"`,
quietReqLogger: true,
genReqId: (req: IncomingMessage): ReqId =>
(<Request>req).header('X-Request-Id'),
...(process.env.NODE_ENV === 'production'
? {}
: {
level: 'debug',
// https://github.com/pinojs/pino-pretty
transport: {
target: 'pino-pretty',
options: { sync: true },
},
}),
autoLogging: {
ignore: (req: IncomingMessage) =>
passUrl.has((<Request>req).originalUrl),
},
customAttributeKeys: {
req: '请求信息',
res: '响应信息',
err: '错误信息',
responseTime: '响应时间(ms)',
},
level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
serializers: {
req(req: {
httpVersion: any;
raw: { httpVersion: any; params: any; query: any; body: any };
params: any;
query: any;
body: any;
}) {
req.httpVersion = req.raw.httpVersion;
req.params = req.raw.params;
req.query = req.raw.query;
req.body = req.raw.body;
return req;
},
err(err: {
params: any;
raw: { params: any; query: any; body: any };
query: any;
body: any;
}) {
err.params = err.raw.params;
err.query = err.raw.query;
err.body = err.raw.body;
return err;
},
},
},
multistream(
[
// https://getpino.io/#/docs/help?id=log-to-different-streams
{ level: 'debug', stream: process.stdout },
{ level: 'error', stream: process.stderr },
{ level: 'fatal', stream: process.stderr },
],
{ dedupe: true },
),
],
};

app.module.ts中注册 LoggerModule

app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
}),
TypeOrmModule.forRootAsync(typeOrmConfigAsync),
LoggerModule.forRoot(loggerOptions),
CoffeeModule,
],
controllers: [AppController, CoffeeController],
providers: [AppService],
})
export class AppModule {}

配置 Swagger

main.ts中增加 Swagger 配置。

main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);

//增加 swagger配置
const options = new DocumentBuilder()
.setTitle("Coffee example")
.setDescription("The Coffee API description")
.setVersion("1.0")
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("api", app, document);
//增加 knife4配置
knife4jSetup(app, {
urls: [
{
name: "2.X版本",
url: `/api-json`,
swaggerVersion: "3.0",
location: `/api-json`,
},
],
});
app.useLogger(app.get(Logger));
app.useGlobalInterceptors(new LoggerErrorInterceptor());
// 全局注册错误的过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 全局注册拦截器
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();

重启完成后访问地址http://localhost:3000/doc.html,就可以进行接口测试了。

配置全局返回统一格式

配置错误过滤器

配置错误过滤器 HttpExceptionFilter过滤所有报错返回信息,统一返回格式。

HttpException.filter.ts
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

const message = exception.message;
Logger.log("错误提示", message);
const errorResponse = {
data: {
error: message,
}, // 获取全部的错误信息
message: "请求失败",
code: 1, // 自定义code
url: request.originalUrl, // 错误的url地址
};
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// 设置返回的状态码、请求头、发送错误信息
response.status(status);
response.header("Content-Type", "application/json; charset=utf-8");
response.send(errorResponse);
}
}

配置全局拦截器

配置错误过滤器 TransformInterceptor拦截所有正常返回信息,统一返回格式。

Transform.interceptor.ts
interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 0,
message: "请求成功",
};
})
);
}
}

TransformInterceptorHttpExceptionFiltermain.ts中注册到 APP 中即可。

总结

通过以上的学习,我们完成了以下任务:

1、完成数据库的安装。
2、通过TypeOrm整合数据库。
3、通过nest的命令生成Coffee实体对象。
4、通过nestjs/pino增强日志输出。
5、通过nestjs/curd自动生成CURD接口。
6、通过Swagger测试接口。
7、通过拦截器和过滤器对返回的数据进行统一封装。

源码

源码可以参考这里config-typeorm,喜欢的话给个 star 吧。