f in x
JPA e Hibernate — Diagnosticare e Risolvere l'N+1 Problem con Esempi Reali
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Sviluppo di siti web

JPA e Hibernate — Diagnosticare e Risolvere l'N+1 Problem con Esempi Reali

[2026-06-22] Author: Ing. Calogero Bono

La tua applicazione Java va bene in sviluppo, ma in produzione impiega secondi per caricare una lista di ordini. Apri i log e vedi centinaia di query SQL identiche. Sei incappato nell'N+1 problem di JPA e Hibernate. Noi, di Meteora Web, lo incontriamo in quasi tutti i progetti che ci arrivano per audit di performance. Non è un difetto del framework: è un uso scorretto delle relazioni e del fetching. In questa guida operativa ti mostriamo come riconoscerlo, diagnosticarlo e risolverlo con strategie concrete.

Come si genera l'N+1 Problem in JPA e Hibernate?

Immagina di avere un'entità Ordine con una relazione @OneToMany verso RigaOrdine. Con il fetching LAZY (default nelle collezioni), quando carichi un ordine Hibernate esegue una query per l'ordine e, solo quando accedi alla lista delle righe, scatta una nuova query per ogni ordine. Se hai 100 ordini, hai 1 + 100 query. Questo è l'N+1.

Esempio classico:

@Entity
public class Ordine {
    @Id private Long id;
    private String cliente;
    @OneToMany(mappedBy = "ordine", fetch = FetchType.LAZY)
    private List<RigaOrdine> righe = new ArrayList<>();
}
@Entity
public class RigaOrdine {
    @Id private Long id;
    private String prodotto;
    private int quantita;
    @ManyToOne
    @JoinColumn(name = "ordine_id")
    private Ordine ordine;
}

Il codice che itera sugli ordini e calcola il totale per ordine:

Sponsored Protocol

List<Ordine> ordini = entityManager.createQuery("SELECT o FROM Ordine o", Ordine.class).getResultList();
for (Ordine o : ordini) {
    System.out.println("Totale ordine " + o.getId() + ": " + o.getRighe().stream().mapToInt(RigaOrdine::getQuantita).sum());
}

Risultato: 1 query per gli ordini, poi 100 query per le righe. L'applicazione va in ginocchio.

Come diagnosticare l'N+1 Problem in produzione?

La diagnosi a occhio è impossibile. Serve strumentazione. Noi di Meteora Web attiviamo sempre le statistiche di Hibernate in fase di test e staging, e se possibile in produzione con un logger dedicato.

Abilitare le statistiche di Hibernate

# application.properties
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

Nel log vedrai una riga come: Number of JDBC connections: 1, PreparedStatement: 101, Time: 1234 ms. Se il numero di PreparedStatement è molto superiore al numero di righe attese, è N+1.

Sponsored Protocol

Usare un interceptor per query count

Puoi anche implementare un semplice interceptor Spring che conta le query per request:

@Component
public class QueryCounter {
    private final ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);
    
    @Autowired
    private EntityManagerFactory emf;
    
    @PostConstruct
    public void init() {
        SessionFactory sf = emf.unwrap(SessionFactory.class);
        sf.getStatistics().setStatisticsEnabled(true);
    }
    
    public int getQueryCount() {
        return sf.getStatistics().getQueryExecutionCount();
    }
    
    public void reset() {
        sf.getStatistics().clear();
    }
}

Poi in un test o in un endpoint di debug richiama getQueryCount() prima e dopo l'operazione.

Quali strategie di fetching usare per risolvere l'N+1?

Hibernate offre diverse strategie. La scelta dipende dal caso d'uso: caricamento eager, join fetch, batch fetching, entity graph, subselect. Vediamole.

JOIN FETCH nella JPQL

La soluzione più immediata: usare JOIN FETCH per caricare le associazioni in una sola query.

Sponsored Protocol

List<Ordine> ordini = entityManager.createQuery(
    "SELECT o FROM Ordine o JOIN FETCH o.righe", Ordine.class).getResultList();

