likes
comments
collection
share

(四) NestJS-数据库连接 MongoDB

作者站长头像
站长
· 阅读数 11

示例工程 GitHub

NestJS 本身于数据库无关,使用者根据需要选择数据库来使用。NestJS网站中database 这篇文章以MySQL为例,介绍了集成了 TypeORM 的 NestJS 如何连接数据库以及使用数据库相关的各项功能的,内容非常扎实。

我的项目是物联网相关的,因此我们数据库是选择了MongoDB,接下来的数据库连接就是以MongoDB为例来做演示的。演示内容必然参考了Mongo 这一章,同时也结合了自己的使用经验。对于Schema的各种使用示例 database 这篇文章提供了很多有用的信息。推荐大家阅读下面的文章以加深了解。

推荐阅读

ORM-Object–relational mapping

NestJS-Database

NestJS-Mongo

1. 数据库连接

连接之前安装相关依赖

$ npm i @nestjs/mongoose mongoose

连接前需要根据自己的工程情况去决定连接需要使用的范围是需要在全局还是只在某个模块内部,连接方式是同步还是异步。MongooseModule 提供的对于的方法实现连接,方法名称上可以直接区分。

export declare class MongooseModule {
    static forRoot(uri: string, options?: MongooseModuleOptions): DynamicModule;
    static forRootAsync(options: MongooseModuleAsyncOptions): DynamicModule;
    static forFeature(models?: ModelDefinition[], connectionName?: string): DynamicModule;
    static forFeatureAsync(factories?: AsyncModelFactory[], connectionName?: string): DynamicModule;
}

下面演示了异步连接与同步连接两种方式及其配置如果实现。

1) 全局异步连接

如果测试数据库没有开启鉴权,直接使用MongooseModule.forRoot第一个参数填入连接Url即可完成连接。

...
@Module({
  imports: [
    ConfigModule.forRoot({envFilePath: '.env', load: [AppConfig]}),
    MongooseModule.forRoot('mongodb://localhost/test'), // 数据库连接在这里
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
...

第二参数是连接相关的选项,可根据需要加入对应的配置

export interface MongooseModuleOptions extends ConnectOptions {
    uri?: string;
    retryAttempts?: number;
    retryDelay?: number;
    connectionName?: string;
    connectionFactory?: (connection: any, name: string) => any;
    connectionErrorFactory?: (error: MongooseError) => MongooseError;
    lazyConnection?: boolean;
}

2)全局同步连接

大部分情况下访问数据库都是需要鉴权的,并且很多应用也需要在启动时就连接到数据库来避免异步连接造成某些场景中发生错误。因此下面演示使用ConfigModule加载在自动配置文件提供的数据来同步连接MongoDB

先看一下数据库相关配置的情况,数据库连接信息都是存储在环境变量文件中的,即.env文件,因为生产环境和开发环境的配置大概率是不一样的。这里我准备两个数据库 appdata 方便演示多个数据库连接的场景。

1) .env 文件配置

# 包括数据库服务地址和端口,各个数据库的名称和用户信息

DB_HOST=127.0.0.1
DB_PORT=27017

APP_DB_NAME=app
APP_DB_USER=admin@app
APP_DB_PASS=admin4DEV
APP_DB_AUTH_SOURCE=app

DATA_DB_NAME=data
DATA_DB_USER=admin@data
DATA_DB_PASS=admin4DEV
DATA_DB_AUTH_SOURCE=data

2)Configs 文件配置

// src/configs/appConfig
export default () => {
  return {
    port: parseInt(process.env.APP_PORT, 10),
    appDb: {
      uri: `mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}`,
      dbName: process.env.APP_DB_NAME,
      user: process.env.APP_DB_USER,
      pass: process.env.APP_DB_PASS,
      authSource: process.env.APP_DB_AUTH_SOURCE,
      autoIndex: true,
      autoCreate: true,
      useNewUrlParser: true,
      useUnifiedTopology: true,
    },
    dataDb: {
      uri: `mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}`,
      dbName: process.env.DATA_DB_NAME,
      user: process.env.DATA_DB_USER,
      pass: process.env.DATA_DB_PASS,
      authSource: process.env.DATA_DB_AUTH_SOURCE,
      autoIndex: true,
      autoCreate: true,
      useNewUrlParser: true,
      useUnifiedTopology: true,
    },
  };
};

然后在AppModule里面直接使用,使用自定义配置文件那一篇时已经介绍了如果引入,这里就直接使用。下面这条经验一定要注意,之前遇到这个问题花了好久才解决,文档里似乎没提过这个问题。

