Salta el contingut

Spring_Boot_Documentation_Header_1200x200px_with_Xavier_Sastre_Mug_and_Logo_ajustat.png

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:

  1. Abstracció de BD: Dialects per a múltiples bases de dades
  2. Estratègies de càrrega: Lazy i Eager loading
  3. Optimitzacions: Cache, batch processing, projections
  4. Debugging: Logging detallat de queries
  5. 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"