Funziona, ma attenzione: se hai più di una collezione, si genera un prodotto cartesiano. Hibernate 6 introduce il @Fetch(FetchMode.SUBSELECT) per evitarlo.

Batch Fetching

Configura il batch fetching a livello di entità. Hibernate caricherà le associazioni LAZY a batch.

spring.jpa.properties.hibernate.default_batch_fetch_size=10

Oppure a livello di mapping:

@OneToMany(mappedBy = "ordine")
@BatchSize(size = 10)
private List<RigaOrdine> righe;

Con batch size 10, per 100 ordini avrai 1 + 10 query (non più 101).

Subselect

Con @Fetch(FetchMode.SUBSELECT), Hibernate esegue una seconda query con una subselect per caricare tutte le collezioni in un colpo solo.

@OneToMany(mappedBy = "ordine")
@Fetch(FetchMode.SUBSELECT)
private List<RigaOrdine> righe;

Utile quando hai molte entità e devi caricare tutte le collezioni. Attenzione: se la prima query è complessa, la subselect può essere pesante.

Come utilizzare @EntityGraph per query dinamiche?

Spring Data JPA offre @EntityGraph per definire path di fetching su metodi dei repository, senza modificare il modello.

Sponsored Protocol

@Repository
public interface OrdineRepository extends JpaRepository<Ordine, Long> {
    @EntityGraph(attributePaths = {"righe"})
    List<Ordine> findAll();
    
    @EntityGraph(attributePaths = {"righe.prodotto"})
    Optional<Ordine> findByIdWithRigheAndProdotti(Long id);
}

Puoi anche definire entity graph a livello di entità con @NamedEntityGraph:

@Entity
@NamedEntityGraph(name = "Ordine.righe", attributeNodes = @NamedAttributeNode("righe"))
public class Ordine { ... }

E poi usarlo nel repository: @EntityGraph("Ordine.righe"). Questo approccio separa le preoccupazioni: il modello resta LAZY, ma in query specifiche carichi ciò che serve.

Come integrare il batch fetching con Spring Boot?

Abbiamo già visto le property. Il batch fetching è semplice da attivare e spesso basta. Però attenzione: il batch size non deve essere troppo alto (sovraccarico di bind parameters) né troppo basso (poco miglioramento). Un buon punto di partenza è 10-20.

Se usi spring.jpa.properties.hibernate.default_batch_fetch_size=10, Hibernate applica il batch fetching a tutte le associazioni LAZY. Puoi anche combinarlo con hibernate.jdbc.batch_size per ottimizzare le scritture.

Sponsored Protocol

Noi, di Meteora Web, consigliamo di partire dal batch fetching prima di passare a strategie più invasive come JOIN FETCH su tutte le query. Spesso risolve l'N+1 senza cambiare una riga di codice delle query.

Cosa fare subito per prevenire l'N+1?

  1. Abilita le statistiche Hibernate nei tuoi ambienti di test e staging. Non aspettare che arrivi la segnalazione di lentezza in produzione.
  2. Imposta un batch fetching globale (hibernate.default_batch_fetch_size=10) come baseline.
  3. Rivedi ogni repository che esegue una findAll o query su collezioni. Aggiungi @EntityGraph dove necessario.
  4. Nei casi con più collezioni, usa @Fetch(FetchMode.SUBSELECT) o effettua due query separate con mappatura in memoria.
  5. Monitora il numero di query con un QueryCounter nei test di integrazione. Scrivi un test che verifichi che una determinata operazione non esegua più di N query.

L'N+1 problem è il killer silenzioso delle applicazioni JPA. Con queste best practice puoi tenerlo sotto controllo. Se hai bisogno di un audit sulle performance della tua applicazione Java, contattaci. Noi ragioniamo sui tuoi dati, non solo sul codice.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // WEB AGENCY

Costruiamo la presenza digitale che la tua azienda merita.

Siti web, social, pubblicità online, e-commerce e hosting performante: ingegnerizzati con metodo da ingegneri informatici a Sciacca, per tutta Italia.

> MW_JOURNAL

> READ_ALL()