问题
I'm trying to build a SAAS product over Nest/TypeORM and I need to configure/change database connection by subdomain.
customer1.domain.com => connect to customer1 database
customer2.domain.com => connect to customer2 database
x.domain.com => connect to x database
How can I do that ? With interceptors or request-context (or Zone.js) ?
I don't know how to start. Is someone already do that ?
WIP : what I am currently doing :
- add all connections settings into ormconfig file
create Middleware on all routes to inject subdomain into
res.locals
(instance name) and create/warn typeorm connectionimport { 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(); }; } }
in Controller : get instance name from @Response and pass it to my Service
@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) ) }
in Service : get TypeORM Manager for given instance and query database through Repository
@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(); }
It seems to work but I not sure about pretty much everything :
- connections poole (how many can I create connections into my ConnectionManager ?)
- pass subdomain into response.locals... bad practice ?
- readability / comprehension / adding lot of additional code...
- side effects : I'm afraid to share connections between several subdomains
- side effects : performance
It's not a pleasure to deals with response.send() + Promise + await(s) + pass subdomain everywhere...
Is there a way to get subdomain directly into my Service ?
Is there a way to get correct subdomain Connection/Repository directly into my Service and Inject it into my Controller ?
回答1:
I came up with another solution.
I created a middleware to get the connection for a specific tenant:
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();
};
}
I added it to the main.ts:
const app = await NestFactory.create(AppModule);
app.use(tenancyConnection(AppModule));
To access the connection you can extend any service by:
export class TenancyConnection {
getConnection(): Connection {
return Reflect.getMetadata('__tenancyConnection__', AppModule);
}
}
It is still a draft, but with this solution you can add, delete and edit the connection for each tenant at runtime. I hope this helps you further.
回答2:
I got inspired by yoh's solution but I tweaked it a bit according to the new features in NestJS. The result is less code.
1) I created 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) in main.ts use it for every routes
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(new DatabaseMiddleware().use);
...
3) In service retrieve connection
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
...
}
}
回答3:
I write an implementation for this issue for nest-mongodb, please check it out it might help.
Similar question 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');
}
}
}
来源:https://stackoverflow.com/questions/51385738/nestjs-database-connection-typeorm-by-request-subdomain