Nestjs框架: 集成 TypeORM 和 Mongoose

概述

  • 虽然 Prisma 用起来非常舒服,不需要创建 repository,它就能实现很好的数据库连接
  • 同时,它对 TypeScript 的支持也非常出色
  • 不过,在一些极端应用场景下,尤其是在关键性数据库方面,TypeORM 的兼容性比某些其他工具要好。

集成 TypeORM


1 ) 安装依赖

  • $ $ npm install --save @nestjs/typeorm typeorm mysql2

2 ) 配置

环境变量 .env 的配置

DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=example
DB_DATABASE=testdb
DB_AUTOLOAD=true
DB_SYNC=true

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { User } from './user/user.entity';

@Module({
  imports: [
    // 下面这个后续可以封装一个新的模块,来匹配 .env 和 其他配置
    ConfigModule.forRoot({  // 配置环境变量模块 
      envFilePath: '.env', // 指定环境变量文件路径 
      isGlobal: true, // 全局可用 
    }), 
    TypeOrmModule.forRootAsync({
      inject:[ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: configService.get<string>('DB_TYPE'),
        host: configService.get<string>('DB_HOST'),  
        port: configService.get<number>('DB_PORT'),  
        username: configService.get<string>('DB_USERNAME'),  
        password: configService.get<string>('DB_PASSWORD'),  
        database: configService.get<string>('DB_DATABASE'),
        autoLoadEntities: Boolean(configService.get<string | boolean>('DB_AUTOLOAD', false)), 
        synchronize: Boolean(configService.get<string | boolean>('DB_SYNC', false)), 
      } as TypeOrmModuleOptions),
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AppController],
  providers: []
})

export class AppModule {} 
  • 它是通过创建 TypeOrmModule.forRootSync 并传入数据库连接配置来实现与数据库的连接
  • 配置完成后,就可以创建对应的实体,比如创建 user 实体

3 ) 创建实体

src/user/user.entity

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

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

	@Column()
	username: string;

	@Column()
	password: string;
}

4 ) 编辑测试控制器

app.controller.ts

import { Controller, Get } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user/user.entity';
import { Repository } from 'typeorm';

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

  @Get()
  async getUsers(): Promise<any> {
    const rs = await this.userRepository.find();
    return rs;
  }
}
  • 打开浏览器访问 localhost:3030/,得到的结果和预期一致
  • 至此,TypeORM 的集成就算完成了

集成 Mongoose


1 ) 安装

  • $ npm i @nestjs/mongoose mongoose

2 ) 配置 app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './user/user.schema'
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    // 下面这个后续可以封装一个新的模块,来匹配 .env 和 其他配置
    ConfigModule.forRoot({  // 配置环境变量模块 
      envFilePath: '.env', // 指定环境变量文件路径 
      isGlobal: true, // 全局可用 
    }),
    MongooseModule.forRootAsync({
      inject: [ConfigService], // 注入配置服务
      useFactory: async (configService: ConfigService) => ({ 
        uri: configService.get('MONGODB_URI'),  // 从配置服务中获取连接字符串 
      }), 
      
    }),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])
  ],
  controllers: [AppController],
  providers: [],
})

export class AppModule {}

3 ) 新建 mongo 服务

docker-compose.mongo.yaml

services:
	mongo:
		image: mongo
		restart: always
		environment:
			MONGO_INITDB_ROOT_USERNAME: root
			MONGO_INITDB_ROOT_PASSWORD: example
		ports:
			- 27018: 27017
			
	mongo-express:
		image: mongo-express
		restart: always
		environment:
			ME_CONFIG_MONGODB_ADMINUSERNAME: root
			ME_CONFIG_MONGODB_ADMINPASSWORD: example
			ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
			ME_CONFIG_BASICAUTH: false
		ports:
			- 8081:8081
  • $ docker compose -f docker-compose.mongo.yml up -d
  • 进入

4 ) 创建数据库

  • 打开 8081 UI 界面, 创建一个名称为 nest 的数据库

5 )编辑 user 相关 schema

src/user/user.schema.ts

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

export type UserDocument = HydratedDocument<User>;

@Schema()
export class User {
	@Prop()
	username: string;
	
	@Prop()
	password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);

6 ) 控制器的配置

app.controller.ts

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

@Controller()
export class AppController {
  constructor(
    @InjectModel(User.name)
    private userModel: Model<User>
  ) {}

  @Get()
  async getUsers(): Promise<any> {
    const rs = await this.userModel.find()
    return rs;
  }
}

7 ) 创建数据并测试

添加测试数据

  • 打开 8081 UI 界面, 在 nest 数据库中,新增 相关 collection, 如新建 users,这里后面多加 s
  • 之后,自行添加 document, 注意 “_id” 不要动,添加 username 和 password 字段就行

为管理员账号分配权限

  • $ docker ps | grep mongo 找到容器名
  • $ docker exec -it nestjs-starter-mongo-1 mongosh -u root -p 之后,输入密码
  • $ show dbs;
  • $ use nest;
  • $ db.createUser({user: 'root', pwd: 'example', roles: [{role: 'dbOwner', db: 'nest'}]})
    • 如果返回 { ok : 1 } 就成功了

测试

  • localhost:3030/
  • 能返回数据,就成功了

当然,也可按照文档上,另一种方式来配置 Schema 和 其他, 如下:

集成 Mongoose 方式2


1 ) 修改 app.module.ts

MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])

这里的 User 和 下面的 User 字符串都可以单独定义出来成一个常量

2 ) 修改 user/user.schema.ts

