サブドメイン(マルチテナント)に基づいて任意のデータベースに接続したいのですが、どうすればよいかわかりません。
アプリの起動時にコードが実行されますが、サブドメインに基づいてデータソースを変更する方法がわかりません。
PS:リクエストごとにミドルウェアを作成しましたが、ソースを変更する方法がわかりません。
DB用に次のコードがあります。
import { connect, createConnection } from 'mongoose';
import { SERVER_CONFIG, DB_CONNECTION_TOKEN } from '../server.constants';
const opts = {
useCreateIndex: true,
useNewUrlParser: true,
keepAlive: true,
socketTimeoutMS: 30000,
poolSize: 100,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 500,
autoReconnect: true,
};
export const databaseProviders = [
{
provide: DB_CONNECTION_TOKEN,
useFactory: async () => {
try {
console.log(`Connecting to ${ SERVER_CONFIG.db }`); return await createConnection(`${SERVER_CONFIG.db}`, opts);
} catch (ex) {
console.log(ex);
}
},
}
];
サブドメイン(マルチテナント)に基づいて、リクエストごとにデータソースを変更したい
これが私がマングースで使用した解決策です
TenantsService
アプリケーション内のすべてのテナントを管理するために使用されます@Injectable()
export class TenantsService {
constructor(
@InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
) {}
/**
* Save tenant data
*
* @param {CreateTenantDto} createTenant
* @returns {Promise<ITenant>}
* @memberof TenantsService
*/
async create(createTenant: CreateTenantDto): Promise<ITenant> {
try {
const dataToPersist = new this.tenantModel(createTenant);
// Persist the data
return await dataToPersist.save();
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/**
* Find details of a tenant by name
*
* @param {string} name
* @returns {Promise<ITenant>}
* @memberof TenantsService
*/
async findByName(name: string): Promise<ITenant> {
return await this.tenantModel.findOne({ name });
}
}
TenantAwareMiddleware
tenant id
リクエストコンテキストから取得するミドルウェア。ここで独自のロジックを作成してtenant id
、リクエストヘッダーまたはリクエストURLサブドメインからを抽出できます。リクエストヘッダーの抽出方法を次に示します。サブドメインを抽出したい場合は、をRequest
呼び出してオブジェクトからサブドメインを抽出することで同じことができますreq.subdomains
。これにより、サブドメインのリストが表示され、そこから探しているものを取得できます。
@Injectable()
export class TenantAwareMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Extract from the request object
const { subdomains, headers } = req;
// Get the tenant id from header
const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];
if (!tenantId) {
throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
}
// Set the tenant id in the header
req['tenantId'] = tenantId.toString();
next();
}
}
TenantConnection
このクラスは、を使用して新しい接続を作成するために使用されtenant id
、使用可能な既存の接続がある場合は、同じ接続を返します(追加の接続の作成を回避するため)。@Injectable()
export class TenantConnection {
private _tenantId: string;
constructor(
private tenantService: TenantsService,
private configService: ConfigService,
) {}
/**
* Set the context of the tenant
*
* @memberof TenantConnection
*/
set tenantId(tenantId: string) {
this._tenantId = tenantId;
}
/**
* Get the connection details
*
* @param {ITenant} tenant
* @returns
* @memberof TenantConnection
*/
async getConnection(): Connection {
// Get the tenant details from the database
const tenant = await this.tenantService.findByName(this._tenantId);
// Validation check if tenant exist
if (!tenant) {
throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
}
// Get the underlying mongoose connections
const connections: Connection[] = mongoose.connections;
// Find existing connection
const foundConn = connections.find((con: Connection) => {
return con.name === `tenantDB_${tenant.name}`; }); // Check if connection exist and is ready to execute if (foundConn && foundConn.readyState === 1) { return foundConn; } // Create a new connection return await this.createConnection(tenant); } /** * Create new connection * * @private * @param {ITenant} tenant * @returns {Connection} * @memberof TenantConnection */ private async createConnection(tenant: ITenant): Promise<Connection> { // Create or Return a mongo connection return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
}
}
TenantConnectionFactory
これはtenant id
、接続の作成を支援するカスタムプロバイダーです。// Tenant creation factory
export const TenantConnectionFactory = [
{
provide: 'TENANT_CONTEXT',
scope: Scope.REQUEST,
inject: [REQUEST],
useFactory: (req: Request): ITenantContext => {
const { tenantId } = req as any;
return new TenantContext(tenantId);
},
},
{
provide: 'TENANT_CONNECTION',
useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose> => {
// Set tenant context
connection.tenantId = context.tenantId;
// Return the connection
return connection.getConnection();
},
inject: ['TENANT_CONTEXT', TenantConnection],
},
];
TenantsModule
-ここでTenantConnectionFactory
は、プロバイダーとして追加され、他のモジュール内で使用するためにエクスポートされていることがわかります。@Module({
imports: [
CoreModule,
],
controllers: [TenantsController],
providers: [
TenantsService,
TenantConnection,
...TenantConnectionFactory,
],
exports: [
...TenantConnectionFactory,
],
})
export class TenantsModule {}
TenantModelProviders
-テナントモデルはテナント接続に依存するため、モデルはプロバイダーを介して定義し、モデルを初期化するモジュール内に含める必要があります。export const TenantModelProviders = [
{
provide: 'USER_MODEL',
useFactory: (connection: Connection) => connection.model('User', UserSchema),
inject: ['TENANT_CONNECTION'],
},
];
UsersModule
-このクラスはモデルを使用します。ここでは、tenanddbルートに作用するようにミドルウェアが構成されていることも確認できます。この場合、すべてのuser
ルートはテナントの一部であり、テナントデータベースによって提供されます。@Module({
imports: [
CoreModule,
TenantsModule,
],
providers: [
UsersService,
...TenantModelProviders,
],
controllers: [UsersController],
})
export class UsersModule implements NestModule {
configure(context: MiddlewareConsumer) {
context.apply(TenantAwareMiddleware).forRoutes('/users');
}
}
UsersService
-ユーザーモジュールからテナントデータベースにアクセスする実装例@Injectable()
export class UsersService {
constructor(
@Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
@Inject('USER_MODEL') private userModel: Model<IUser>,
) {
Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
}
/**
* Create a new user
*
* @param {CreateUserDto} user
* @returns {Promise<IUser>}
* @memberof UsersService
*/
async create(user: CreateUserDto): Promise<IUser> {
try {
const dataToPersist = new this.userModel(user);
// Persist the data
return await dataToPersist.save();
} catch (error) {
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/**
* Get the list of all users
*
* @returns {Promise<IUser>}
* @memberof UsersService
*/
async findAll(): Promise<IUser> {
return await this.userModel.find({});
}
}
NestJSセットアップ用のマルチテナンシーセットアップもあります。
要求に応じて、使用するデータソースを決定するミドルウェアを使用できます。この例では、NestJSにかなりよく統合されているTypeORMを使用しています。TypeORMパッケージにはいくつかの便利な機能があります。
export class AppModule {
constructor(private readonly connection: Connection) {
}
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(async (req, res, next) => {
try {
getConnection(tenant);
next();
} catch (e) {
const tenantRepository = this.connection.getRepository(tenant);
const tenant = await tenantRepository.findOne({ name: tenant });
if (tenant) {
const createdConnection: Connection = await createConnection(options);
if (createdConnection) {
next();
} else {
throw new CustomNotFoundException(
'Database Connection Error',
'There is a Error with the Database!',
);
}
}
}
}).forRoutes('*');
}
これは私たちのミドルウェアの例です。TypeORMは内部で接続を管理しています。したがって、最初に試みることは、その特定のテナントの接続をロードすることです。ある場合は、それ以外の場合は作成するだけです。ここでの良い点は、一度作成された接続はTypeORM接続マネージャーで引き続き使用できることです。このようにして、ルートには常に接続があります。
ルートでは、テナントのIDが必要です。私たちの場合、それはURLから抽出された単なる文字列です。値が何であれ、ミドルウェア内のリクエストオブジェクトにバインドできます。コントローラで、その値を再度抽出し、サービスに渡します。次に、テナントと準備完了のリポジトリをロードする必要があります。
@Injectable()
export class SampleService {
constructor() {}
async getTenantRepository(tenant: string): Promise<Repository<Entity>> {
try {
const connection: Connection = await getConnection(tenant);
return connection.getRepository(Property);
} catch (e) {
throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
}
}
async findOne(params: Dto, tenant: string) {
const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);
return await propertyRepository.findOne({ where: params });
}
これが、アプリケーションでのサービスの外観です。
うまくいけば、これはあなたに刺激を与え、あなたがあなたの問題に取り組むようになるでしょう:)
特徴的なスターのコリン・エッグレスフィールドは、RomaDrama Liveでのスリル満点のファンとの出会いについて料理しました!加えて、大会での彼のINSPIREプログラム。
ノーザンエクスポージャーが90年代の最も人気のある番組の1つになった理由を確認するには、Blu-rayまたはDVDプレーヤーをほこりで払う必要があります。
ドミニカのボイリング湖は、世界で2番目に大きいボイリング湖です。そこにたどり着くまでのトレッキングは大変で長いですが、努力する価値は十分にあります。
DropMixはNFC対応のカードゲームで、基本的にはリミックスアーティストになります。現在、Amazonでは$ 30まで下がっており、これまでで最高の価格に匹敵します。ロックバンドで有名なHarmonixによって開発されたDropMixは、おそらく少し野心的すぎるように思われます。結局のところ、ほとんどの人は素晴らしいリズムを持っていませんが、ゲームは驚くほどうまく実行されます。
写真:APメアリーJ.ブライジは、間もなくハリウッドウォークオブフェイムのスターを獲得します。これは、メアリーJよりもハリウッドウォークオブフェイムのほうが正直なところ恩恵です。
画像:グラズ工科大学/ NataschaEiblがデザインしたロゴ。MeltdownとSpectreは、攻撃者がシステムメモリに保存されているあらゆる種類の情報にアクセスできるようにする2つの脆弱性に付けられた名前です。
日本人に襲われたときに真珠湾にいた97歳の第二次世界大戦のベテランが、ニューヨークのブルックリンから追い出されています。
Zendaya shared a sweet photo in honor of boyfriend Tom Holland's 26th birthday Wednesday
シーレン「Ms.JuicyBaby」ピアソンは、先月脳卒中で入院した後、「もう一度たくさんのことをする方法を学ばなければならない」ため、言語療法を受けていることを明らかにしました。
オスカー受賞者の世紀半ばの家には、3つのベッドルーム、2つのバス、オーシャンフロントの景色があります。
Bioscoutは、農家を運転席に置くという使命を負っています。Artesian(GrainInnovate)やUniseedと並んで、最新のシードラウンドでチームを支援できることをうれしく思います。問題真菌症による重大な作物の損失は、農民にとって試練であることが証明されています。
遠隔医療は、パンデミック後の時代では新しいものではなく、時代遅れの分野でもありません。しかし、業界を詳しく見ると、需要と供給の強力な持続可能性と、米国で絶え間ない革命となる強力な潜在的成長曲線を示しています。
2021年は、世界的なベンチャーキャピタル(VC)の資金調達にとって記録的な年でした。DealStreetAsiaによると、東南アジアも例外ではなく、この地域では年間で記録的な25の新しいユニコーンが採掘されました。
計算に対する私たちの欲求とムーアの法則が提供できるものとの間には、指数関数的に増大するギャップがあります。私たちの文明は計算に基づいています—建築と想像力の現在の限界を超える技術を見つけなければなりません。