-
Notifications
You must be signed in to change notification settings - Fork 0
Modulo Tenant
Jose Navarro edited this page Aug 12, 2026
·
1 revision
Sin controlador REST propio — es la infraestructura multi-tenant transversal que usan todos los demás módulos. Resuelve, por cada request, a qué esquema de PostgreSQL debe apuntar Hibernate, y arranca/migra los esquemas con Flyway.
| Clase | Responsabilidad |
|---|---|
ContextoEmpresa |
ThreadLocal<String> con el esquema activo + validación de nombre (^[a-z][a-z0-9_]{0,62}$) |
FiltroEsquemaEmpresa |
Filtro de servlet: lee app_metadata.empresa_schema del JWT, fija ContextoEmpresa + MDC (empresa, usuario) |
ConfigMultiEsquema |
Registra los hooks de Hibernate multi-tenant |
ResolutorEsquemaEmpresa |
CurrentTenantIdentifierResolver: ContextoEmpresa.obtener() o esquema por defecto |
ProveedorConexionesPorEsquema |
MultiTenantConnectionProvider: SET search_path TO <esquema> al tomar conexión, reset a public al liberarla |
MigradorEsquemas |
Flyway: migra public + todos los esquemas de empresa al arrancar (@PostConstruct); expone migrarEsquemaEmpresa() reusado por platform
|
flowchart TD
subgraph Arranque["Arranque de la aplicación"]
BOOT["@PostConstruct MigradorEsquemas.migrarAlArrancar()"]
BOOT --> M1["Flyway migra esquema 'public'\n(db/migration/plataforma)"]
M1 --> M2["Por cada empresa en public.empresas:\nFlyway migra su esquema\n(db/migration/tenant)"]
M2 --> M3{"Esquema por defecto existe\nfísicamente pero no registrado?"}
M3 -->|"sí"| WARN["log WARN + lo migra también"]
end
subgraph PorRequest["Por cada request autenticado"]
REQ["Request con JWT válido\n(ya pasó AuthorizationFilter)"] --> READ["FiltroEsquemaEmpresa lee\napp_metadata.empresa_schema"]
READ --> VALID{"esquema válido\n(regex) presente?"}
VALID -->|"no y esquema-estricto=true"| E401["401: 'Tu usuario no tiene empresa asignada'"]
VALID -->|"no, pero ROLE_SUPERADMIN"| SUPERADM["MDC empresa=PLATFORM\nsin fijar ContextoEmpresa\n(superadmin opera sin esquema)"]
VALID -->|"sí"| SET["ContextoEmpresa.establecer(esquema)\n+ MDC empresa/usuario"]
SUPERADM --> CTRL["Controller ejecuta"]
SET --> CTRL
CTRL --> HIB["Hibernate pide conexión →\nResolutorEsquemaEmpresa.resolveCurrentTenantIdentifier()"]
HIB --> CONN["ProveedorConexionesPorEsquema.getConnection()\nSET search_path TO <esquema>"]
CONN --> QUERY["Query ejecuta en el esquema del tenant"]
QUERY --> RELEASE["releaseConnection():\nSET search_path TO public\n(antes de volver al pool Hikari)"]
RELEASE --> FIN["finally del filtro:\nContextoEmpresa.limpiar() + MDC.clear()\n(siempre, incluso con excepción)"]
end
style E401 fill:#f4d4d4
style RELEASE fill:#fde9c8
style FIN fill:#fde9c8
sequenceDiagram
participant C as Cliente
participant F as FiltroEsquemaEmpresa
participant CTX as ContextoEmpresa (ThreadLocal)
participant CTRL as Controller/Service
participant HIB as Hibernate
participant RES as ResolutorEsquemaEmpresa
participant CONN as ProveedorConexionesPorEsquema
participant POOL as Pool Hikari
participant PG as PostgreSQL
C->>F: request con JWT (empresa_schema=acme)
F->>CTX: establecer("acme")
F->>CTRL: continúa cadena
CTRL->>HIB: repository.findAll()
HIB->>RES: resolveCurrentTenantIdentifier()
RES->>CTX: obtener()
CTX-->>RES: "acme"
RES-->>HIB: "acme"
HIB->>CONN: getConnection("acme")
CONN->>POOL: toma conexión del pool
CONN->>PG: SET search_path TO acme
PG-->>CONN: ok
CONN-->>HIB: Connection (search_path=acme)
HIB->>PG: SELECT ... (dentro del esquema acme)
PG-->>CTRL: filas
CTRL-->>C: respuesta
HIB->>CONN: releaseConnection()
CONN->>PG: SET search_path TO public
CONN->>POOL: devuelve conexión (limpia)
F->>CTX: limpiar() [finally, siempre]
-
MigradorEsquemasusa unDataSourcepropio (no el Hikari de la app) para Flyway — evita que una conexión consearch_pathcontaminado vuelva al pool compartido (ver ADR-001). - El reset de
search_pathapublical liberar cada conexión es la salvaguarda crítica de aislamiento entre tenants en un pool de conexiones compartido.
Generada a partir de docs/ en el repo — cualquier cambio de arquitectura real debe actualizar primero el código y CLAUDE.md, y luego reflejarse aquí.