import mongoose, { Document } from 'mongoose';

export interface User extends Document {
  name: string;
  password: string;
}

export const UserSchema = new mongoose.Schema(
	{
		username: String,
		password: String,
	},
	{ collection: 'users' },
);

import mongoose from 'mongoose';

export const UserSchema = new mongoose.Schema(
	{
		username: String,
		password: String,
	},
	{ collection: 'users' },
);

3 ) 修改 app.controller.ts

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

@Controller()
export class AppController {
  constructor(
    @InjectModel('User')
    private userModel: Model<User>
  ) {}

  @Get()
  async getUsers(): Promise<any> {
    const rs = await this.userModel.find()
    return rs;
  }
}

import { Controller, Get } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Document as Doc } from 'mongoose';

@Controller()
export class AppController {
  constructor(
    @InjectModel('User')
    private userModel: Model<Doc>
  ) {}

  @Get()
  async getUsers(): Promise<any> {
    const rs = await this.userModel.find()
    return rs;
  }
}
  • 这里的 User 字符串可以单独定义出去,形成一个变量

深入 mongoose 原理

  • @nestjs/mongoose 这个官方的包

  • 进入 lib 目录,查看 index.ts

    export * from './common';
    export * from './decorators';
    export * from './errors';
    export * from './factories';
    export * from './interfaces';
    export * from './mongoose.module';
    export * from './pipes';
    export * from './utils';
    
  • 可以看到它导出来很多方法

  • 先看 decorators 目录

    • 其中有两个装饰器非常重要,一个叫做 schema.decorator.ts
    • 第二个叫 prop.decorator.ts,这两个装饰器让 @nestjs/mongoose
    • 更加贴合于 NestJS 的设计思想,代码的可读性更强。
  • 在 schema.decorator.ts 中

    import * as mongoose from 'mongoose';
    import { TypeMetadataStorage } from '../storages/type-metadata.storage';
    
    /**
     * Interface defining schema options that can be passed to `@Schema()` decorator.
     */
    export type SchemaOptions = mongoose.SchemaOptions;
    
    /**
     * @Schema decorator is used to mark a class as a Mongoose schema.
     * Only properties decorated with this decorator will be defined in the schema.
     * 
     * @publicApi
     */
    export function Schema(options?: SchemaOptions): ClassDecorator {
      return (target: Function) => {
        TypeMetadataStorage.addSchemaMetadata({
          target,
          options,
        });
      };
    }
    
  • 这里调用 TypeMetadataStorage.addSchemaMetadata 方法

  • 进入 TypeMetadataStorage 这里可知,这里是一个工具集合,进入 addSchemaMetadata

    addSchemaMetadata(metadata: SchemaMetadata) {
    	this.compileClassMetadata(metadata);
    	this.schemas.push(metadata);
    }
    
    • 这里metadata就是我们定义的 schema 被添加到 schemas 数组中
  • 进入 moogoose.module.ts 核心模块,有两个核心方法

    • MongooseCoreModule.forRoot
    • createMongooseProviders
  • 进入 moggoose.providers.ts 核心代码在这里

    {
        provide: getModelToken(option.name, connectionName),
        useFactory: (connection: Connection) => {
          const model = connection.models[option.name]
            ? connection.models[option.name]
            : connection.model(option.name, option.schema, option.collection);
          return model;
        },
        inject: [getConnectionToken(connectionName)],
    }
    
    • 注意,完整代码是递归执行,里面很可能是 schema 套 schema
  • 这里对应,https://mongoosejs.com 官网的这段代码

    const mongoose = require('mongoose');
    mongoose.connect('mongodb://127.0.0.1:27017/test');
    
    const Cat = mongoose.model('Cat', { name: String });
    
    const kitty = new Cat({ name: 'Zildjian' });
    kitty.save().then(() => console.log('meow'));
    
    • 上述原理代码代表了如何查找到相关的表
  • 再看 mongoose-core.module.ts 最核心的代码

    const connectionProvider = {
      provide: mongooseConnectionName,
      useFactory: async (): Promise<any> =>
        await lastValueFrom(
          defer(async () =>
            mongooseConnectionFactory(
              await this.createMongooseConnection(uri, mongooseOptions, {
                lazyConnection,
                onConnectionCreate,
              }),
              mongooseConnectionName,
            ),
          ).pipe(
            handleRetry(retryAttempts, retryDelay, verboseRetryLog),
            catchError((error) => {
              throw mongooseConnectionError(error);
            }),
          ),
        ),
    };
    
    • 核心在这里, createMongooseConnection, 它具体就是调用 mongoose.createConnection 创建一次连接
  • 进入 createMongooseConnection

      private static async createMongooseConnection(
        uri: string,
        mongooseOptions: ConnectOptions,
        factoryOptions: {
          lazyConnection?: boolean;
          onConnectionCreate?: MongooseModuleOptions['onConnectionCreate'];
        },
      ): Promise<Connection> {
        const connection = mongoose.createConnection(uri, mongooseOptions);
    
        if (factoryOptions?.lazyConnection) {
          return connection;
        }
    
        factoryOptions?.onConnectionCreate?.(connection);
    
        return connection.asPromise();
      }
    
      async onApplicationShutdown() {
        const connection = this.moduleRef.get<any>(this.connectionName);
        if (connection) {
          await connection.close();
        }
      }
    }
    
    • 这里最核心的就是 调用 mongoose.createConnection 创建一次连接并返回
    • 这里的 lazyConnection 是可选配置,这里叫做延迟创建,用于改善性能
  • 以上的代码流程,后续可以详细研究下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值