NestJS:通过请求(子域)连接数据库(TypeORM)

Posted

技术标签:

【中文标题】NestJS:通过请求(子域)连接数据库(TypeORM)【英文标题】:NestJS : database connection (TypeORM) by request (subdomain) 【发布时间】:2018-12-25 09:54:32 【问题描述】:

我正在尝试通过 Nest/TypeORM 构建 SAAS 产品,我需要按子域配置/更改数据库连接。

customer1.domain.com => connect to customer1 database
customer2.domain.com => connect to customer2 database
x.domain.com => connect to x database

我该怎么做?使用拦截器或请求上下文(或 Zone.js)?

我不知道如何开始。有人已经这样做了吗?


WIP:我目前在做什么:

    将所有连接设置添加到 ormconfig 文件中

    在所有路由上创建中间件以将子域注入res.locals(实例名称)并创建/警告 typeorm 连接

    import  Injectable, NestMiddleware, MiddlewareFunction  from '@nestjs/common';
    import  getConnection, createConnection  from "typeorm";
    
    @Injectable()
    export class DatabaseMiddleware implements NestMiddleware 
        resolve(): MiddlewareFunction 
          return async (req, res, next) => 
              const instance = req.headers.host.split('.')[0]
              res.locals.instance = instance
    
              try 
                  getConnection(instance)
               catch (error) 
                  await createConnection(instance)
              
    
              next();
          ;
        
    
    

    在 Controller 中:从 @Response 获取实例名称并将其传递给我的服务

    @Controller('/catalog/categories')
    export class CategoryController 
        constructor(private categoryService: CategoryService) 
    
        @Get()
        async getList(@Query() query: SearchCategoryDto, @Response() response): Promise<Category[]> 
          return response.send(
            await this.categoryService.findAll(response.locals.instance, query)
          )
        
    

    在服务中:获取给定实例的 TypeORM 管理器并通过存储库查询数据库

    @Injectable()
    export class CategoryService 
      // constructor(
      //   @InjectRepository(Category) private readonly categoryRepository: Repository<Category>
      // ) 
    
      async getRepository(instance: string): Promise<Repository<Category>> 
          return (await getManager(instance)).getRepository(Category)
      
    
      async findAll(instance: string, dto: SearchCategoryDto): Promise<Category[]> 
        let queryBuilder = (await this.getRepository(instance)).createQueryBuilder('category')
    
        if (dto.name) 
            queryBuilder.andWhere("category.name like :name",  name: `%$dto.name%` )
        
    
        return await queryBuilder.getMany();
      
    

它似乎有效,但我几乎不确定所有事情:

连接池(我可以在我的 ConnectionManager 中创建多少连接?) 将子域传递到 response.locals...不好的做法? 可读性/理解性/添加大量额外代码... 副作用:我害怕在几个子域之间共享连接 副作用:性能

处理 response.send() + Promise + await(s) + 到处传递子​​域并不令人愉快...

有没有办法让子域直接进入我的服务?

有没有办法让正确的子域连接/存储库直接进入我的服务并将其注入我的控制器?

【问题讨论】:

您应该使用环境变量来配置它,在您启动节点服务器之前定义,例如:DOMAIN=customer1.domain.com node server.js(如果您使用的是 linux)。要在您的代码中使用 process.env.DOMAIN 这意味着我需要通过 sudbomain 运行一个节点(子域 1 个应用程序/端口)...我想为所有子域运行一个节点并根据请求切换数据库连接。 【参考方案1】:

最好的方法是使用动态模块,就像您对请求范围所做的那样来获取ORM 连接并使其特定于连接。 一个非常简单的示例可能是这样的:

const tenancyFactory: Provider = 
    provide: NEST_mysql2_TENANCY,
    scope: 'REQUEST',
    useFactory: async (mysql: Mysql, options: MysqlTenancyOption, req: Request): Promise<any> => 
        console.log("TENANCY FACTORY");

        const executer = function (mysqlPool: Mysql): MysqlExecuter 
            return 
                db: function (dbName: string): MysqlRunner 
                    return 
                        run: async function (sqlString: string) 
                            const q = `\nUSE $dbName;\n` +
                                sqlString.replace("; ", ";\n");
                            if (options.debug) 
                                tLogger.verbose(q);
                            
                            const [[_, ...queryResult], __] = await mysqlPool.query(q)
                            return queryResult as any;
                        
                    
                
            
        
        return executer(mysql);
    ,
    inject: [NEST_MYSQL2_CONNECTION, NEST_MYSQL2_TENANCY_OPTION],
