Salta al contenuto principale

Le Entity in JPA: la rappresentazione a oggetti del database

Profile picture for user luca77king

Nel cuore dell’architettura JPA (Java Persistence API) si trovano le entità, le classi che rappresentano le tabelle del database e che permettono di lavorare con i dati in modo completamente orientato agli oggetti. Comprendere come funzionano e come si definiscono è essenziale per sfruttare appieno la potenza di JPA e di un provider come Hibernate. In questa guida esploreremo in modo approfondito cos’è un’entità, come si mappa su una tabella, come si gestisce la chiave primaria con le varie strategie di generazione automatica e perché la scelta tra tipi primitivi e oggetti wrapper è molto più importante di quanto possa sembrare.

Cos’è un’entità in JPA

Un’entità è una classe Java POJO (Plain Old Java Object) che rappresenta una riga di una tabella del database. Ogni istanza della classe corrisponde a un record, e ogni campo della classe corrisponde a una colonna della tabella. Per essere riconosciuta come entità da JPA, una classe deve essere annotata con @Entity.

Ecco un esempio base di definizione di entità:

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
}

In questo esempio, la classe User rappresenta una tabella chiamata (per default) “user”. L’annotazione @Entity indica a JPA che questa classe deve essere gestita come un’entità persistente, e che quindi può essere salvata, aggiornata o eliminata dal database tramite un EntityManager.

Se si desidera personalizzare il nome della tabella, è possibile utilizzare l’annotazione @Table:

@Entity
@Table(name = "users")
public class User { ... }

In questo modo, la tabella associata sarà chiamata users, mantenendo però invariata la logica interna dell’entità.

La chiave primaria: il cuore dell’entità

Ogni entità deve possedere un campo identificativo univoco, la chiave primaria, che serve a distinguere un record da un altro. In JPA, la chiave primaria è definita tramite l’annotazione @Id.

Nel nostro esempio, il campo id è la chiave primaria. Tuttavia, dichiarare un campo come chiave non basta: spesso si desidera che il valore sia generato automaticamente dal database o dal provider JPA. Per questo esiste l’annotazione @GeneratedValue, che può essere configurata con diverse strategie di generazione.

Le strategie di generazione della chiave primaria

L’annotazione @GeneratedValue specifica come viene assegnato il valore del campo identificativo al momento della persistenza dell’entità. Esistono varie strategie, definite dall’enum GenerationType, che permettono di adattare il comportamento di JPA ai diversi database o scenari applicativi.

GenerationType.IDENTITY

È la strategia più comune con database come MySQL o PostgreSQL. Il valore della chiave primaria viene generato dal database stesso, solitamente tramite una colonna con auto-increment. Quando si inserisce una nuova entità, Hibernate invia l’istruzione SQL e lascia che il database assegni automaticamente l’ID.
Esempio:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Questa modalità è molto semplice e diretta, ma comporta che l’inserimento dell’entità richieda immediatamente un’operazione di scrittura sul database per ottenere il valore generato.

GenerationType.SEQUENCE

In questa modalità, la generazione della chiave si basa su una sequenza del database, tipica di sistemi come Oracle o PostgreSQL. Hibernate utilizza una sequenza configurata per ottenere nuovi valori di ID in modo efficiente.
Esempio:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;

È anche possibile definire una sequenza personalizzata con l’annotazione @SequenceGenerator, specificando il nome della sequenza e il valore di incremento.

GenerationType.AUTO

È la modalità predefinita. Hibernate sceglie automaticamente la strategia migliore in base al database utilizzato. In genere utilizza IDENTITY per MySQL, SEQUENCE per Oracle e PostgreSQL, o TABLE per altri database.
Esempio:

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

È una soluzione comoda perché permette al codice di rimanere indipendente dal database, ma in ambienti di produzione è spesso preferibile specificare esplicitamente la strategia per evitare comportamenti imprevisti.

GenerationType.TABLE

