La lliçó anterior va acabar amb un missatge d'error molt concret: Failed to configure a DataSource. Spring Boot va detectar l'starter de JPA, va intentar construir la unitat de persistència i es va quedar sense el més bàsic, una connexió a una base de dades. Aquesta lliçó resol exactament això i va bastant més enllà d'enganxar quatre propietats: un DataSource mal dimensionat és la causa número u de caigudes d'aplicacions Spring Boot en producció, molt per davant de qualsevol error de lògica.

Muntarem dos entorns per a CicloUrbana: H2 en memòria per desenvolupar de pressa, i PostgreSQL 16 en Docker com a base de dades real. Entendrem què és un pool de connexions i afinarem HikariCP paràmetre a paràmetre amb criteris de dimensionament que es poden defensar davant d'un company. Fixarem les propietats d'Hibernate que governen el comportament de l'ORM, inclosa la decisió —important i poc discutida— de desactivar open-in-view. I deixarem el registre preparat per veure el SQL que realment viatja cap a Ribalta.

Contingut

  1. Què és un DataSource i per què s'agrupa en un pool
  2. H2 en memòria per a desenvolupament
  3. La consola d'H2 i el seu risc de seguretat
  4. PostgreSQL 16 amb Docker Compose
  5. Les propietats essencials de spring.datasource
  6. HikariCP en profunditat
  7. Dimensionar el pool amb criteri
  8. Les propietats de JPA i Hibernate
  9. open-in-view: per què es desactiva
  10. Veure el SQL generat de forma llegible
  11. Múltiples fonts de dades
  12. Credencials fora del repositori
  13. Comprovar la connexió en arrencar
  14. Errors Comuns i Consells
  15. Exercicis

  1. Què és un DataSource i per què s'agrupa en un pool

javax.sql.DataSource és una interfície de Java amb un mètode essencial: getConnection(). És la fàbrica estàndard de connexions a base de dades, i és el que Hibernate demana quan necessita parlar amb PostgreSQL.

La pregunta interessant és què hi ha darrere d'aquest mètode. La implementació ingènua obriria una connexió TCP nova cada vegada. I obrir una connexió a una base de dades és car: negociació TCP, autenticació, negociació TLS, creació d'un procés o fil servidor, reserva de memòria de sessió. En PostgreSQL, entre 20 i 100 mil·lisegons. Si GET /api/v1/estacions triga 5 ms en la seva consulta i 40 ms a obrir la connexió, el 89 % del temps se'n va en lampisteria.

Un pool de connexions resol això mantenint un conjunt de connexions ja obertes i prestant-les:

sequenceDiagram
    participant S as EstacioService
    participant P as Pool HikariCP
    participant BD as PostgreSQL

    Note over P,BD: En arrencar: s'obren N connexions
    S->>P: getConnection()
    P-->>S: connexió #3 (ja oberta, ~0,1 ms)
    S->>BD: SELECT * FROM estacions
    BD-->>S: files
    S->>P: close()
    Note over P: NO es tanca: torna al pool
    P-->>P: connexió #3 disponible

El detall que descol·loca la primera vegada: quan el teu codi crida connection.close(), la connexió no es tanca. El pool retorna un embolcall el close() del qual significa «torna-la al pool». Per això els try-with-resources sobre connexions continuen sent correctes i necessaris.

Conseqüències que cal tenir presents tot el mòdul:

  • El nombre de connexions simultànies està acotat per la mida del pool, no pel nombre de peticions.
  • Si totes estan prestades, la petició següent espera. Si espera massa, falla amb un timeout.
  • Una connexió que es presta i no es torna és una fuita que acaba esgotant el pool i tombant l'aplicació.

Spring Boot inclou HikariCP a través de spring-boot-starter-jdbc, que arrossega l'starter de JPA. No cal afegir res.

  1. H2 en memòria per a desenvolupament

H2 és una base de dades relacional escrita en Java que pot viure dins del propi procés. Per desenvolupar CicloUrbana és ideal: arrenca en mil·lisegons, no requereix instal·lació i es reinicia neta a cada execució.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

L'scope runtime és deliberat: el driver cal en executar, mai en compilar. El teu codi no ha d'importar ni una classe d'H2. Si algun cop necessites compile, és senyal que alguna cosa s'ha acoblat al motor.

La configuració a application.yml:

spring:
  datasource:
    url: jdbc:h2:mem:ciclourbana;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
    username: sa
    password:
    driver-class-name: org.h2.Driver
  h2:
    console:
      enabled: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: update
    open-in-view: false
    show-sql: true
    properties:
      hibernate:
        format_sql: true

Desglossem la URL, que és on hi ha el més interessant:

