Hibernate: Guia complementària a "JPA (Java Persistence API) i Spring Boot"¶
Document relacionat: JPA amb Spring Boot
Introducció a Hibernate¶
Hibernate és la implementació més popular de JPA en el desenvolupament amb Java i Spring Boot. Mentre que JPA defineix la especificació estàndard, Hibernate és la implementació concreta que ofereix funcionalitats avançades i optimitzacions de rendiment.
Hibernate vs altres implementacions¶
| Aspecte | Hibernate | EclipseLink | OpenJPA |
|---|---|---|---|
| Popularitat | Molt alta (90%+) | Moderada | Baixa |
| Suport a Spring Boot | Integrat per defecte | Compatible | Compatible |
| Documentació | Excel·lent | Bona | Acceptable |
| Performance | Excellent | Bon | Acceptable |
| Funcionalitats avançades | Moltes | Moderades | Moderades |
Arquitectura de Hibernate¶
Hibernate actua com a capa intermediària entre la aplicació Java i la base de dades:
┌─────────────────┐
│ Aplicació Java │
└────────┬────────┘
│
▼
┌─────────────────────┐
│ Hibernate (JPA) │
│ - Mapeig ORM │
│ - Transaccions │
│ - Cache │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ JDBC / Drivers │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ Base de dades │
└─────────────────────┘
Configuració de Hibernate a Spring Boot¶
Dependències necessàries¶
Afegir al pom.xml:
<!-- Spring Data JPA (inclou Hibernate) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Driver JDBC per a la BD (exemple MySQL) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
Configuració de application.properties¶
# ======================================
# CONFIGURACIÓ DE LA BASE DE DADES
# ======================================
# Connexió a la BD
spring.datasource.url=jdbc:mysql://localhost:3306/mi_aplicacio
spring.datasource.username=root
spring.datasource.password=contrasenya
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ======================================
# CONFIGURACIÓ DE HIBERNATE
# ======================================
# Dialect: defineix com traduir HQL a SQL natiu
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# DDL Auto: estratègia de creació/actualització de taules
# - create: elimina i crea les taules cada vegada (NOMÉS en desarrollo)
# - create-drop: crea les taules i les elimina al tancar (PROVES)
# - update: actualitza les taules (RECOMANAT en desarrollo)
# - validate: verifica que les entitats coincideixen amb la BD (PRODUCCIÓ)
# - none: no fa res (PRODUCCIÓ)
spring.jpa.hibernate.ddl-auto=update
# Mostrar SQL al log
spring.jpa.show-sql=true
# Formatejar SQL per llegibilitat
spring.jpa.properties.hibernate.format_sql=true
# Mostrar paràmetres bind de SQL
spring.jpa.properties.hibernate.use_sql_comments=true
# ======================================
# CONFIGURACIÓ AVANÇADA
# ======================================
# Batch size per a insercions/actualitzacions
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
# Second-level cache (opcional)
spring.jpa.properties.hibernate.cache.use_second_level_cache=false
# Maximum pool connections
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
Configuració alternativa amb application.yml¶
spring:
datasource:
url: jdbc:mysql://localhost:3306/mi_aplicacio
username: root
password: contrasenya
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
dialect: org.hibernate.dialect.MySQL8Dialect
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
jdbc:
batch_size: 20
SQL Dialects de Hibernate¶
Un Dialect en Hibernate és una classe que actua com a pont entre HQL (Hibernate Query Language) i SQL natiu de la base de dades. Cada dialecte és específic per a cada SGBD.
Dialects més comuns¶
| Base de dades | Dialect | Versió mínima |
|---|---|---|
| MySQL 5.7+ | org.hibernate.dialect.MySQL8Dialect |
5.7 |
| MySQL 8.0+ | org.hibernate.dialect.MySQL8Dialect |
8.0 |
| MariaDB | org.hibernate.dialect.MariaDBDialect |
10.3 |
| PostgreSQL 11+ | org.hibernate.dialect.PostgreSQLDialect |
11.0 |
| Oracle 11g+ | org.hibernate.dialect.OracleDialect |
11.2 |
| SQL Server 2016+ | org.hibernate.dialect.SQLServerDialect |
2016 |
| H2 (en memòria) | org.hibernate.dialect.H2Dialect |
2.1.214 |
| SQLite | org.hibernate.dialect.SQLiteDialect |
- |
Exemple: canviar de MySQL a PostgreSQL¶
MySQL:
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.datasource.url=jdbc:mysql://localhost:3306/mi_bd
PostgreSQL:
spring.jpa.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url=jdbc:postgresql://localhost:5432/mi_bd
La resta del codi Java no canvia. Aquesta és la potència dels dialects.
Estratègies de càrrega (Fetch Strategies)¶
Una de les funcionalitats més importants de Hibernate és la estratègia de càrrega de dades, que defineix quan es carreguen les relacions entre entitats.
1. Lazy Loading (per defecte)¶
Lazy Loading carrega les dades relacionades només quan s'accedeixen per primera vegada.
Exemple:¶
@Entity
public class Autor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
// Per defecte és LAZY
@OneToMany(mappedBy = "autor", fetch = FetchType.LAZY)
private List<Llibre> llibres = new ArrayList<>();
}
// En el servei:
Autor autor = autorRepository.findById(1L).get();
System.out.println(autor.getNom()); // ✓ Còpia SQL: SELECT de Autor
// La primera vegada que accedim a llibres:
List<Llibre> llibres = autor.getLlibres(); // ✓ 2a SQL: SELECT de Llibres
Avantatges del Lazy Loading:¶
- Menor consum de memòria: Només es carreguen les dades necessàries
- Més ràpid al principi: La consulta inicial és més rápida
- Flexibilitat: Pots decidir quin moment carregar les relacions
Desavantatges del Lazy Loading:¶
- Més consultes a la BD: Una consulta per cada relació que accedim
- LazyInitializationException: Si accedim fora de la transacció, pot fallar
// ERROR possible:
Autor autor = autorRepository.findById(1L).get();
entityManager.detach(autor); // Surt del context
List<Llibre> llibres = autor.getLlibres(); // LazyInitializationException!
2. Eager Loading¶
Eager Loading carrega immediatament totes les relacions quan es carrega l'entitat principal.
Exemple:¶
@Entity
public class Autor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
@OneToMany(mappedBy = "autor", fetch = FetchType.EAGER)
private List<Llibre> llibres = new ArrayList<>();
}
// En el servei:
Autor autor = autorRepository.findById(1L).get();
// ✓ Una única SQL amb JOIN: SELECT Autor + Llibres
List<Llibre> llibres = autor.getLlibres(); // Ja estan carregats!
Avantatges del Eager Loading:¶
- Menys consultes: Tot es carrega en una única consulta (o poques)
- No hi ha LazyInitializationException
- Més simple de codificar: No necessites pensar en quan carregar
Desavantatges del Eager Loading:¶
- Més memòria: Es carreguen sempre dades que potser no necessites
- Més lent al principi: La primera consulta pot ser pesada
- Cartesian Product problem: Amb múltiples relacions, les dades es multipliquen
// Problema: si tinguis 2 relacions EAGER, les files es multipliquen
// 1 Autor + 10 Llibres + 5 Premis = 50 files del mateix Autor!
3. Recomanacions d'ús¶
Quan usar Lazy Loading (RECOMANAT):¶
- Quan les relacions contenen molts registres
- Quan no sempre necessites les relacions
- Quan vols optimitzar el primer carregar
- La majoria dels casos!
Quan usar Eager Loading:¶
- Quan sempre necessites les relacions
- Quan les relacions són petites
- Quan vols evitar múltiples consultes
Solució alternativa: JOIN FETCH (RECOMANADA)¶
@Repository
public interface AutorRepository extends JpaRepository<Autor, Long> {
// Lazy per defecte en l'entitat, però EAGER quan el necessitem
@Query("SELECT DISTINCT a FROM Autor a " +
"LEFT JOIN FETCH a.llibres " +
"WHERE a.id = :id")
Optional<Autor> findByIdWithLlibres(@Param("id") Long id);
// Sense llibres, si no els necessitem
Autor findById(Long id);
}
Anotacions avançades de Hibernate¶
@NamedQueries¶
Definir queries reutilitzables a nivell d'entitat:
@Entity
@NamedQueries({
@NamedQuery(
name = "Alumne.findPerNom",
query = "SELECT a FROM Alumne a WHERE a.nom = :nom"
),
@NamedQuery(
name = "Alumne.findPerEdat",
query = "SELECT a FROM Alumne a WHERE a.edat >= :edat"
)
})
public class Alumne {
// ...
}
// En el repository:
@Repository
public interface AlumneRepository extends JpaRepository<Alumne, Long> {
@Query(name = "Alumne.findPerNom")
List<Alumne> buscarPerNom(@Param("nom") String nom);
@Query(name = "Alumne.findPerEdat")
List<Alumne> buscarPerEdatMinima(@Param("edat") int edat);
}
@DynamicInsert i @DynamicUpdate¶
@Entity
@DynamicInsert // Només inserta les columnes no-null
@DynamicUpdate // Només actualitza les columnes que canvien
public class Alumne {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
private String correu;
private Integer edat;
}
@Check (validació a nivell de BD)¶
@Entity
@Check(constraints = "edat >= 18")
public class Alumne {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nom;
@Min(18)
private int edat;
}
Caching en Hibernate¶
Hibernate ofereix dues nivells de cache:
First-Level Cache (SessionCache)¶
Actiu per defecte, local a cada sesió:
@Service
public class AlumneService {
@Autowired
private AlumneRepository alumneRepository;
@Transactional
public void demoCache() {
// Primera consulta: accedeix a la BD
Alumne a1 = alumneRepository.findById(1L).get(); // SQL!
// Segona consulta: agafa de la cache de sessió
Alumne a2 = alumneRepository.findById(1L).get(); // SIN SQL!
System.out.println(a1 == a2); // true (mateixa instància)
}
}
Second-Level Cache (Distributed Cache)¶
Per compartir cache entre sessions (opcional):
# Activar cache de nivell 2
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
# Provider de cache (exemple: ehcache)
spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
@Entity
@Cacheable // Habilitar cache a nivell 2 per a aquesta entitat
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Alumne {
// ...
}
Logging i debugging amb Hibernate¶
Habilitar logs detallats¶
# Mostrar SQL executa
spring.jpa.show-sql=true
# Logs de Hibernate
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Logs de database connection pool (HikariCP)
logging.level.com.zaxxer.hikari=DEBUG
# Format SQL
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
Exemple de log útil:¶
Hibernate: select alumne0_.id as id1_0_, alumne0_.correu as correu2_0_,
alumne0_.edat as edat3_0_, alumne0_.nom as nom4_0_
from alumnes alumne0_
where alumne0_.id=?
2025-12-16 08:00:00 - SQL binding parameter [1] as [BIGINT] - [1]
Optimitzacions de rendiment¶
1. Batch Processing¶
Per inserir/actualitzar molts registres eficientment:
@Service
public class AlumneService {
@Autowired
private AlumneRepository alumneRepository;
@Transactional
public void inserirMuchosAlumnos(List<Alumne> alumnes) {
for (int i = 0; i < alumnes.size(); i++) {
alumneRepository.save(alumnes.get(i));
// Cada 20 registres, flush per no carregar memòria
if (i % 20 == 0) {
// Hibernate farà flush automàtic si està configurat
}
}
}
}
2. Projection (Carregar només columnes necessàries)¶
// Interface per a la projecció
public interface AlumneDTO {
Long getId();
String getNom();
String getCorreu();
}
@Repository
public interface AlumneRepository extends JpaRepository<Alumne, Long> {
// Carregar només nom i correu
@Query("SELECT a.id as id, a.nom as nom, a.correu as correu FROM Alumne a")
List<AlumneDTO> trobarTots();
}
3. Evitar N+1 Problem¶
// EVITA (provoca N+1 queries):
List<Autor> autors = autorRepository.findAll(); // 1 query
for (Autor a : autors) {
System.out.println(a.getLlibres()); // N queries més!
}
// CORRECTE (usa JOIN FETCH):
@Query("SELECT DISTINCT a FROM Autor a JOIN FETCH a.llibres")
List<Autor> findAllWithBooks();
Comparació: JPA pura vs Hibernate¶
| Feature | JPA | Hibernate |
|---|---|---|
| Especificació | Estàndard | Implementació |
| EntityManager | ✓ | ✓ |
| Dialects | No | ✓ |
| Lazy/Eager | ✓ | ✓ (avançat) |
| Named Queries | ✓ | ✓ |
| Cache nivell 2 | ✗ | ✓ |
| Criteria API | ✓ | ✓ (millor) |
| Native Query | ✓ | ✓ |
| Batch processing | ✗ | ✓ |
| Dynamic Insert/Update | ✗ | ✓ |
Resum¶
Hibernate és la implementació més popular de JPA que ofereix:
- Abstracció de BD: Dialects per a múltiples bases de dades
- Estratègies de càrrega: Lazy i Eager loading
- Optimitzacions: Cache, batch processing, projections
- Debugging: Logging detallat de queries
- Flexibilitat: Configuració avançada per a casos complexos
Per a la majoria de casos amb Spring Boot, Lazy Loading és la millor opció. Usa JOIN FETCH quan necessitis carregar relacions de forma explícita.
Recursos addicionals¶
Data de creació: Desembre 2025
Destinatari: Alumnes de DWES, DWEC i DAW - CIFP Francesc de Borja Moll
Referència: Document "JPA (Java Persistence API) i Spring Boot"