Questa strategia, meno utilizzata, memorizza i valori delle chiavi primarie in una tabella dedicata di supporto, gestita da Hibernate. È utile nei database che non supportano sequenze o auto-incrementi, ma ha performance inferiori rispetto alle altre modalità.

Esempio completo di entità con strategie di chiave

Ecco un esempio di entità che utilizza una sequenza personalizzata per la generazione degli ID:

import jakarta.persistence.*;

@Entity
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence", allocationSize = 1)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
    private Long id;

    private String name;
    private String email;
}

In questo caso, viene definita una sequenza chiamata user_sequence nel database, che genera valori incrementali utilizzati da Hibernate ogni volta che viene persistita una nuova entità User. L’attributo allocationSize controlla di quanti valori aumentare la sequenza ad ogni richiesta.

Primitive e wrapper: una scelta non banale

Quando si definiscono i campi di un’entità, una delle decisioni più importanti riguarda l’uso dei tipi primitivi o degli oggetti wrapper. A prima vista potrebbe sembrare una differenza stilistica, ma in realtà ha implicazioni profonde sul comportamento dell’applicazione.

I tipi primitivi come int, long, double, boolean non possono assumere il valore null. Questo significa che se si definisce un campo come private long id;, il valore di default sarà sempre 0, anche prima della persistenza. Tuttavia, nella logica di JPA, un valore 0 può creare ambiguità, perché JPA considera un’entità “nuova” se l’ID è null, non se è 0.

Gli oggetti wrapper (Long, Integer, Double, Boolean) risolvono questo problema, poiché possono essere nulli. Per questo motivo, è buona pratica utilizzare wrapper per i campi che rappresentano chiavi primarie o valori che possono non essere inizializzati.

Esempio:

private Long id; // consigliato per @Id
private Integer age; // può essere nullo
private boolean active; // primitivo, utile per valori booleani di default

La regola generale è: usa i tipi wrapper per i campi che possono essere assenti o non ancora definiti, mentre usa i primitivi quando vuoi garantire la presenza di un valore predefinito.

Ciclo di vita di un’entità

Un’entità JPA attraversa diversi stati nel suo ciclo di vita:

  • New (Transient): l’oggetto è appena creato e non è ancora gestito da JPA;

  • Managed: l’entità è associata a un EntityManager e le modifiche sono tracciate;

  • Detached: l’entità è stata scollegata dal persistence context;

  • Removed: l’entità è segnata per la rimozione dal database.

Questo ciclo è gestito automaticamente da JPA, ma conoscere i suoi stati è fondamentale per evitare errori, soprattutto quando si lavora con chiavi generate automaticamente.

Un esempio pratico di salvataggio

Vediamo un esempio completo in cui un’entità viene creata e salvata nel database:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPU");
EntityManager em = emf.createEntityManager();

em.getTransaction().begin();

User user = new User();
user.setName("Luca");
user.setEmail("luca@example.com");

em.persist(user);

em.getTransaction().commit();

em.close();
emf.close();

In questo esempio, al momento della chiamata em.persist(user), Hibernate genera l’ID automaticamente in base alla strategia definita, e il record viene salvato nella tabella associata all’entità User. Dopo il commit, l’entità è sincronizzata con il database e il suo ID è valorizzato.

Conclusione

Le entità sono la base su cui si costruisce l’intero ecosistema di JPA. Ogni classe annotata con @Entity diventa una rappresentazione diretta di una tabella, trasformando il modo in cui si interagisce con i dati: non più query SQL, ma oggetti e metodi. Capire come definire correttamente una chiave primaria, scegliere la giusta strategia di generazione e distinguere tra tipi primitivi e wrapper è fondamentale per evitare errori logici e comportamenti imprevedibili.

Una configurazione pulita e coerente delle entità rende il codice più leggibile, più manutenibile e soprattutto più vicino al dominio applicativo reale. Quando la tua entità prende vita, si mappa correttamente e i dati scorrono fluidi tra oggetti e tabelle, il risultato è chiaro: daje.