La lección anterior terminó con un mensaje de error muy concreto: Failed to configure a DataSource. Spring Boot detectó el starter de JPA, intentó construir la unidad de persistencia y se quedó sin lo más básico, una conexión a una base de datos. Esta lección resuelve exactamente eso y va bastante más allá de pegar cuatro propiedades: un DataSource mal dimensionado es la causa número uno de caídas de aplicaciones Spring Boot en producción, muy por delante de cualquier error de lógica.
Vamos a montar dos entornos para CicloUrbana: H2 en memoria para desarrollar rápido, y PostgreSQL 16 en Docker como base de datos real. Entenderemos qué es un pool de conexiones y afinaremos HikariCP parámetro a parámetro con criterios de dimensionado que se pueden defender ante un compañero. Fijaremos las propiedades de Hibernate que gobiernan el comportamiento del ORM, incluida la decisión —importante y poco discutida— de desactivar open-in-view. Y dejaremos el log preparado para ver el SQL que realmente viaja hacia Ribalta.
Contenido
- Qué es un
DataSourcey por qué se agrupa en un pool - H2 en memoria para desarrollo
- La consola de H2 y su riesgo de seguridad
- PostgreSQL 16 con Docker Compose
- Las propiedades esenciales de
spring.datasource - HikariCP en profundidad
- Dimensionar el pool con criterio
- Las propiedades de JPA e Hibernate
open-in-view: por qué se desactiva- Ver el SQL generado de forma legible
- Múltiples fuentes de datos
- Credenciales fuera del repositorio
- Comprobar la conexión al arrancar
- Errores Comunes y Consejos
- Ejercicios
- Qué es un
DataSource y por qué se agrupa en un pool
DataSource y por qué se agrupa en un pooljavax.sql.DataSource es una interfaz de Java con un método esencial: getConnection(). Es la fábrica estándar de conexiones a base de datos, y es lo que Hibernate pide cuando necesita hablar con PostgreSQL.
La pregunta interesante es qué hay detrás de ese método. La implementación ingenua abriría una conexión TCP nueva cada vez. Y abrir una conexión a una base de datos es caro: negociación TCP, autenticación, negociación TLS, creación de un proceso o hilo servidor, reserva de memoria de sesión. En PostgreSQL, entre 20 y 100 milisegundos. Si GET /api/v1/estaciones tarda 5 ms en su consulta y 40 ms en abrir la conexión, el 89 % del tiempo se va en fontanería.
Un pool de conexiones resuelve esto manteniendo un conjunto de conexiones ya abiertas y prestándolas:
sequenceDiagram
participant S as EstacionService
participant P as Pool HikariCP
participant BD as PostgreSQL
Note over P,BD: Al arrancar: se abren N conexiones
S->>P: getConnection()
P-->>S: conexión #3 (ya abierta, ~0,1 ms)
S->>BD: SELECT * FROM estaciones
BD-->>S: filas
S->>P: close()
Note over P: NO se cierra: vuelve al pool
P-->>P: conexión #3 disponible
El detalle que descoloca la primera vez: cuando tu código llama a connection.close(), la conexión no se cierra. El pool devuelve un envoltorio cuyo close() significa «devuélvela al pool». Por eso los try-with-resources sobre conexiones siguen siendo correctos y necesarios.
Consecuencias que hay que tener presentes todo el módulo:
- El número de conexiones simultáneas está acotado por el tamaño del pool, no por el número de peticiones.
- Si todas están prestadas, la siguiente petición espera. Si espera demasiado, falla con un timeout.
- Una conexión que se presta y no se devuelve es una fuga que acaba agotando el pool y tumbando la aplicación.
Spring Boot incluye HikariCP a través de spring-boot-starter-jdbc, que arrastra el starter de JPA. No hay que añadir nada.
- H2 en memoria para desarrollo
H2 es una base de datos relacional escrita en Java que puede vivir dentro del propio proceso. Para desarrollar CicloUrbana es ideal: arranca en milisegundos, no requiere instalación y se reinicia limpia en cada ejecución.
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>El scope runtime es deliberado: el driver hace falta al ejecutar, nunca al compilar. Tu código no debe importar ni una clase de H2. Si alguna vez necesitas compile, es señal de que algo se ha acoplado al motor.
La configuración en 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: trueDesmenucemos la URL, que es donde está lo interesante:
| Fragmento | Significado |
|---|---|
jdbc:h2:mem: |
Base de datos en memoria: desaparece al terminar el proceso |
ciclourbana |
Nombre de la base de datos; distintos nombres son bases distintas |
DB_CLOSE_DELAY=-1 |
No destruir la BD cuando se cierre la última conexión |
MODE=PostgreSQL |
Emula la sintaxis y los tipos de PostgreSQL |
DB_CLOSE_DELAY=-1 no es opcional. Sin él, en cuanto el pool cierra su última conexión activa H2 borra la base de datos entera, y los datos que cargó el CargadorEstacionesDemo desaparecen a mitad de ejecución.
MODE=PostgreSQL es una decisión estratégica del curso: hace que H2 se comporte como PostgreSQL en tipos, funciones y sintaxis, de modo que lo que funciona en desarrollo tiene muchas más probabilidades de funcionar en producción. No es equivalencia total —por eso en 06-05 usaremos Testcontainers con PostgreSQL de verdad para las pruebas—, pero reduce mucho la brecha.
Si prefieres que los datos sobrevivan entre arranques, H2 también puede escribir en fichero con jdbc:h2:file:./datos/ciclourbana;MODE=PostgreSQL; recuerda entonces añadir datos/ al .gitignore.
- La consola de H2 y su riesgo de seguridad
Con spring.h2.console.enabled: true, al arrancar dispones de un cliente SQL web en http://localhost:8080/h2-console. Los datos de conexión que hay que introducir son los mismos del YAML:
Es utilísimo para ver qué tablas ha creado Hibernate a partir de tus entidades (04-03) y comprobar que los datos están donde crees.
Y es, a la vez, el agujero de seguridad más grande que puedes dejar abierto. La consola de H2 permite ejecutar SQL arbitrario sin autenticación real. Peor aún: H2 permite ejecutar código Java desde SQL mediante alias, lo que convierte una consola expuesta en ejecución remota de código. Ha habido CVE graves justamente por esto.
Las reglas son innegociables:
- Nunca habilitar la consola en un entorno accesible desde fuera.
- Activarla solo en el perfil de desarrollo (los perfiles se ven en 07-02):
# application-dev.yml
spring:
h2:
console:
enabled: true
settings:
web-allow-others: false # solo localhost- En
application.yml(base), dejarla desactivada:enabled: false. - Cuando añadamos Spring Security en el módulo 5, la consola necesitará una regla explícita y, aun así, seguirá restringida a desarrollo.
web-allow-others: false es el valor por defecto y limita el acceso a localhost. No lo cambies.
- PostgreSQL 16 con Docker Compose
H2 sirve para desarrollar, pero CicloUrbana funcionará en producción sobre PostgreSQL 16. Levantarlo con Docker evita instalar nada en la máquina y garantiza que todo el equipo usa la misma versión. Crea docker-compose.yml en la raíz del proyecto:
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-datos:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ciclourbana -d ciclourbana"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-datos:Punto por punto:
| Elemento | Por qué está |
|---|---|
postgres:16-alpine |
Versión fijada; alpine reduce la imagen a ~80 MB |
restart: unless-stopped |
Vuelve a levantarse tras reiniciar el equipo |
POSTGRES_DB/USER/PASSWORD |
En el primer arranque crean base de datos y usuario |
${POSTGRES_PASSWORD:-...} |
Toma la variable de entorno; si no existe, usa el valor de desarrollo |
ports: 5432:5432 |
Expone el puerto al host para conectarse desde el IDE |
volumes: postgres-datos |
Volumen con nombre: los datos sobreviven a docker compose down |
healthcheck |
Permite saber cuándo está realmente listo, no solo arrancado |
Comandos habituales:
docker compose up -d # levantar en segundo plano
docker compose ps # ver estado y salud
docker compose logs -f postgres # seguir el log
docker compose exec postgres psql -U ciclourbana -d ciclourbana
docker compose down # parar (los datos permanecen)
docker compose down -v # parar Y BORRAR el volumenCuidado con down -v: borra el volumen y con él todos los datos. En 07-04 retomaremos este fichero para añadir el contenedor de la propia aplicación.
El driver de PostgreSQL:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>Y la configuración correspondiente:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/ciclourbana
username: ciclourbana
password: ${POSTGRES_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: falsePuedes tener ambos drivers en el pom.xml simultáneamente: el que se use lo decide la URL. En 07-02 veremos cómo separar limpiamente ambas configuraciones en perfiles.
- Las propiedades esenciales de
spring.datasource
spring.datasource| Propiedad | Qué es | Ejemplo | ¿Obligatoria? |
|---|---|---|---|
url |
Cadena JDBC completa | jdbc:postgresql://localhost:5432/ciclourbana |
Sí (salvo BD embebida) |
username |
Usuario de base de datos | ciclourbana |
Casi siempre |
password |
Contraseña | ${POSTGRES_PASSWORD} |
Casi siempre |
driver-class-name |
Clase del driver JDBC | org.postgresql.Driver |
No, se deduce |
name |
Nombre del DataSource |
ciclourbana-ds |
No |
Por qué no hace falta el driver-class-name. Spring Boot usa DatabaseDriver, un enumerado que asocia prefijos de URL con clases de driver: jdbc:postgresql: → org.postgresql.Driver, jdbc:h2: → org.h2.Driver, jdbc:mysql: → com.mysql.cj.jdbc.Driver. Con la URL y el driver en el classpath, la deducción es automática. Solo hay que declararlo cuando usas un driver alternativo o una URL con un prefijo no reconocido.
Y si no configuras nada teniendo H2 en el classpath, Spring Boot crea un DataSource embebido con una URL aleatoria del estilo jdbc:h2:mem:2a5f.... Es cómodo para un arranque rápido, pero como el nombre cambia en cada ejecución no puedes conectarte con la consola. Para CicloUrbana lo declaramos siempre explícitamente.
- HikariCP en profundidad
HikariCP es el pool por defecto de Spring Boot desde la versión 2.0, y con razón: es el más rápido del ecosistema Java y el que menos configuración necesita para estar bien. Su filosofía es tener pocas opciones y valores por defecto sensatos.
Todos sus parámetros van bajo 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ámetro | Qué controla | Por defecto | Si te quedas corto | Si te pasas |
|---|---|---|---|---|
maximum-pool-size |
Máximo de conexiones simultáneas | 10 | Peticiones esperando y timeouts | Saturas la base de datos y empeora el rendimiento |
minimum-idle |
Conexiones ociosas que se mantienen | = máximo | Latencia al crear conexiones bajo pico | Conexiones ociosas consumiendo memoria en la BD |
connection-timeout |
Espera máxima por una conexión | 30 000 ms | Fallos espurios bajo carga | Los hilos se acumulan esperando y la app se congela |
idle-timeout |
Tiempo antes de cerrar una ociosa | 600 000 ms | Se abren y cierran conexiones sin parar | Conexiones muertas ocupando sitio |
max-lifetime |
Vida máxima de una conexión | 1 800 000 ms | Reciclado excesivo | Conexiones caducadas por el cortafuegos o la BD |
leak-detection-threshold |
Aviso si una conexión no se devuelve | 0 (desactivado) | Las fugas pasan inadvertidas | Falsos positivos con procesos largos legítimos |
Detalles que importan de verdad:
max-lifetime debe ser menor que el tiempo de vida que impongan la base de datos o el cortafuegos. Es la causa más común de errores intermitentes Connection is closed en producción: un cortafuegos corta conexiones ociosas a los 30 minutos y el pool sigue creyéndolas válidas. La recomendación oficial es fijarlo varios segundos por debajo de ese límite; 30 minutos es un valor prudente casi siempre.
minimum-idle igual a maximum-pool-size es la recomendación de los autores de HikariCP para cargas estables: un pool de tamaño fijo evita el coste de abrir conexiones justo en el peor momento, el del pico de tráfico.
leak-detection-threshold merece estar activado en desarrollo y preproducción. Cuando una conexión lleva prestada más del umbral, HikariCP escribe la traza de pila de quien la pidió. Es la forma más directa de encontrar una fuga:
Connection leak detection triggered for org.postgresql.jdbc.PgConnection@3f2a1b,
stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.ciclourbana.alquileres.AlquilerService.iniciar(AlquilerService.java:64)auto-commit: false deja la gestión de transacciones a Spring, que es lo que queremos con @Transactional (04-07). Spring Boot ya lo ajusta correctamente al usar JPA.
- Dimensionar el pool con criterio
La intuición dice «más conexiones, más rendimiento». Es falsa, y entenderlo diferencia a quien configura de quien copia.
Una base de datos ejecuta consultas en un número limitado de núcleos y discos. Con más conexiones activas que recursos, el servidor dedica tiempo a cambiar de contexto y a competir por bloqueos en lugar de trabajar. La documentación de HikariCP muestra un caso clásico: un servidor con 10 000 usuarios rindiendo mejor con un pool de 10 que con uno de 100.
La fórmula de referencia de PostgreSQL:
Para un PostgreSQL en un contenedor con 4 vCPU y almacenamiento SSD (donde el término de discos ronda 1-2):
Diez. Ese es el valor por defecto de HikariCP y rara vez hay que subirlo. Reglas prácticas para CicloUrbana:
- Empieza en 10. Solo súbelo con métricas que lo justifiquen (Actuator y Micrometer exponen
hikaricp.connections.*; lo veremos en 09-03). - Suma todas las instancias. Si despliegas 4 réplicas con pool de 10, la base de datos ve 40 conexiones. El
max_connectionsde PostgreSQL (100 por defecto) es un techo global que se agota antes de lo que la gente cree. - Si hay esperas, casi nunca la solución es más conexiones: suele ser una consulta lenta, un índice ausente o una transacción demasiado larga.
- Nunca hagas trabajo lento dentro de una transacción. Llamar a la pasarela de pago con una conexión prestada retiene un recurso escaso durante segundos. En CicloUrbana, el cobro del alquiler debe ocurrir fuera de la transacción; en 04-07 lo resolveremos con
@TransactionalEventListener.
- Las propiedades de JPA e Hibernate
Bajo spring.jpa se configura el comportamiento del ORM.
| Propiedad | Qué hace | Valor para CicloUrbana |
|---|---|---|
spring.jpa.hibernate.ddl-auto |
Gestión automática del esquema | update en dev, validate en prod |
spring.jpa.show-sql |
Imprime el SQL por System.out |
false (mejor usar el log) |
spring.jpa.properties.hibernate.format_sql |
Formatea el SQL en varias líneas | true en desarrollo |
spring.jpa.database-platform |
Dialecto SQL | Se deduce, no tocar |
spring.jpa.open-in-view |
Mantiene el contexto abierto en la vista | false |
spring.jpa.properties.hibernate.jdbc.batch_size |
Agrupa sentencias por lotes | 20 (útil en cargas) |
spring.jpa.defer-datasource-initialization |
Retrasa data.sql tras crear el esquema |
true si usas data.sql |
Los cinco valores de ddl-auto, que es la propiedad más peligrosa del framework:
| Valor | Qué hace | Cuándo usarlo |
|---|---|---|
none |
Nada. Hibernate no toca el esquema | Producción con Flyway (04-08) |
validate |
Comprueba que el esquema coincide con las entidades y falla si no | Producción. La mejor red de seguridad |
update |
Añade tablas y columnas que falten. No borra ni modifica | Desarrollo temprano, nunca producción |
create |
Borra el esquema y lo crea de cero al arrancar | Pruebas manuales desechables |
create-drop |
Como create, y además borra al parar |
Pruebas automáticas (módulo 6) |
update no es seguro en producción, y el motivo suele malinterpretarse. No es solo que «pueda borrar datos» —de hecho no borra columnas—: es que no puede modificar lo existente. Si cambias un varchar(80) a varchar(40), o añades una columna NOT NULL a una tabla con filas, update lo ignora en silencio o falla a medias, dejando el esquema en un estado que nadie ha revisado ni puede reproducir. Además no hay registro de qué se aplicó ni forma de deshacerlo.
El plan del módulo es explícito: usamos update mientras diseñamos las entidades en 04-03 y 04-04, y en 04-08 lo sustituimos por Flyway con validate. Cuando llegues a esa lección, update desaparece del proyecto para siempre.
El dialecto no se configura. Hibernate 6 lo detecta interrogando al driver y ajusta la generación de SQL a la versión concreta del motor. Fijarlo a mano solo sirve para quedarse en una versión antigua sin darse cuenta.
open-in-view: por qué se desactiva
open-in-view: por qué se desactivaSpring Boot activa por defecto spring.jpa.open-in-view=true, y avisa de ello con un mensaje en el log:
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é hace: un filtro (OpenEntityManagerInViewInterceptor) mantiene abierto el contexto de persistencia durante toda la petición HTTP, no solo durante la transacción del servicio.
Suena cómodo, y esa es la trampa. Los tres problemas que causa:
- Oculta el N+1 hasta producción. Si el controlador serializa una entidad con una colección perezosa, con
open-in-viewactivo la colección se carga sin protestar, disparando consultas durante la serialización. Con él desactivado, saltaLazyInitializationExceptionen desarrollo, que es donde quieres enterarte. - Retiene la conexión más tiempo del necesario. La conexión JDBC puede quedar asociada a la petición completa, incluida la serialización JSON. Con un pool de 10, esto reduce drásticamente el número de peticiones concurrentes.
- Difumina las fronteras. La capa de presentación acaba ejecutando consultas, justo lo contrario de la separación de capas que construimos en 03-05 con los DTOs.
Para CicloUrbana la decisión está tomada desde esta lección:
La contrapartida es que el servicio debe devolver DTOs completamente mapeados, con todo lo necesario ya cargado. Es exactamente lo que ya hacemos desde 03-05 con EstacionMapper y EstacionDetalleResponse, así que el coste es cero. Volveremos sobre ello en 04-04 y 04-07.
- Ver el SQL generado de forma legible
Trabajar con un ORM sin ver el SQL es programar a ciegas. Hay dos formas de verlo y una es claramente mejor.
Forma rápida (evítala en serio): spring.jpa.show-sql: true. Escribe por System.out, sin formato, sin nivel, sin marca de tiempo y sin pasar por el sistema de logs. Sirve para un vistazo y poco más.
Forma correcta: el log de Hibernate.
logging:
level:
org.hibernate.SQL: DEBUG # las sentencias
org.hibernate.orm.jdbc.bind: TRACE # los valores de los parámetros
org.hibernate.stat: DEBUG # estadísticas por sesión
spring:
jpa:
show-sql: false
properties:
hibernate:
format_sql: true
highlight_sql: true
generate_statistics: trueCon org.hibernate.SQL en DEBUG verás cada sentencia; con org.hibernate.orm.jdbc.bind en TRACE, los valores que sustituyen a las ?. Ojo con el nombre: en Hibernate 5 era org.hibernate.type.descriptor.sql; en Hibernate 6, el que trae Spring Boot 3, es org.hibernate.orm.jdbc.bind. Copiar la configuración antigua es un motivo frecuente de «no me salen los parámetros».
El resultado en consola:
Hibernate:
select
e1_0.id,
e1_0.capacidad,
e1_0.direccion,
e1_0.nombre
from
estaciones e1_0
where
e1_0.capacidad>=?
binding parameter [1] as [INTEGER] - [24]generate_statistics: true añade, al final de cada sesión, un resumen valiosísimo para detectar el N+1:
Cuarenta y siete entidades con dos sentencias está bien. Cuarenta y siete entidades con cuarenta y ocho sentencias es un N+1 de manual (04-04).
Advertencia de seguridad: el nivel TRACE de los parámetros imprime en el log todos los valores enviados, incluidos correos, contraseñas cifradas o datos personales de los ciudadanos de Ribalta. Es una configuración de desarrollo. En producción, nunca.
- Múltiples fuentes de datos
Ocasionalmente una aplicación necesita hablar con dos bases de datos: CicloUrbana podría tener su base operativa y una réplica de solo lectura para informes. Al declarar dos DataSource, la autoconfiguración se desactiva y hay que construirlos a mano.
package com.ciclourbana.comun.config;
@Configuration
public class ConfiguracionFuentesDatos {
@Bean
@Primary
@ConfigurationProperties("ciclourbana.datasource.principal")
public DataSourceProperties propiedadesPrincipal() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSourcePrincipal() {
return propiedadesPrincipal()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
// El par de beans para "informes" es idéntico, sin @Primary y
// apuntando 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 ya conoces de 02-02) resuelve la ambigüedad: sin él, cualquier inyección de DataSource fallaría con NoUniqueBeanDefinitionException. Además, con dos fuentes hay que declarar manualmente EntityManagerFactory y TransactionManager para cada una, y separar los repositorios por paquetes con @EnableJpaRepositories(basePackages = ...).
Es bastante trabajo, y por eso la recomendación es clara: no lo hagas salvo que sea imprescindible. Muchas veces lo que se busca (aislar informes) se resuelve mejor con vistas materializadas, caché (09-02) o separando el servicio (07-05).
- Credenciales fuera del repositorio
La contraseña de PostgreSQL no puede estar en application.yml, porque application.yml está en Git y Git tiene memoria eterna: borrarla en un commit posterior no la elimina del historial.
Ya vimos la precedencia de configuración en 02-04; aquí la aplicamos. La forma más portable es la variable de entorno con marcador de posición:
spring:
datasource:
url: ${CICLOURBANA_DB_URL:jdbc:postgresql://localhost:5432/ciclourbana}
username: ${CICLOURBANA_DB_USER:ciclourbana}
password: ${CICLOURBANA_DB_PASSWORD}Los dos primeros llevan valor por defecto tras :; el tercero no, deliberadamente: si la variable no está definida, la aplicación falla al arrancar con un mensaje claro en lugar de intentar conectarse con una contraseña vacía.
Recuerda además la traducción automática de nombres: Spring Boot convierte spring.datasource.password en SPRING_DATASOURCE_PASSWORD, así que definir esa variable de entorno funciona sin escribir nada en el YAML.
| Opción | Dónde encaja | Nivel |
|---|---|---|
| Variables de entorno | Cualquier despliegue | Bueno |
Fichero .env fuera de Git |
Desarrollo local | Aceptable |
| Secretos de Kubernetes | Producción en K8s (08-04) | Muy bueno |
| Vault / AWS Secrets Manager | Producción con rotación | El mejor |
Contraseña en application.yml |
Ninguno | Inaceptable |
Y en .gitignore, como mínimo: .env, *.env.local y datos/.
- Comprobar la conexión al arrancar
Con todo configurado, conviene una verificación explícita. Un CommandLineRunner como los de 01-05, restringido a desarrollo:
package com.ciclourbana.comun;
// imports: javax.sql.DataSource, java.sql.Connection, java.sql.DatabaseMetaData,
// org.slf4j.*, org.springframework.boot.CommandLineRunner, org.springframework.stereotype.Component
@Component
public class VerificadorConexion implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(VerificadorConexion.class);
private final DataSource dataSource;
public VerificadorConexion(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void run(String... args) throws Exception {
try (Connection conexion = dataSource.getConnection()) {
DatabaseMetaData metadatos = conexion.getMetaData();
log.info("Conexión establecida con {} {}",
metadatos.getDatabaseProductName(),
metadatos.getDatabaseProductVersion());
log.info("Driver: {} {}",
metadatos.getDriverName(), metadatos.getDriverVersion());
log.info("URL: {}", metadatos.getURL());
}
}
}El try-with-resources es importante: sin él, la conexión no vuelve al pool y acabas de crear una fuga en la primera línea de código que toca el DataSource.
Salida esperada con PostgreSQL:
INFO c.c.comun.VerificadorConexion : Conexión establecida con PostgreSQL 16.2
INFO c.c.comun.VerificadorConexion : Driver: PostgreSQL JDBC Driver 42.7.2
INFO c.c.comun.VerificadorConexion : URL: jdbc:postgresql://localhost:5432/ciclourbanaEn el módulo 7 veremos que Actuator ofrece esto mismo de forma permanente en /actuator/health, con un indicador db que ejecuta una consulta de validación.
Errores Comunes y Consejos
Dejar ddl-auto: update al desplegar. El error más caro del módulo. Funciona en desarrollo, parece funcionar en producción y un día deja el esquema en un estado que nadie sabe reconstruir. La lección 04-08 existe para eliminarlo.
Subir maximum-pool-size para arreglar lentitud. Casi siempre empeora: la base de datos se satura y todas las consultas se ralentizan. Antes de tocar el pool, mira el SQL, los índices y la duración de las transacciones.
Dejar open-in-view en su valor por defecto. El aviso del log se ignora sistemáticamente. Ponlo a false en la primera línea de configuración JPA del proyecto; hacerlo más tarde saca a la luz decenas de LazyInitializationException de golpe.
Olvidar DB_CLOSE_DELAY=-1 en H2. Produce el desconcertante «mis datos desaparecen a mitad de ejecución» sin ningún error.
Usar org.hibernate.type.descriptor.sql para ver los parámetros. Es el nombre de Hibernate 5. En Spring Boot 3 hay que usar org.hibernate.orm.jdbc.bind.
Dejar la consola de H2 accesible. Ejecución de SQL arbitrario sin autenticación. Solo en desarrollo, solo en localhost.
Consejo: fija pool-name. Con CicloUrbanaPool los mensajes del log y las métricas son identificables de un vistazo, sobre todo si algún día hay dos pools.
Consejo: activa leak-detection-threshold en desarrollo. Sesenta segundos es un buen umbral. Encontrar una fuga por su traza de pila cuesta minutos; encontrarla en producción por agotamiento del pool cuesta una tarde.
Consejo: fija la versión de la imagen de Docker. postgres:16-alpine, nunca postgres:latest. Que el equipo entero use el mismo motor evita la clase de fallo más difícil de reproducir.
Ejercicios
Ejercicio 1: dimensionar el pool de CicloUrbana
El ayuntamiento despliega CicloUrbana en 3 réplicas. El PostgreSQL tiene 8 vCPU, SSD y max_connections = 100. Hay además un proceso nocturno de informes que abre hasta 5 conexiones y un panel de administración con 5 más.
- Calcula el
maximum-pool-sizepor réplica con la fórmula de PostgreSQL. - Comprueba que el total no agota
max_connections. - Escribe el bloque
spring.datasource.hikaricompleto, justificando cada valor.
Ejercicio 2: separar desarrollo y producción sin duplicar configuración
Escribe la configuración de CicloUrbana repartida en application.yml (común), application-dev.yml (H2 + consola + logs de SQL) y application-prod.yml (PostgreSQL + credenciales por variable de entorno + validate). Indica qué propiedad nunca debe aparecer en el fichero de producción y por qué.
Ejercicio 3: diagnosticar un agotamiento del pool
En producción aparece este error de forma intermitente en horas punta:
Enumera cuatro causas posibles ordenadas de más a menos probable, y para cada una indica cómo confirmarla y cómo corregirla. Explica por qué subir maximum-pool-size no es la primera respuesta.
Soluciones
Solución 1.
- Fórmula:
(núcleos * 2) + husos_efectivos = (8 * 2) + 2 = 18conexiones para toda la base de datos, no por réplica. Repartidas entre 3 réplicas:18 / 3 = 6por réplica. Un valor de 6 es defendible; 8 también, dejando margen. Lo que no es defendible es 20 por réplica. - Total:
3 réplicas × 6 = 18, más 5 del proceso nocturno y 5 del panel = 28 conexiones. Frente amax_connections = 100queda muchísimo margen, incluido el que PostgreSQL reserva para superusuario y mantenimiento. Con 20 por réplica serían 70, ya incómodo y muy por encima de lo que 8 vCPU pueden atender en paralelo. - Configuración:
spring:
datasource:
hikari:
pool-name: CicloUrbanaPool
maximum-pool-size: 6 # (8*2+2)/3 réplicas
minimum-idle: 6 # pool fijo: sin coste de apertura en el pico
connection-timeout: 3000 # fallar rápido (3 s) en vez de encolar hilos
idle-timeout: 600000 # irrelevante con minimum-idle = maximum
max-lifetime: 1800000 # 30 min, por debajo del corte del cortafuegos
leak-detection-threshold: 0 # desactivado en producciónEl valor más discutible es connection-timeout: 3000. Bajarlo de los 30 s por defecto es deliberado: si el pool está agotado, esperar 30 segundos solo consigue acumular hilos y agravar el problema. Fallar en 3 segundos devuelve un 503 rápido, mantiene la aplicación viva y deja el síntoma visible en las métricas.
Solución 2.
# application.yml — común a todos los entornos
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 }Lo que nunca debe aparecer en producción, por orden de gravedad:
org.hibernate.orm.jdbc.bind: TRACE: volcaría al log todos los datos personales de los ciudadanos de Ribalta que pasen por una consulta. Es una brecha de privacidad, además de un coste de rendimiento notable.ddl-auto: updateocreate: modificaciones de esquema no revisadas, o pérdida total de datos.spring.h2.console.enabled: true: ejecución de SQL arbitrario sin autenticación.- La contraseña literal: siempre por variable de entorno, y sin valor por defecto para que el fallo sea ruidoso.
Los perfiles se activan con --spring.profiles.active=prod o SPRING_PROFILES_ACTIVE=prod, y se estudian a fondo en 07-02.
Solución 3. Causas ordenadas por probabilidad real:
- Transacciones demasiado largas (la más probable). Un método
@Transactionalque llama a un servicio externo —la pasarela de pago de CicloUrbana— retiene la conexión durante toda la llamada de red. Con 6 conexiones y llamadas de 2 segundos, el pool se agota con muy poco tráfico. Confirmar: activarleak-detection-thresholden preproducción y revisar los métodos@Transactionalen busca de E/S. Corregir: sacar la llamada externa fuera de la transacción, con@TransactionalEventListener(AFTER_COMMIT)(04-07). - Fuga de conexiones. Algún código obtiene una conexión del
DataSourcesintry-with-resources. Es raro con Spring Data, pero aparece en utilidades escritas a mano. Confirmar:leak-detection-threshold: 60000y buscar las trazas «Apparent connection leak detected». Corregir: cerrar siempre contry-with-resources. - Consultas lentas por falta de índice. Una consulta que tarda 5 segundos ocupa su conexión 5 segundos. Confirmar:
pg_stat_statementsen PostgreSQL olog_min_duration_statement = 1000. Corregir: índices y reescritura de la consulta (09-01). - Problema N+1. Un endpoint que dispara cientos de consultas por petición multiplica el tiempo de retención. Confirmar:
generate_statistics: truey contar sentencias por sesión. Corregir:JOIN FETCHo@EntityGraph(04-04 y 04-06).
Por qué subir maximum-pool-size no es la primera respuesta: el agotamiento es un síntoma, no la enfermedad. Si la causa es una transacción de 2 segundos, duplicar el pool duplica la carga sobre la base de datos sin arreglar nada; el problema reaparece con el doble de tráfico, ahora con el servidor más saturado. Más aún, más conexiones activas competiendo por CPU y bloqueos ralentizan todas las consultas, incluidas las que iban bien. Subir el pool es la última medida, después de haber medido y descartado las cuatro causas anteriores.
Conclusión
CicloUrbana ya tiene fontanería. Sabes qué es un DataSource y por qué abrir conexiones es caro, lo que justifica que exista un pool y que close() no cierre nada. Tienes H2 en memoria configurado para desarrollo con DB_CLOSE_DELAY=-1 y MODE=PostgreSQL, su consola web disponible solo en localhost y la advertencia de seguridad grabada. Tienes un docker-compose.yml con PostgreSQL 16, volumen con nombre, healthcheck y contraseña por variable de entorno, listo para retomarse en 07-04. Conoces las cuatro propiedades esenciales de spring.datasource y por qué el driver se deduce solo. Has recorrido HikariCP parámetro a parámetro, sabes que max-lifetime debe quedar por debajo del corte del cortafuegos, que minimum-idle igual al máximo evita el peor momento para abrir conexiones y que leak-detection-threshold es la forma más rápida de encontrar una fuga. Y, sobre todo, tienes un criterio defendible para dimensionar el pool: (núcleos × 2) + discos, repartido entre réplicas, con la certeza de que más conexiones no significan más rendimiento.
Del lado de JPA has fijado las decisiones que gobiernan el resto del módulo: ddl-auto en update solo mientras diseñamos las entidades, con el compromiso explícito de sustituirlo por Flyway y validate en 04-08; open-in-view: false desde ya, aceptando que el servicio devuelva DTOs completos a cambio de que el N+1 y las cargas perezosas den la cara en desarrollo; y el log de Hibernate configurado con org.hibernate.SQL y org.hibernate.orm.jdbc.bind para poder leer el SQL real con sus parámetros. También sabes cómo se declaran varias fuentes de datos con @Primary y por qué conviene evitarlo, cómo mantener las credenciales fuera de Git y cómo verificar la conexión al arrancar sin dejarte una fuga por el camino.
Pero la base de datos está vacía: no hay ni una tabla, porque no hay ni una entidad. Estacion sigue siendo un record inmutable en memoria. La lección 04-03, Creación de Entidades JPA, lo cambia: veremos por qué un record no puede ser una entidad, convertiremos Estacion, Bicicleta y Alquiler en entidades con @Entity, @Table e índices, elegiremos la estrategia de generación de identificadores adecuada para PostgreSQL, mapearemos cada tipo de dato con cuidado —incluyendo por qué los importes son BigDecimal y nunca double—, incrustaremos la Ubicacion con @Embeddable, añadiremos auditoría automática y estrenaremos @Version, el bloqueo optimista que jubila definitivamente al ShallowEtagHeaderFilter de 03-03.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