Fragment Significat
jdbc:h2:mem: Base de dades en memòria: desapareix en acabar el procés
ciclourbana Nom de la base de dades; noms diferents són bases diferents
DB_CLOSE_DELAY=-1 No destruir la BD quan es tanqui l'última connexió
MODE=PostgreSQL Emula la sintaxi i els tipus de PostgreSQL

DB_CLOSE_DELAY=-1 no és opcional. Sense ell, tan bon punt el pool tanca la seva última connexió activa H2 esborra la base de dades sencera, i les dades que va carregar el CarregadorEstacionsDemo desapareixen a mitja execució.

MODE=PostgreSQL és una decisió estratègica del curs: fa que H2 es comporti com PostgreSQL en tipus, funcions i sintaxi, de manera que el que funciona en desenvolupament té moltes més probabilitats de funcionar en producció. No és equivalència total —per això a 06-05 farem servir Testcontainers amb PostgreSQL de debò per a les proves—, però redueix molt la distància.

Si prefereixes que les dades sobrevisquin entre arrencades, H2 també pot escriure en fitxer amb jdbc:h2:file:./dades/ciclourbana;MODE=PostgreSQL; recorda llavors afegir dades/ al .gitignore.

  1. La consola d'H2 i el seu risc de seguretat

Amb spring.h2.console.enabled: true, en arrencar disposes d'un client SQL web a http://localhost:8080/h2-console. Les dades de connexió que cal introduir són les mateixes del YAML:

JDBC URL:  jdbc:h2:mem:ciclourbana
User Name: sa
Password:  (buit)

És utilíssim per veure quines taules ha creat Hibernate a partir de les teves entitats (04-03) i comprovar que les dades són on et penses.

I és, alhora, el forat de seguretat més gran que pots deixar obert. La consola d'H2 permet executar SQL arbitrari sense autenticació real. Pitjor encara: H2 permet executar codi Java des de SQL mitjançant àlies, cosa que converteix una consola exposada en execució remota de codi. Hi ha hagut CVE greus justament per això.

Les regles són innegociables:

  • Mai habilitar la consola en un entorn accessible des de fora.
  • Activar-la només al perfil de desenvolupament (els perfils es veuen a 07-02):
# application-dev.yml
spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: false   # només localhost
  • A application.yml (base), deixar-la desactivada: enabled: false.
  • Quan afegim Spring Security al mòdul 5, la consola necessitarà una regla explícita i, tot i així, continuarà restringida a desenvolupament.

web-allow-others: false és el valor per defecte i limita l'accés a localhost. No el canviïs.

  1. PostgreSQL 16 amb Docker Compose

H2 serveix per desenvolupar, però CicloUrbana funcionarà en producció sobre PostgreSQL 16. Aixecar-lo amb Docker evita instal·lar res a la màquina i garanteix que tot l'equip fa servir la mateixa versió. Crea docker-compose.yml a l'arrel del projecte:

services:
  postgres:
    image: postgres:16-alpine
    container_name: ciclourbana-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: ciclourbana
      POSTGRES_USER: ciclourbana
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ciclourbana_dev}
      TZ: Europe/Madrid
    ports:
      - "5432:5432"
    volumes:
      - postgres-dades:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ciclourbana -d ciclourbana"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres-dades:

Punt per punt:

Element Per què hi és
postgres:16-alpine Versió fixada; alpine redueix la imatge a ~80 MB
restart: unless-stopped Torna a aixecar-se després de reiniciar l'equip
POSTGRES_DB/USER/PASSWORD A la primera arrencada creen base de dades i usuari
${POSTGRES_PASSWORD:-...} Pren la variable d'entorn; si no existeix, fa servir el valor de desenvolupament
ports: 5432:5432 Exposa el port a l'amfitrió per connectar-s'hi des de l'IDE
volumes: postgres-dades Volum amb nom: les dades sobreviuen a docker compose down
healthcheck Permet saber quan està realment llest, no només arrencat

Comandes habituals:

docker compose up -d                 # aixecar en segon pla
docker compose ps                    # veure estat i salut
docker compose logs -f postgres      # seguir el registre
docker compose exec postgres psql -U ciclourbana -d ciclourbana
docker compose down                  # aturar (les dades es mantenen)
docker compose down -v               # aturar I ESBORRAR el volum

Compte amb down -v: esborra el volum i amb ell totes les dades. A 07-04 reprendrem aquest fitxer per afegir el contenidor de la mateixa aplicació.

El driver de PostgreSQL:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