;

@Global()
@Module(
    providers: [tenancyFactory],
    exports: [tenancyFactory],
)
export class MultiTenancyModule 
    constructor(

    )  
    public static register(options: MysqlTenancyOption): DynamicModule 
        return 
            module: MultiTenancyModule,
            providers: [
                provide: NEST_MYSQL2_TENANCY_OPTION,
                useValue: options
            ]
        ;
    

在这个示例中,我有用户 mysql2-nestjs 模块,但您可以使用自己的 ORM 创建 tenancyFactory

您可以在以下 ling 的工作解决方案中找到此示例 https://github.com/golkhandani/multi-tenancy/blob/main/test/src/tenancy.module.ts

【讨论】:

【参考方案2】:

您应该使用具有REQUEST 范围的自定义提供程序。

租赁提供商

import  Global, Module, Scope  from '@nestjs/common';
import  REQUEST  from '@nestjs/core';
import  Connection, createConnection, getConnectionManager  from 'typeorm';

const connectionFactory = 
  provide: 'CONNECTION',
  scope: Scope.REQUEST,
  useFactory: async (req) => 
    const instance = req.headers.host.split('.')[0]
    if (instance) 
      const connectionManager = getConnectionManager();

      if (connectionManager.has(instance)) 
        const connection = connectionManager.get(instance);
        return Promise.resolve(connection.isConnected ? connection : connection.connect());
      

      return createConnection(
        ...tenantsOrmconfig,
        entities: [...(tenantsOrmconfig as any).entities, ...(ormconfig as any).entities],
        name: instance,
        type: 'postgres',
        schema: instance
      );
    
  ,
  inject: [REQUEST]
;

@Global()
@Module(
  providers: [connectionFactory],
  exports: ['CONNECTION']
)
export class TenancyModule  


服务类

然后在您的服务上,您可以获得这样的连接:

import  Injectable from '@nestjs/common';
import  InjectRepository  from '@nestjs/typeorm';
import  Repository  from 'typeorm';
import  GameEntity  from './game.entity';

@Injectable()
export class MyService 
  constructor(
    @Inject('CONNECTION') connection
  ) 
    this.myRepository = connection.getRepository(GameEntity);
  

  findAll(): Promise<GameEntity[]> 
    return this.myRepository.find();
  




您可以在以下多租户文章中获取更多信息:https://tech.canyonlegal.com/multitenancy-with-nestjs-typeorm-postgres

【讨论】:

上述文章现已移至此链接:thomasvds.com/…,同时带来了一些重构(基于 @adrien_om 的原始文章,以及相关的 Github 存储库【参考方案3】:

我受到了 yoh 的解决方案的启发,但我根据 NestJS 中的新功能对其进行了一些调整。结果是更少的代码。

1) 我创建了DatabaseMiddleware

import  Injectable, NestMiddleware, Inject  from '@nestjs/common';
import  getConnection, createConnection, ConnectionOptions  from "typeorm";

@Injectable()
export class DatabaseMiddleware implements NestMiddleware 

  public static COMPANY_NAME = 'company_name';

  async use(req: any, res: any, next: () => void) 
    const databaseName = req.headers[DatabaseMiddleware.COMPANY_NAME];

    const connection: ConnectionOptions = 
      type: "mysql",
      host: "localhost",
      port: 3307,
      username: "***",
      password: "***",
      database: databaseName,
      name: databaseName,
      entities: [
        "dist/**/*.entity.ts,.js",
        "src/**/*.entity.ts,.js"
      ],
      synchronize: false
    ;

    try 
      getConnection(connection.name);
     catch (error) 
      await createConnection(connection);
    

    next();
  


2) 在 main.ts 中为每条路由使用它

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

  app.use(new DatabaseMiddleware().use);
  ...

3) 在服务中检索连接

import  Injectable, Inject  from '@nestjs/common';
import  Repository, getManager  from 'typeorm';
import  MyEntity  from './my-entity.entity';
import  REQUEST  from '@nestjs/core';
import  DatabaseMiddleware  from '../connections';

@Injectable()
export class MyService 
  private repository: Repository<MyEntity>;

  constructor(@Inject(REQUEST) private readonly request)  
    this.repository = getManager(this.request.headers[DatabaseMiddleware.COMPANY_NAME]).getRepository(MyEntity);
  

  async findOne(): Promise<MyEntity> 
    return await this.repository
    ...
  


【讨论】:

