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 中的请求范围提供程序动态更改数据库连接?