I la configuració corresponent:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/ciclourbana
    username: ciclourbana
    password: ${POSTGRES_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false

Pots tenir tots dos drivers al pom.xml simultàniament: el que es faci servir el decideix la URL. A 07-02 veurem com separar netament les dues configuracions en perfils.

  1. Les propietats essencials de spring.datasource

Propietat Què és Exemple És obligatòria?
url Cadena JDBC completa jdbc:postgresql://localhost:5432/ciclourbana Sí (llevat de BD incrustada)
username Usuari de base de dades ciclourbana Gairebé sempre
password Contrasenya ${POSTGRES_PASSWORD} Gairebé sempre
driver-class-name Classe del driver JDBC org.postgresql.Driver No, es dedueix
name Nom del DataSource ciclourbana-ds No

Per què no cal el driver-class-name. Spring Boot fa servir DatabaseDriver, un enumerat que associa prefixos d'URL amb classes de driver: jdbc:postgresql: → org.postgresql.Driver, jdbc:h2: → org.h2.Driver, jdbc:mysql: → com.mysql.cj.jdbc.Driver. Amb la URL i el driver al classpath, la deducció és automàtica. Només cal declarar-lo quan fas servir un driver alternatiu o una URL amb un prefix no reconegut.

I si no configures res tenint H2 al classpath, Spring Boot crea un DataSource incrustat amb una URL aleatòria de l'estil jdbc:h2:mem:2a5f.... És còmode per a una arrencada ràpida, però com que el nom canvia a cada execució no pots connectar-t'hi amb la consola. Per a CicloUrbana el declarem sempre explícitament.

  1. HikariCP en profunditat

HikariCP és el pool per defecte de Spring Boot des de la versió 2.0, i amb raó: és el més ràpid de l'ecosistema Java i el que menys configuració necessita per estar bé. La seva filosofia és tenir poques opcions i valors per defecte assenyats.

Tots els seus paràmetres van sota spring.datasource.hikari:

spring:
  datasource:
    hikari:
      pool-name: CicloUrbanaPool
      maximum-pool-size: 10
      minimum-idle: 10
      connection-timeout: 30000       # 30 s
      idle-timeout: 600000            # 10 min
      max-lifetime: 1800000           # 30 min
      leak-detection-threshold: 60000 # 60 s
      auto-commit: false
Paràmetre Què controla Per defecte Si et quedes curt Si te'n passes
maximum-pool-size Màxim de connexions simultànies 10 Peticions esperant i timeouts Satures la base de dades i empitjora el rendiment
minimum-idle Connexions ocioses que es mantenen = màxim Latència en crear connexions sota pic Connexions ocioses consumint memòria a la BD
connection-timeout Espera màxima per una connexió 30.000 ms Errades espúries sota càrrega Els fils s'acumulen esperant i l'app es congela
idle-timeout Temps abans de tancar-ne una d'ociosa 600.000 ms S'obren i tanquen connexions sense parar Connexions mortes ocupant lloc
max-lifetime Vida màxima d'una connexió 1.800.000 ms Reciclatge excessiu Connexions caducades pel tallafocs o la BD
leak-detection-threshold Avís si una connexió no es torna 0 (desactivat) Les fuites passen desapercebudes Falsos positius amb processos llargs legítims

Detalls que importen de debò:

max-lifetime ha de ser menor que el temps de vida que imposin la base de dades o el tallafocs. És la causa més comuna d'errors intermitents Connection is closed en producció: un tallafocs talla connexions ocioses als 30 minuts i el pool continua creient-les vàlides. La recomanació oficial és fixar-lo uns quants segons per sota d'aquest límit; 30 minuts és un valor prudent gairebé sempre.

minimum-idle igual a maximum-pool-size és la recomanació dels autors d'HikariCP per a càrregues estables: un pool de mida fixa evita el cost d'obrir connexions justament en el pitjor moment, el del pic de trànsit.

leak-detection-threshold mereix estar activat en desenvolupament i preproducció. Quan una connexió porta prestada més del llindar, HikariCP escriu la traça de pila de qui la va demanar. És la forma més directa de trobar una fuita:

Connection leak detection triggered for org.postgresql.jdbc.PgConnection@3f2a1b,
stack trace follows
  java.lang.Exception: Apparent connection leak detected
    at com.ciclourbana.lloguers.LloguerService.iniciar(LloguerService.java:64)

auto-commit: false deixa la gestió de transaccions a Spring, que és el que volem amb @Transactional (04-07). Spring Boot ja ho ajusta correctament en fer servir JPA.

  1. Dimensionar el pool amb criteri

La intuïció diu «més connexions, més rendiment». És falsa, i entendre-ho diferencia qui configura de qui copia.

Una base de dades executa consultes en un nombre limitat de nuclis i discos. Amb més connexions actives que recursos, el servidor dedica temps a canviar de context i a competir per bloqueigs en lloc de treballar. La documentació d'HikariCP mostra un cas clàssic: un servidor amb 10.000 usuaris rendint millor amb un pool de 10 que amb un de 100.

La fórmula de referència de PostgreSQL:

connexions = ((nuclis_cpu * 2) + fusos_de_disc_efectius)

Per a un PostgreSQL en un contenidor amb 4 vCPU i emmagatzematge SSD (on el terme de discos ronda 1-2):

connexions ≈ (4 * 2) + 2 = 10

Deu. Aquest és el valor per defecte d'HikariCP i rarament cal apujar-lo. Regles pràctiques per a CicloUrbana:

  • Comença a 10. Només l'apuges amb mètriques que ho justifiquin (Actuator i Micrometer exposen hikaricp.connections.*; ho veurem a 09-03).
  • Suma totes les instàncies. Si desplegues 4 rèpliques amb pool de 10, la base de dades veu 40 connexions. El max_connections de PostgreSQL (100 per defecte) és un sostre global que s'esgota abans del que la gent es pensa.
  • Si hi ha esperes, gairebé mai la solució són més connexions: sol ser una consulta lenta, un índex absent o una transacció massa llarga.
  • Mai facis feina lenta dins d'una transacció. Cridar la passarel·la de pagament amb una connexió prestada reté un recurs escàs durant segons. A CicloUrbana, el cobrament del lloguer ha de passar fora de la transacció; a 04-07 ho resoldrem amb @TransactionalEventListener.

  1. Les propietats de JPA i Hibernate

Sota spring.jpa es configura el comportament de l'ORM.

Propietat Què fa Valor per a CicloUrbana
spring.jpa.hibernate.ddl-auto Gestió automàtica de l'esquema update en dev, validate en prod
spring.jpa.show-sql Imprimeix el SQL per System.out false (millor fer servir el registre)
spring.jpa.properties.hibernate.format_sql Formata el SQL en diverses línies true en desenvolupament
spring.jpa.database-platform Dialecte SQL Es dedueix, no tocar
spring.jpa.open-in-view Manté el context obert a la vista false
spring.jpa.properties.hibernate.jdbc.batch_size Agrupa sentències per lots 20 (útil en càrregues)
spring.jpa.defer-datasource-initialization Endarrereix data.sql fins després de crear l'esquema true si fas servir data.sql

Els cinc valors de ddl-auto, que és la propietat més perillosa del framework:

Valor Què fa Quan fer-lo servir
none Res. Hibernate no toca l'esquema Producció amb Flyway (04-08)
validate Comprova que l'esquema coincideix amb les entitats i falla si no Producció. La millor xarxa de seguretat
update Afegeix taules i columnes que falten. No esborra ni modifica Desenvolupament primerenc, mai producció
create Esborra l'esquema i el crea de zero en arrencar Proves manuals descartables
create-drop Com create, i a més esborra en aturar Proves automàtiques (mòdul 6)

update no és segur en producció, i el motiu se sol malinterpretar. No és només que «pugui esborrar dades» —de fet no esborra columnes—: és que no pot modificar el que ja existeix. Si canvies un varchar(80) a varchar(40), o afegeixes una columna NOT NULL a una taula amb files, update ho ignora en silenci o falla a mitges, deixant l'esquema en un estat que ningú no ha revisat ni pot reproduir. A més no hi ha registre de què es va aplicar ni forma de desfer-ho.

El pla del mòdul és explícit: fem servir update mentre dissenyem les entitats a 04-03 i 04-04, i a 04-08 el substituïm per Flyway amb validate. Quan arribis a aquella lliçó, update desapareix del projecte per sempre.

El dialecte no es configura. Hibernate 6 el detecta interrogant el driver i ajusta la generació de SQL a la versió concreta del motor. Fixar-lo a mà només serveix per quedar-se en una versió antiga sense adonar-se'n.

  1. open-in-view: per què es desactiva

Spring Boot activa per defecte spring.jpa.open-in-view=true, i n'avisa amb un missatge al registre:

spring.jpa.open-in-view is enabled by default. Therefore, database queries may be
performed during view rendering. Explicitly configure spring.jpa.open-in-view to
disable this warning

Què fa: un filtre (OpenEntityManagerInViewInterceptor) manté obert el context de persistència durant tota la petició HTTP, no només durant la transacció del servei.

Sona còmode, i aquest és el parany. Els tres problemes que causa:

  1. Amaga el N+1 fins a producció. Si el controlador serialitza una entitat amb una col·lecció mandrosa, amb open-in-view actiu la col·lecció es carrega sense protestar, disparant consultes durant la serialització. Amb ell desactivat, salta LazyInitializationException en desenvolupament, que és on vols assabentar-te'n.
  2. Reté la connexió més temps del necessari. La connexió JDBC pot quedar associada a la petició completa, serialització JSON inclosa. Amb un pool de 10, això redueix dràsticament el nombre de peticions concurrents.
  3. Difumina les fronteres. La capa de presentació acaba executant consultes, just el contrari de la separació de capes que vam construir a 03-05 amb els DTOs.

Per a CicloUrbana la decisió està presa des d'aquesta lliçó:

spring:
  jpa:
    open-in-view: false

La contrapartida és que el servei ha de retornar DTOs completament mapejats, amb tot el que cal ja carregat. És exactament el que ja fem des de 03-05 amb EstacioMapper i EstacioDetallResponse, així que el cost és zero. Hi tornarem a 04-04 i 04-07.

  1. Veure el SQL generat de forma llegible

Treballar amb un ORM sense veure el SQL és programar a cegues. Hi ha dues formes de veure'l i una és clarament millor.

Forma ràpida (evita-la de debò): spring.jpa.show-sql: true. Escriu per System.out, sense format, sense nivell, sense marca de temps i sense passar pel sistema de registres. Serveix per a un cop d'ull i poca cosa més.

Forma correcta: el registre d'Hibernate.

logging:
  level:
    org.hibernate.SQL: DEBUG                  # les sentències
    org.hibernate.orm.jdbc.bind: TRACE        # els valors dels paràmetres
    org.hibernate.stat: DEBUG                 # estadístiques per sessió

spring:
  jpa:
    show-sql: false
    properties:
      hibernate:
        format_sql: true
        highlight_sql: true
        generate_statistics: true

Amb org.hibernate.SQL en DEBUG veuràs cada sentència; amb org.hibernate.orm.jdbc.bind en TRACE, els valors que substitueixen les ?. Ull amb el nom: a Hibernate 5 era org.hibernate.type.descriptor.sql; a Hibernate 6, el que porta Spring Boot 3, és org.hibernate.orm.jdbc.bind. Copiar la configuració antiga és un motiu freqüent de «no em surten els paràmetres».

El resultat a la consola:

Hibernate:
    select
        e1_0.id,
        e1_0.capacitat,
        e1_0.adreca,
        e1_0.nom
    from
        estacions e1_0
    where
        e1_0.capacitat>=?
binding parameter [1] as [INTEGER] - [24]

generate_statistics: true afegeix, al final de cada sessió, un resum valuosíssim per detectar el N+1:

Session Metrics {
    2 JDBC statements, 1 collections fetched, 47 entities loaded
}

Quaranta-set entitats amb dues sentències està bé. Quaranta-set entitats amb quaranta-vuit sentències és un N+1 de manual (04-04).

Advertiment de seguretat: el nivell TRACE dels paràmetres imprimeix al registre tots els valors enviats, correus, contrasenyes xifrades o dades personals dels ciutadans de Ribalta inclosos. És una configuració de desenvolupament. En producció, mai.

  1. Múltiples fonts de dades

Ocasionalment una aplicació necessita parlar amb dues bases de dades: CicloUrbana podria tenir la seva base operativa i una rèplica de només lectura per a informes. En declarar dos DataSource, l'autoconfiguració es desactiva i cal construir-los a mà.

package com.ciclourbana.comu.config;

@Configuration
public class ConfiguracioFontsDades {

    @Bean
    @Primary
    @ConfigurationProperties("ciclourbana.datasource.principal")
    public DataSourceProperties propietatsPrincipal() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    public DataSource dataSourcePrincipal() {
        return propietatsPrincipal()
                .initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
    }

    // El parell de beans per a "informes" és idèntic, sense @Primary i
    // apuntant a ciclourbana.datasource.informes.
}
ciclourbana:
  datasource:
    principal:
      url: jdbc:postgresql://localhost:5432/ciclourbana
      username: ciclourbana
      password: ${POSTGRES_PASSWORD}
    informes:
      url: jdbc:postgresql://replica:5432/ciclourbana
      username: informes
      password: ${INFORMES_PASSWORD}

@Primary (que ja coneixes de 02-02) resol l'ambigüitat: sense ell, qualsevol injecció de DataSource fallaria amb NoUniqueBeanDefinitionException. A més, amb dues fonts cal declarar manualment EntityManagerFactory i TransactionManager per a cadascuna, i separar els repositoris per paquets amb @EnableJpaRepositories(basePackages = ...).

És força feina, i per això la recomanació és clara: no ho facis llevat que sigui imprescindible. Moltes vegades el que es busca (aïllar informes) es resol millor amb vistes materialitzades, memòria cau (09-02) o separant el servei (07-05).

  1. Credencials fora del repositori

La contrasenya de PostgreSQL no pot estar a application.yml, perquè application.yml és a Git i Git té memòria eterna: esborrar-la en un commit posterior no l'elimina de l'historial.

Ja vam veure la precedència de configuració a 02-04; aquí l'apliquem. La forma més portable és la variable d'entorn amb marcador de posició:

spring:
  datasource:
    url: ${CICLOURBANA_DB_URL:jdbc:postgresql://localhost:5432/ciclourbana}
    username: ${CICLOURBANA_DB_USER:ciclourbana}
    password: ${CICLOURBANA_DB_PASSWORD}

Els dos primers porten valor per defecte després de :; el tercer no, deliberadament: si la variable no està definida, l'aplicació falla en arrencar amb un missatge clar en lloc d'intentar connectar-se amb una contrasenya buida.

export CICLOURBANA_DB_PASSWORD='una-contrasenya-llarga-i-unica'
./mvnw spring-boot:run

Recorda a més la traducció automàtica de noms: Spring Boot converteix spring.datasource.password en SPRING_DATASOURCE_PASSWORD, així que definir aquesta variable d'entorn funciona sense escriure res al YAML.

Opció On encaixa Nivell
Variables d'entorn Qualsevol desplegament Bo
Fitxer .env fora de Git Desenvolupament local Acceptable
Secrets de Kubernetes Producció en K8s (08-04) Molt bo
Vault / AWS Secrets Manager Producció amb rotació El millor
Contrasenya a application.yml Cap Inacceptable

I al .gitignore, com a mínim: .env, *.env.local i dades/.

  1. Comprovar la connexió en arrencar

Amb tot configurat, convé una verificació explícita. Un CommandLineRunner com els de 01-05, restringit a desenvolupament:

package com.ciclourbana.comu;

// imports: javax.sql.DataSource, java.sql.Connection, java.sql.DatabaseMetaData,
// org.slf4j.*, org.springframework.boot.CommandLineRunner, org.springframework.stereotype.Component

@Component
public class VerificadorConnexio implements CommandLineRunner {

    private static final Logger log = LoggerFactory.getLogger(VerificadorConnexio.class);

    private final DataSource dataSource;

    public VerificadorConnexio(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void run(String... args) throws Exception {
        try (Connection connexio = dataSource.getConnection()) {
            DatabaseMetaData metadades = connexio.getMetaData();
            log.info("Connexió establerta amb {} {}",
                    metadades.getDatabaseProductName(),
                    metadades.getDatabaseProductVersion());
            log.info("Driver: {} {}",
                    metadades.getDriverName(), metadades.getDriverVersion());
            log.info("URL: {}", metadades.getURL());
        }
    }
}

El try-with-resources és important: sense ell, la connexió no torna al pool i acabes de crear una fuita a la primera línia de codi que toca el DataSource.

Sortida esperada amb PostgreSQL:

INFO  c.c.comu.VerificadorConnexio : Connexió establerta amb PostgreSQL 16.2
INFO  c.c.comu.VerificadorConnexio : Driver: PostgreSQL JDBC Driver 42.7.2
INFO  c.c.comu.VerificadorConnexio : URL: jdbc:postgresql://localhost:5432/ciclourbana

Al mòdul 7 veurem que Actuator ofereix això mateix de forma permanent a /actuator/health, amb un indicador db que executa una consulta de validació.

Errors Comuns i Consells

Deixar ddl-auto: update en desplegar. L'error més car del mòdul. Funciona en desenvolupament, sembla funcionar en producció i un dia deixa l'esquema en un estat que ningú no sap reconstruir. La lliçó 04-08 existeix per eliminar-lo.

Apujar maximum-pool-size per arreglar lentitud. Gairebé sempre empitjora: la base de dades se satura i totes les consultes s'alenteixen. Abans de tocar el pool, mira el SQL, els índexs i la durada de les transaccions.

Deixar open-in-view al seu valor per defecte. L'avís del registre s'ignora sistemàticament. Posa'l a false a la primera línia de configuració JPA del projecte; fer-ho més tard treu a la llum desenes de LazyInitializationException de cop.

Oblidar DB_CLOSE_DELAY=-1 a H2. Produeix el desconcertant «les meves dades desapareixen a mitja execució» sense cap error.

Fer servir org.hibernate.type.descriptor.sql per veure els paràmetres. És el nom d'Hibernate 5. A Spring Boot 3 cal fer servir org.hibernate.orm.jdbc.bind.

Deixar la consola d'H2 accessible. Execució de SQL arbitrari sense autenticació. Només en desenvolupament, només a localhost.

Consell: fixa pool-name. Amb CicloUrbanaPool els missatges del registre i les mètriques són identificables d'un cop d'ull, sobretot si algun dia hi ha dos pools.

Consell: activa leak-detection-threshold en desenvolupament. Seixanta segons és un bon llindar. Trobar una fuita per la seva traça de pila costa minuts; trobar-la en producció per esgotament del pool costa una tarda.

Consell: fixa la versió de la imatge de Docker. postgres:16-alpine, mai postgres:latest. Que l'equip sencer faci servir el mateix motor evita la classe d'errada més difícil de reproduir.

Exercicis

Exercici 1: dimensionar el pool de CicloUrbana

L'ajuntament desplega CicloUrbana en 3 rèpliques. El PostgreSQL té 8 vCPU, SSD i max_connections = 100. Hi ha a més un procés nocturn d'informes que obre fins a 5 connexions i un tauler d'administració amb 5 més.

  1. Calcula el maximum-pool-size per rèplica amb la fórmula de PostgreSQL.
  2. Comprova que el total no esgota max_connections.
  3. Escriu el bloc spring.datasource.hikari complet, justificant cada valor.

Exercici 2: separar desenvolupament i producció sense duplicar configuració

Escriu la configuració de CicloUrbana repartida en application.yml (comuna), application-dev.yml (H2 + consola + registres de SQL) i application-prod.yml (PostgreSQL + credencials per variable d'entorn + validate). Indica quina propietat mai no ha d'aparèixer al fitxer de producció i per què.

Exercici 3: diagnosticar un esgotament del pool

En producció apareix aquest error de forma intermitent en hores punta:

HikariPool-1 - Connection is not available, request timed out after 30001ms.

Enumera quatre causes possibles ordenades de més a menys probable, i per a cadascuna indica com confirmar-la i com corregir-la. Explica per què apujar maximum-pool-size no és la primera resposta.

Solucions

Solució 1.

  1. Fórmula: (nuclis * 2) + fusos_efectius = (8 * 2) + 2 = 18 connexions per a tota la base de dades, no per rèplica. Repartides entre 3 rèpliques: 18 / 3 = 6 per rèplica. Un valor de 6 és defensable; 8 també, deixant marge. El que no és defensable és 20 per rèplica.
  2. Total: 3 rèpliques × 6 = 18, més 5 del procés nocturn i 5 del tauler = 28 connexions. Davant de max_connections = 100 queda moltíssim marge, inclòs el que PostgreSQL reserva per a superusuari i manteniment. Amb 20 per rèplica serien 70, ja incòmode i molt per sobre del que 8 vCPU poden atendre en paral·lel.
  3. Configuració:
spring:
  datasource:
    hikari:
      pool-name: CicloUrbanaPool
      maximum-pool-size: 6         # (8*2+2)/3 rèpliques
      minimum-idle: 6              # pool fix: sense cost d'obertura al pic
      connection-timeout: 3000     # fallar de pressa (3 s) en lloc d'encuar fils
      idle-timeout: 600000         # irrellevant amb minimum-idle = maximum
      max-lifetime: 1800000        # 30 min, per sota del tall del tallafocs
      leak-detection-threshold: 0  # desactivat en producció

El valor més discutible és connection-timeout: 3000. Abaixar-lo dels 30 s per defecte és deliberat: si el pool està esgotat, esperar 30 segons només aconsegueix acumular fils i agreujar el problema. Fallar en 3 segons retorna un 503 ràpid, manté l'aplicació viva i deixa el símptoma visible a les mètriques.

Solució 2.

# application.yml — comú a tots els entorns
spring:
  application:
    name: ciclourbana
  jpa:
    open-in-view: false
    properties:
      hibernate:
        jdbc:
          batch_size: 20
  h2:
    console:
      enabled: false
# application-dev.yml
spring:
  datasource:
    url: jdbc:h2:mem:ciclourbana;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
    username: sa
    password:
    hikari: { pool-name: CicloUrbanaPool, maximum-pool-size: 5, leak-detection-threshold: 60000 }
  h2:
    console: { enabled: true, path: /h2-console }
  jpa:
    hibernate: { ddl-auto: update }
    properties: { hibernate: { format_sql: true } }
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE
# application-prod.yml
spring:
  datasource:
    url: ${CICLOURBANA_DB_URL}
    username: ${CICLOURBANA_DB_USER}
    password: ${CICLOURBANA_DB_PASSWORD}
    hikari:
      pool-name: CicloUrbanaPool
      maximum-pool-size: 6
      minimum-idle: 6
      connection-timeout: 3000
      max-lifetime: 1800000
  jpa:
    hibernate: { ddl-auto: validate }
logging:
  level: { org.hibernate.SQL: WARN }

El que mai no ha d'aparèixer en producció, per ordre de gravetat:

  • org.hibernate.orm.jdbc.bind: TRACE: abocaria al registre totes les dades personals dels ciutadans de Ribalta que passin per una consulta. És una bretxa de privadesa, a més d'un cost de rendiment notable.
  • ddl-auto: update o create: modificacions d'esquema no revisades, o pèrdua total de dades.
  • spring.h2.console.enabled: true: execució de SQL arbitrari sense autenticació.
  • La contrasenya literal: sempre per variable d'entorn, i sense valor per defecte perquè l'errada sigui sorollosa.

Els perfils s'activen amb --spring.profiles.active=prod o SPRING_PROFILES_ACTIVE=prod, i s'estudien a fons a 07-02.

Solució 3. Causes ordenades per probabilitat real:

  1. Transaccions massa llargues (la més probable). Un mètode @Transactional que crida un servei extern —la passarel·la de pagament de CicloUrbana— reté la connexió durant tota la crida de xarxa. Amb 6 connexions i crides de 2 segons, el pool s'esgota amb molt poc trànsit. Confirmar: activar leak-detection-threshold en preproducció i revisar els mètodes @Transactional buscant-hi E/S. Corregir: treure la crida externa fora de la transacció, amb @TransactionalEventListener(AFTER_COMMIT) (04-07).
  2. Fuita de connexions. Algun codi obté una connexió del DataSource sense try-with-resources. És estrany amb Spring Data, però apareix en utilitats escrites a mà. Confirmar: leak-detection-threshold: 60000 i buscar les traces «Apparent connection leak detected». Corregir: tancar sempre amb try-with-resources.
  3. Consultes lentes per manca d'índex. Una consulta que triga 5 segons ocupa la seva connexió 5 segons. Confirmar: pg_stat_statements a PostgreSQL o log_min_duration_statement = 1000. Corregir: índexs i reescriptura de la consulta (09-01).
  4. Problema N+1. Un endpoint que dispara centenars de consultes per petició multiplica el temps de retenció. Confirmar: generate_statistics: true i comptar sentències per sessió. Corregir: JOIN FETCH o @EntityGraph (04-04 i 04-06).

Per què apujar maximum-pool-size no és la primera resposta: l'esgotament és un símptoma, no la malaltia. Si la causa és una transacció de 2 segons, duplicar el pool duplica la càrrega sobre la base de dades sense arreglar res; el problema reapareix amb el doble de trànsit, ara amb el servidor més saturat. Encara més, més connexions actives competint per CPU i bloqueigs alenteixen totes les consultes, incloses les que anaven bé. Apujar el pool és l'última mesura, després d'haver mesurat i descartat les quatre causes anteriors.

Conclusió

CicloUrbana ja té lampisteria. Saps què és un DataSource i per què obrir connexions és car, cosa que justifica que existeixi un pool i que close() no tanqui res. Tens H2 en memòria configurat per a desenvolupament amb DB_CLOSE_DELAY=-1 i MODE=PostgreSQL, la seva consola web disponible només a localhost i l'advertiment de seguretat gravat. Tens un docker-compose.yml amb PostgreSQL 16, volum amb nom, healthcheck i contrasenya per variable d'entorn, a punt per reprendre's a 07-04. Coneixes les quatre propietats essencials de spring.datasource i per què el driver es dedueix sol. Has recorregut HikariCP paràmetre a paràmetre, saps que max-lifetime ha de quedar per sota del tall del tallafocs, que minimum-idle igual al màxim evita el pitjor moment per obrir connexions i que leak-detection-threshold és la forma més ràpida de trobar una fuita. I, sobretot, tens un criteri defensable per dimensionar el pool: (nuclis × 2) + discos, repartit entre rèpliques, amb la certesa que més connexions no signifiquen més rendiment.

Del costat de JPA has fixat les decisions que governen la resta del mòdul: ddl-auto en update només mentre dissenyem les entitats, amb el compromís explícit de substituir-lo per Flyway i validate a 04-08; open-in-view: false des d'ara, acceptant que el servei retorni DTOs complets a canvi que el N+1 i les càrregues mandroses donin la cara en desenvolupament; i el registre d'Hibernate configurat amb org.hibernate.SQL i org.hibernate.orm.jdbc.bind per poder llegir el SQL real amb els seus paràmetres. També saps com es declaren diverses fonts de dades amb @Primary i per què convé evitar-ho, com mantenir les credencials fora de Git i com verificar la connexió en arrencar sense deixar-te una fuita pel camí.

Però la base de dades està buida: no hi ha ni una taula, perquè no hi ha ni una entitat. Estacio continua sent un record immutable en memòria. La lliçó 04-03, Creació d'Entitats JPA, ho canvia: veurem per què un record no pot ser una entitat, convertirem Estacio, Bicicleta i Lloguer en entitats amb @Entity, @Table i índexs, triarem l'estratègia de generació d'identificadors adequada per a PostgreSQL, mapejarem cada tipus de dada amb cura —incloent-hi per què els imports són BigDecimal i mai double—, incrustarem la Ubicacio amb @Embeddable, afegirem auditoria automàtica i estrenarem @Version, el bloqueig optimista que jubila definitivament el ShallowEtagHeaderFilter de 03-03.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats