Tenant-kohtaiset tietokantayhteydet
Vaihtakaa tietokantaskeemaa tai yhteyttä dynaamisesti selvitetyn tenantin perusteella.
Tenant-kohtaiset tietokantayhteydet on ilmainen NestJS-yritysbackendien API:t-oppitunti CoddyKitissä. Tämä on oppitunti 2/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu NestJS-yritysbackendien API:t-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.
Schema per tenant: kokonaiskuva
Monen tenantin API:ssa jokaisen tenantin tietojen on pysyttävä erillään. Schema-per-tenant-mallissa käytetään yhtä fyysistä tietokantaa, mutta jokaiselle tenantille annetaan oma PostgreSQL-schema (esim. tenant_acme, tenant_globex). Tauluilla on sama rakenne kaikissa schemoissa.
- Taulujoukko (jaettu schema): yksi taulujoukko, eristys
tenant_id-sarakkeen avulla. Yksinkertainen, mutta tietovuoto on yhden puuttuvan WHERE-ehdon päässä. - Schema per tenant: vahvempi eristys ja helppo tenant-kohtainen varmuuskopiointi, mutta aktiivinen schema on vaihdettava jokaisen pyynnön yhteydessä.
- Tietokanta per tenant: paras eristys, mutta suurimmat operatiiviset kustannukset.
Tässä oppitunnissa keskitytään jokaisen pyynnön dynaamiseen reitittämiseen oikeaan schemaan tai yhteyteen sen jälkeen, kun tenant on ratkaistu.
Tenantin ratkaiseminen pyyntökohtaisesti
Ennen kuin voitte vaihtaa schemaa, tarvitsette tenantin. Se ratkaistaan yleensä aliverkkotunnuksesta, otsakkeesta tai JWT-claimista. Kevyt middleware poimii tenantin ja liittää sen pyyntöön, jotta myöhemmät providerit voivat lukea sen.
Pidä tässä vaiheessa ratkaiseminen yksinkertaisena ja edullisena; tenantin olemassaolon tarkistus tehdään yhteyttä muodostettaessa.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
// Prefer an explicit header; fall back to subdomain.
const headerTenant = req.headers['x-tenant-id'] as string | undefined;
const host = req.headers.host ?? '';
const subdomain = host.split('.')[0];
const tenantId = headerTenant ?? subdomain;
(req as any).tenantId = tenantId;
next();
}
}Miksi REQUEST-scope sopii tähän luontevasti
Aktiivinen schema vaihtuu jokaisen pyynnön yhteydessä. NestJS-providerit ovat oletusarvoisesti singleton-instansseja, joten singleton ei voi turvallisesti säilyttää pyyntökohtaista schemaa. Siisti ratkaisu on request-scoped provider, joka vastaanottaa nykyisen REQUEST-objektin.
Scope.REQUESTluo uuden provider-instanssin jokaista saapuvaa pyyntöä varten.- Myös providerista, joka injektoi request-scoped-providerin, tulee request-scoped (scope kulkeutuu ketjussa ylöspäin).
- Kääntöpuolena on pyyntökohtaisen instanssin luonnin kustannus, joten pidä request-scoped-providerit ohuina ja cacheta raskaat osat (yhteydet) niiden ulkopuolelle.
Scheman vaihtaminen SET search_path -komennolla
Yksinkertaisin tapa vaihtaa schemaa yhdellä Postgres-yhteydellä on SET search_path. Se kertoo Postgresille, mistä schemasta kvalifioimattomien taulujen nimet ratkaistaan kyseisen session loppuajan.
Olennainen huomio: yhteyspoolissa yhteys voidaan seuraavaksi antaa toisen tenantin pyynnölle. Vaihto on rajattava kyseiseen työhön ja palautettava ennalleen, tai on käytettävä transaktion paikallista muunnelmaa.
import { DataSource } from 'typeorm';
export async function withTenantSchema<T>(
dataSource: DataSource,
schema: string,
work: () => Promise<T>,
): Promise<T> {
const runner = dataSource.createQueryRunner();
await runner.connect();
try {
// SET LOCAL is transaction-scoped and auto-resets on commit/rollback.
await runner.startTransaction();
await runner.query('SET LOCAL search_path TO $1', [schema]);
const result = await work();
await runner.commitTransaction();
return result;
} catch (err) {
await runner.rollbackTransaction();
throw err;
} finally {
await runner.release();
}
}Scheman nimen puhdistaminen
Scheman nimet ovat tunnisteita, eivät arvoja. Tunnistetta ei voi turvallisesti parametroida SET search_path -komennossa samalla tavalla kuin dataa. Haitallinen tai virheellinen tenant-id voisi johtaa SQL-injektioon.
Validoi ratkaistu schema aina tiukkaa sallittujen arvojen kaavaa vasten ja mielellään myös tunnettujen tenantien rekisteriä vasten ennen sen interpolointia.
const SCHEMA_PATTERN = /^[a-z][a-z0-9_]{1,62}$/;
export function tenantSchema(tenantId: string): string {
const candidate = `tenant_${tenantId.toLowerCase()}`;
if (!SCHEMA_PATTERN.test(candidate)) {
throw new Error(`Invalid tenant schema: ${candidate}`);
}
return candidate;
}
console.log(tenantSchema('Acme')); // tenant_acme
try {
tenantSchema('acme; DROP SCHEMA x'); // throws
} catch (e) {
console.log((e as Error).message);
}Tenant-kohtaiset yhteydet search_path-komennon sijaan
Vaihtoehto jaetun poolin search_path-arvon muuttamiselle on pitää yllä omaa DataSourcea (ja poolia) kullekin tenant-schemalle. Jokainen DataSource määritetään kerran omalla schemallaan ja käytetään uudelleen.
- Hyöty: schemaa ei tarvitse muuttaa pyyntökohtaisesti eikä poolien ristiinkontaminaation riskiä ole.
- Haitta: yhteyksien määrä kasvaa aktiivisten tenantien määrän mukaan — poolien koot on rajoitettava ja joutilaiden tenantien yhteydet poistettava.
Tässä kohtaa connection manager, joka luo ja cachetaa DataSource-objektit laiskasti, on hyödyllinen.
Tenant-yhteyksien hallinta
Hallintaohjelma vastaa elinkaaresta: se luo DataSourcen ensimmäisellä tenantin kohtaamisella, cachetaa sen ja käyttää sitä myöhemmin uudelleen. Se on singleton — vain haku tehdään pyyntökohtaisesti, ja raskaat yhteydet voidaan jakaa turvallisesti, koska kukin on sidottu omaan schemaansa.
Huomaa, että cachen avain on schema ja että samanaikaiset ensimmäiset haut eivät saa luoda yhteyttä kahdesti (tallenna promise, älä pelkästään ratkaistua arvoa).
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class TenantConnectionManager {
private readonly pools = new Map<string, Promise<DataSource>>();
get(schema: string): Promise<DataSource> {
let pool = this.pools.get(schema);
if (!pool) {
pool = this.build(schema);
this.pools.set(schema, pool); // cache the promise to dedupe races
}
return pool;
}
private async build(schema: string): Promise<DataSource> {
const ds = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
schema,
entities: [__dirname + '/**/*.entity.{ts,js}'],
poolSize: 5,
});
await ds.initialize();
return ds;
}
}Tenantin DataSourcen tarjoaminen providerina
Liitä nyt mukaan request-scoped factory provider, joka lukee tenantin REQUEST-objektista, laskee sen scheman ja pyytää hallintaohjelmalta oikean DataSourcen. Palvelut injektoivat tämän tokenin kiinteän yhteyden sijaan.
Koska factory injektoi REQUEST-objektin, provider on request-scoped — mutta sen kutsuma hallintaohjelma on singleton, joten yhteyksien uudelleenkäyttö säilyy.
import { Scope, Provider } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { DataSource } from 'typeorm';
export const TENANT_DATA_SOURCE = 'TENANT_DATA_SOURCE';
export const tenantDataSourceProvider: Provider = {
provide: TENANT_DATA_SOURCE,
scope: Scope.REQUEST,
inject: [REQUEST, TenantConnectionManager],
useFactory: (req: Request, manager: TenantConnectionManager): Promise<DataSource> => {
const tenantId = (req as any).tenantId as string | undefined;
if (!tenantId) {
throw new Error('No tenant resolved for this request');
}
const schema = tenantSchema(tenantId);
return manager.get(schema);
},
};Tenantin DataSourcen käyttäminen palvelussa
Request-scoped-palvelu injektoi ratkaistun DataSourcen tokenin avulla. Jokainen sen suorittama kysely kohdistuu jo oikeaan schemaan — liiketoimintakoodissa ei tarvita tenant_id-suodatinta eikä manuaalista scheman vaihtoa.
Factory palauttaa arvon Promise<DataSource>, joten käyttäkää await-komentoa ennen repositoryjen avaamista (tai antakaa factoryn odottaa ennen palautusta).
import { Inject, Injectable, Scope } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Invoice } from './invoice.entity';
@Injectable({ scope: Scope.REQUEST })
export class InvoiceService {
constructor(
@Inject(TENANT_DATA_SOURCE) private readonly dataSource: DataSource,
) {}
findAll(): Promise<Invoice[]> {
// Already bound to tenant_<x> schema — no tenant filter needed.
return this.dataSource.getRepository(Invoice).find();
}
}Dynaamiset moduulit konfiguroitavaa tenancyä varten
Uudelleenkäytettävä tenancy-logiikka kuuluu dynaamiseen moduuliin, jotta sovellukset voivat määrittää ratkaisutavan, scheman etuliitteen ja poolin koon forRoot/forRootAsync-metodien avulla. Moduuli exporttaa hallintaohjelman ja request-scoped DataSource -providerin.
Tässä yhdistyvät multi-tenancy ja dynaaminen moduuli: konfiguraatio on staattinen (asetetaan kerran käynnistyksen yhteydessä), kun taas ratkaistu yhteys on dynaaminen (pyyntökohtainen).
import { DynamicModule, Module } from '@nestjs/common';
export interface TenancyOptions {
schemaPrefix: string;
poolSize: number;
}
@Module({})
export class TenancyModule {
static forRoot(options: TenancyOptions): DynamicModule {
return {
module: TenancyModule,
global: true,
providers: [
{ provide: 'TENANCY_OPTIONS', useValue: options },
TenantConnectionManager,
tenantDataSourceProvider,
],
exports: [TenantConnectionManager, TENANT_DATA_SOURCE],
};
}
}Elinkaari, poistaminen ja migraatiot
Tenant-kohtaiset poolit ovat resurssivuoto, joka odottaa toteutumistaan. Hallitse niitä suunnitelmallisesti:
- Rajoita samanaikaisuutta: käytä pientä
poolSize-arvoa tenantia kohden; suuri tenant-määrä yhdistettynä suuriin pooleihin kuluttaa Postgresinmax_connections-rajan loppuun. - Poista joutilaat tenantit: seuraa viimeisintä käyttöaikaa ja kutsu
destroy()niiden DataSource-objekteille, jotka jäävät käyttämättömiksi (LRU pitää muistin käytön rajattuna). - Sammuta siististi: toteuta
OnModuleDestroysulkemaan kaikki cachetatut DataSource-objektit. - Migraatiot: uusi tenant tarkoittaa
CREATE SCHEMA-komennon suorittamista ja migraatioiden ajamista sitä vasten ennen ensimmäistä käyttöä; aja migraatiot käyttöönottovaiheessa kaikkien tenant-schemojen läpi.
Käsittele scheman valmistelu eksplisiittisenä onboarding-vaiheena, ei ensimmäisen kyselyn sattumanvaraisena seurauksena.
Pikatarkistus: tenantien välisten vuotojen välttäminen
Vaihdat schemaa SET search_path -komennolla jaetusta TypeORM-poolista lainatuissa yhteyksissä. Toisinaan tenant A näkee tenant B:n rivejä. Mikä on todennäköisin juurisyy ja oikea korjaus?
Kertaus: dynaaminen schemareititys
Opitte reitittämään jokaisen pyynnön oikeaan tenant-schemaan:
- Ratkaise tenant aikaisin (otsakkeesta, aliverkkotunnuksesta tai JWT:stä) middlewaressa ja liitä se pyyntöön.
- Vaihda schemaa joko käyttämällä jaetussa poolissa transaktion laajuista
SET LOCAL search_path-komentoa tai singleton-hallintaohjelmaan cachetattua DataSourcea tenantia kohden. - Liitä käyttöön request-scoped factory provider, joka lukee
REQUEST-objektin, validoi scheman nimen ja palauttaa oikean DataSourcen — palvelut pysyvät tenantista riippumattomina. - Määritä kokonaisuus dynaamisen moduulin (
forRoot) avulla niin, että konfiguraatio pysyy staattisena ja yhteys dynaamisena. - Ylläpidä kokonaisuutta turvallisesti: validoi tunnisteet, rajoita ja poista pooleja, sulje ne sammutuksen yhteydessä ja valmistele sekä migroi schemat eksplisiittisenä onboarding-vaiheena.
Opi TypeScript tekoälytuutorin avulla — ilmaiseksi
Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.
- Kurssit
- 20
- Oppitunnit
- 76
Usein kysytyt kysymykset
Onko oppitunti ”Tenant-kohtaiset tietokantayhteydet” ilmainen?
Kyllä – oppitunnin ”Tenant-kohtaiset tietokantayhteydet” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko NestJS-yritysbackendien API:t-kurssin, päivitä CoddyKit PROhon. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.
Mitä opin oppitunnilla ”Tenant-kohtaiset tietokantayhteydet”?
Vaihtakaa tietokantaskeemaa tai yhteyttä dynaamisesti selvitetyn tenantin perusteella. Harjoittelet NestJS-yritysbackendien API:t-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.
Tarvitsenko kokemusta aloittaakseni NestJS-yritysbackendien API:t-opiskelun?
Aiempi kokemus ei ole tarpeen. CoddyKitin NestJS-yritysbackendien API:t-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 2/4.
Kuinka kauan ”Tenant-kohtaiset tietokantayhteydet”-oppitunnin suorittaminen kestää?
Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.
Voinko kirjoittaa ja suorittaa koodia tällä NestJS-yritysbackendien API:t-oppitunnilla?
Kyllä. Jokainen NestJS-yritysbackendien API:t-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.
Kaikki tämän kurssin oppitunnit
- Tenantin selvittäminen middlewaren ja AsyncLocalStoragen avulla
- Tenant-kohtaiset tietokantayhteydet
- Määritettävien dynaamisten moduulien rakentaminen
- Pyyntökohtaisten palveluntarjoajien hyödyt ja haitat