【问题标题】:NestJS : database connection (TypeORM) by request (subdomain)NestJS:通过请求(子域)连接数据库(TypeORM)
【发布时间】: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:我目前在做什么:

  1. 将所有连接设置添加到 ormconfig 文件中
  2. 在所有路由上创建中间件以将子域注入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();
          };
        }
    }
    
  3. 在 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)
          )
        }
    
  4. 在服务中:获取给定实例的 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 个应用程序/端口)...我想为所有子域运行一个节点并根据请求切换数据库连接。

标签: typeorm nestjs


【解决方案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://stackoverflow.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);
      }
    }
    

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

    【讨论】:

      猜你喜欢
      • 2020-04-25
      • 2021-10-19
      • 2021-05-26
      • 2021-12-24
      • 2019-05-09
      • 1970-01-01
      • 2022-11-19
      • 2019-08-13
      • 2022-01-21
      相关资源
      最近更新 更多