⚠️ 如果工程下只有一个数据库连接,那么connectionName是否指定名称都不影响。但如果是多个数据库连接,一定要connectionName 否则在应用是会报错

...
@Module({
  imports: [
    ...
    ConfigModule.forRoot({
      envFilePath: '.env',
      load: [AppConfig],
    }),
    MongooseModule.forRootAsync({
      connectionName: 'APP_DB',
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return configService.get('appDb');
      },
    }),
    MongooseModule.forRootAsync({
      connectionName: 'DATA_DB',
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return configService.get('dataDb');
      },
    }),
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

forFeature, forFeatureAsync,后面介绍应用时会涉及这里下暂且不表。

2. Schema 配置

在使用数据库连接或者使用数据库中的docment的之前,我们先实现Schema的定义,这里我直接使用的官网提供的TypeORM实现Schema,文档里面提供的信息提到 mongoose 应该的,但是我跟偏好使用官网推荐的,感觉更规范一些。下面我们实现一个User Schema的定义。

1)user.schema.ts

(四) NestJS-数据库连接 MongoDB

// src/schema/user.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type UserDocumentType = HydratedDocument<User>;

@Schema()
export class User {
  @Prop({ required: true, unique: true, index: true })
  username: string;

  @Prop({ default: '' })
  nickname: string;

  @Prop({ required: true })
  organization: string;

  @Prop({ required: true })
  password: string;

  @Prop({ default: '' })
  email: string;

  @Prop({ default: '' })
  phone: string;

  @Prop({ required: true })
  createdAt: Date;

  @Prop({ required: true })
  createdBy: string;

  @Prop()
  updatedAt: Date;

  @Prop({ default: '' })
  deletedBy: string;

  @Prop({ default: null })
  deletedAt: Date;
}

export const UserSchema = SchemaFactory.createForClass(User);

2)一些其他知识点

  • 移除versionKey,只需主要在@Schema装饰器中指定配置

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { HydratedDocument } from 'mongoose';
    
    export type UserDocumentType = HydratedDocument<User>;
    
    // 移除 versionKey
    @Schema({versionKey:false})
    export class User {
      @Prop({ required: true, unique: true, index: true })
      username: string;
      ...
    }
    
    export const UserSchema = SchemaFactory.createForClass(User);
    
  • 移除某个值为Object属性的_id

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { HydratedDocument, Schema as MongooseSchema } from 'mongoose';
    
    export type UserDocumentType = HydratedDocument<User>;
    
    @Schema({versionKey:false})
    export class User {
      @Prop({ required: true, unique: true, index: true })
      username: string;
      // 移除 _id
      @Prop({ _id: false, type:{ math: MongooseSchema.Types.Number, eng: MongooseSchema.Types.Number} })
      score:{math: number, eng:number}
      ...
    }
    
    export const UserSchema = SchemaFactory.createForClass(User);
    

3. 数据库连接引用

1) 使用数据库连接

这里完全是官网提供的信息,有时候在需要调用原始的API来做一些操作时,会用到这个数据库连接。我没用使用经验,所以这里就提供一个成功引入的示例。工程下存在多个数据库连接时,注意连接时是需要指定连接的名称。如果按上面多个数据库连接配置,应该不会又问题的。

import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

@Injectable()
export class AppService {
  constructor(
    @InjectConnection('APP_DB') private appDbConnection: Connection, // 在这里
  ) {}
  getHello(): string {
    return 'Hello World!';
  }
}

2)实现数据操作

通常情况下都是使用集合模型实现数据的增删改查,下面以User为例,在app service中引用userModel。引入需要两步:

(1)Module 文件配置数据库中的文档对象的属性, 包括名称shema数据库里面集合的名称以及数据源名称,都需要一一对应。然后使用 MongooseModule.forFeature 实现引入。

// src/app.module
...
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schema/user.schema';
import { UsersModule } from './users/users.module';
@Module({
  imports: [
   ...
    // 在这里
    MongooseModule.forFeature(
      [
        {
          name: User.name,
          schema: UserSchema,
          collection: 'users',
        },
      ],
      'APP_DB',
    ),
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

(2)在 Service 中引入,是通过@InjectModel装饰器实现的,在constructor中添加引入,然后在Service的其他方法中直接调用即可。

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User } from './schema/user.schema';
import { Model } from 'mongoose';

@Injectable()
export class AppService {
  constructor(
    @InjectModel(User.name, 'APP_DB') private readonly userModel: Model<User>, // 在这里
  ) {}
  async getHello() {
    const users = await this.userModel.find({}).limit(10).exec();
    return await users;
  }
}

NestJS 中还内置了一些实用的方法,有的还挺实用的。

(四) NestJS-数据库连接 MongoDB