如果我应用此解决方案,我会收到错误。[[Nest] 43292 - 2019-10-10 04:19:31 [ExceptionsHandler] 找不到连接“默认”。 +1260ms ConnectionNotFoundError:未找到连接“默认”。] @WinterTime:您需要在 app.module.ts 中设置一个“虚拟”连接,如下所示:@Module( imports: [ TypeOrmModule.forRoot( type: "sqlite", database: ":memory:", entities: entities, dropSchema: true, entities: entities, synchronize: true, logging: false, name: name ), CaseModule, CompanyInfoModule, TeamModule, ], ) 因为连接由每个请求确定,但 TypeORM 需要有一个“默认”开始时的连接。 昨天测试了这个解决方案,听起来不错,但是在嵌套中由于某种原因你无法在中间件中获取 req.body,所以我需要看看是否还有其他可能性。 @WinterTime 你能在 HTTP 标头中发送特定信息吗? 有人告诉我,在构造函数中调用异步方法可能会导致竞争条件或其他错误。你有解决方案吗?【参考方案4】:

我为nest-mongodb 编写了这个问题的实现,请检查一下它可能会有所帮助。

类似问题https://***.com/a/57842819/7377682

import 
    Module,
    Inject,
    Global,
    DynamicModule,
    Provider,
    OnModuleDestroy,
 from '@nestjs/common';
import  ModuleRef  from '@nestjs/core';
import  MongoClient, MongoClientOptions  from 'mongodb';
import 
    DEFAULT_MONGO_CLIENT_OPTIONS,
    MONGO_MODULE_OPTIONS,
    DEFAULT_MONGO_CONTAINER_NAME,
    MONGO_CONTAINER_NAME,
 from './mongo.constants';
import 
    MongoModuleAsyncOptions,
    MongoOptionsFactory,
    MongoModuleOptions,
 from './interfaces';
import  getClientToken, getContainerToken, getDbToken  from './mongo.util';
import * as hash from 'object-hash';

@Global()
@Module()
export class MongoCoreModule implements OnModuleDestroy 
    constructor(
        @Inject(MONGO_CONTAINER_NAME) private readonly containerName: string,
        private readonly moduleRef: ModuleRef,
    ) 

    static forRoot(
        uri: string,
        dbName: string,
        clientOptions: MongoClientOptions = DEFAULT_MONGO_CLIENT_OPTIONS,
        containerName: string = DEFAULT_MONGO_CONTAINER_NAME,
    ): DynamicModule 

        const containerNameProvider = 
            provide: MONGO_CONTAINER_NAME,
            useValue: containerName,
        ;

        const connectionContainerProvider = 
            provide: getContainerToken(containerName),
            useFactory: () => new Map<any, MongoClient>(),
        ;

        const clientProvider = 
            provide: getClientToken(containerName),
            useFactory: async (connections: Map<any, MongoClient>) => 
                const key = hash.sha1(
                    uri: uri,
                    clientOptions: clientOptions,
                );
                if (connections.has(key)) 
                    return connections.get(key);
                
                const client = new MongoClient(uri, clientOptions);
                connections.set(key, client);
                return await client.connect();
            ,
            inject: [getContainerToken(containerName)],
        ;

        const dbProvider = 
            provide: getDbToken(containerName),
            useFactory: (client: MongoClient) => client.db(dbName),
            inject: [getClientToken(containerName)],
        ;

        return 
            module: MongoCoreModule,
            providers: [
                containerNameProvider,
                connectionContainerProvider,
                clientProvider,
                dbProvider,
            ],
            exports: [clientProvider, dbProvider],
        ;
    

    static forRootAsync(options: MongoModuleAsyncOptions): DynamicModule 
        const mongoContainerName =
            options.containerName || DEFAULT_MONGO_CONTAINER_NAME;

        const containerNameProvider = 
            provide: MONGO_CONTAINER_NAME,
            useValue: mongoContainerName,
        ;

        const connectionContainerProvider = 
            provide: getContainerToken(mongoContainerName),
            useFactory: () => new Map<any, MongoClient>(),
        ;

        const clientProvider = 
            provide: getClientToken(mongoContainerName),
            useFactory: async (
                connections: Map<any, MongoClient>,
                mongoModuleOptions: MongoModuleOptions,
            ) => 
                const  uri, clientOptions  = mongoModuleOptions;
                const key = hash.sha1(
                    uri: uri,
                    clientOptions: clientOptions,
                );
                if (connections.has(key)) 
                    return connections.get(key);
                
                const client = new MongoClient(
                    uri,
                    clientOptions || DEFAULT_MONGO_CLIENT_OPTIONS,
                );
                connections.set(key, client);
                return await client.connect();
            ,
            inject: [getContainerToken(mongoContainerName), MONGO_MODULE_OPTIONS],
        ;

        const dbProvider = 
            provide: getDbToken(mongoContainerName),
            useFactory: (
                mongoModuleOptions: MongoModuleOptions,
                client: MongoClient,
            ) => client.db(mongoModuleOptions.dbName),
            inject: [MONGO_MODULE_OPTIONS, getClientToken(mongoContainerName)],
        ;

        const asyncProviders = this.createAsyncProviders(options);

        return 
            module: MongoCoreModule,
            imports: options.imports,
            providers: [
                ...asyncProviders,
                clientProvider,
                dbProvider,
                containerNameProvider,
                connectionContainerProvider,
            ],
            exports: [clientProvider, dbProvider],
        ;
    

    async onModuleDestroy() 
        const clientsMap: Map<any, MongoClient> = this.moduleRef.get<
            Map<any, MongoClient>
        >(getContainerToken(this.containerName));

        if (clientsMap) 
            await Promise.all(
                [...clientsMap.values()].map(connection => connection.close()),
            );
        
    

    private static createAsyncProviders(
        options: MongoModuleAsyncOptions,
    ): Provider[] 
        if (options.useExisting || options.useFactory) 
            return [this.createAsyncOptionsProvider(options)];
         else if (options.useClass) 
            return [
                this.createAsyncOptionsProvider(options),
                
                    provide: options.useClass,
                    useClass: options.useClass,
                ,
            ];
         else 
            return [];
        
    

    private static createAsyncOptionsProvider(
        options: MongoModuleAsyncOptions,
    ): Provider 
        if (options.useFactory) 
            return 
                provide: MONGO_MODULE_OPTIONS,
                useFactory: options.useFactory,
                inject: options.inject || [],
            ;
         else if (options.useExisting) 
            return 
                provide: MONGO_MODULE_OPTIONS,
                useFactory: async (optionsFactory: MongoOptionsFactory) =>
                    await optionsFactory.createMongoOptions(),
                inject: [options.useExisting],
            ;
         else if (options.useClass) 
            return 
                provide: MONGO_MODULE_OPTIONS,
                useFactory: async (optionsFactory: MongoOptionsFactory) =>
                    await optionsFactory.createMongoOptions(),
                inject: [options.useClass],
            ;
         else 
            throw new Error('Invalid MongoModule options');
        
    

【讨论】:

虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review @MihaiChelaru 注意到【参考方案5】:

我想出了另一个解决方案。

我创建了一个中间件来获取特定租户的连接:

import  createConnection, getConnection  from 'typeorm';
import  Tenancy  from '@src/tenancy/entity/tenancy.entity';

export function tenancyConnection(...modules: Array< new(...args: any[]): 
any; >) 

  return async (req, res, next) => 

    const tenant = req.headers.host.split(process.env.DOMAIN)[0].slice(0, -1);

    // main database connection
    let con = ...

    // get db config that is stored in the main db
    const tenancyRepository = await con.getRepository(Tenancy);
    const db_config = await tenancyRepository.findOne( subdomain: tenant );

    let connection;
    try 
       connection = await getConnection(db_config.name);
     catch (e) 
      connection = await createConnection(db_config.config);
    

    // stores connection to selected modules
    for (let module of modules) 
      Reflect.defineMetadata('__tenancyConnection__', connection, module);
    

    next();
  ;

我将它添加到 main.ts 中:

const app = await NestFactory.create(AppModule);
app.use(tenancyConnection(AppModule));

要访问连接,您可以通过以下方式扩展任何服务:

export class TenancyConnection 

  getConnection(): Connection 
    return Reflect.getMetadata('__tenancyConnection__', AppModule);
  

它仍然是一个草稿,但使用此解决方案,您可以在运行时为每个租户添加、删除和编辑连接。 希望对您有所帮助。

【讨论】:

以上是关于NestJS:通过请求(子域)连接数据库(TypeORM)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Nestjs 中的请求范围提供程序动态更改数据库连接?

NestJs连接MySql:错误无法连接数据库

NestJS & TypeORM:发布请求中的 DTO 格式

如何在 Nestjs 中创建 mongodb 连接提供程序

无法在域和子域之间检索数据

Nestjs如何同时使用http请求和Websocket