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
- Què és un
DataSourcei per què s'agrupa en un pool - H2 en memòria per a desenvolupament
- La consola d'H2 i el seu risc de seguretat
- PostgreSQL 16 amb Docker Compose
- Les propietats essencials de
spring.datasource - HikariCP en profunditat
- Dimensionar el pool amb criteri
- Les propietats de JPA i Hibernate
open-in-view: per què es desactiva- Veure el SQL generat de forma llegible
- Múltiples fonts de dades
- Credencials fora del repositori
- Comprovar la connexió en arrencar
- Errors Comuns i Consells
- Exercicis
- Què és un
DataSource i per què s'agrupa en un pool
DataSource i per què s'agrupa en un pooljavax.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.
- 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: trueDesglossem 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.
- 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:
É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.
- 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 volumCompte 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: falsePots 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.
- Les propietats essencials de
spring.datasource
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.
- 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.
- 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:
Per a un PostgreSQL en un contenidor amb 4 vCPU i emmagatzematge SSD (on el terme de discos ronda 1-2):
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_connectionsde 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.
- 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.
open-in-view: per què es desactiva
open-in-view: per què es desactivaSpring 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 warningQuè 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:
- Amaga el N+1 fins a producció. Si el controlador serialitza una entitat amb una col·lecció mandrosa, amb
open-in-viewactiu la col·lecció es carrega sense protestar, disparant consultes durant la serialització. Amb ell desactivat, saltaLazyInitializationExceptionen desenvolupament, que és on vols assabentar-te'n. - 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.
- 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çó:
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.
- 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: trueAmb 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:
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.
- 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).
- 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.
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/.
- 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/ciclourbanaAl 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.
- Calcula el
maximum-pool-sizeper rèplica amb la fórmula de PostgreSQL. - Comprova que el total no esgota
max_connections. - Escriu el bloc
spring.datasource.hikaricomplet, 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:
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.
- Fórmula:
(nuclis * 2) + fusos_efectius = (8 * 2) + 2 = 18connexions per a tota la base de dades, no per rèplica. Repartides entre 3 rèpliques:18 / 3 = 6per rèplica. Un valor de 6 és defensable; 8 també, deixant marge. El que no és defensable és 20 per rèplica. - Total:
3 rèpliques × 6 = 18, més 5 del procés nocturn i 5 del tauler = 28 connexions. Davant demax_connections = 100queda 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. - 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: updateocreate: 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:
- Transaccions massa llargues (la més probable). Un mètode
@Transactionalque 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: activarleak-detection-thresholden preproducció i revisar els mètodes@Transactionalbuscant-hi E/S. Corregir: treure la crida externa fora de la transacció, amb@TransactionalEventListener(AFTER_COMMIT)(04-07). - Fuita de connexions. Algun codi obté una connexió del
DataSourcesensetry-with-resources. És estrany amb Spring Data, però apareix en utilitats escrites a mà. Confirmar:leak-detection-threshold: 60000i buscar les traces «Apparent connection leak detected». Corregir: tancar sempre ambtry-with-resources. - Consultes lentes per manca d'índex. Una consulta que triga 5 segons ocupa la seva connexió 5 segons. Confirmar:
pg_stat_statementsa PostgreSQL olog_min_duration_statement = 1000. Corregir: índexs i reescriptura de la consulta (09-01). - Problema N+1. Un endpoint que dispara centenars de consultes per petició multiplica el temps de retenció. Confirmar:
generate_statistics: truei comptar sentències per sessió. Corregir:JOIN FETCHo@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
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
