Skip to content

Modulo Tenant

Jose Navarro edited this page Aug 12, 2026 · 1 revision

Módulo tenant

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.

Piezas

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 &lt;esquema&gt; 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

Flujo de negocio

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 &lt;esquema&gt;"]
        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
Loading

Secuencia: resolución de esquema en un request cualquiera

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]
Loading

Notas

  • MigradorEsquemas usa un DataSource propio (no el Hikari de la app) para Flyway — evita que una conexión con search_path contaminado vuelva al pool compartido (ver ADR-001).
  • El reset de search_path a public al liberar cada conexión es la salvaguarda crítica de aislamiento entre tenants en un pool de conexiones compartido.

Clone this wiki locally