
CORSO JPA JAVA PERSISTENCE API #2 | Entità e transizioni di stato
Una volta definite le entità vediamo come sono messe in relazione con gli oggetti del database attraverso l'ORM (Object Relational Mapping).
In questo nuovo articolo ci occupiamo degli stati di una entity e delle transizioni di stato che è possibile effettuare, analizzando in particolare i metodi dell'Entity Manager necessari per effettuare questi passaggi così da comprendere effettivamente come funziona l'interazione tra Java e i database.
Ci focalizzeremo ora sugli aspetti teorici per poi passare agli esempi pratici di codice.
In primo luogo riepiloghiamo quanto visto in dettaglio nel precedente articolo.
Una classe entity è di fatto una classe annotata con @Entity (e al cui interno possono esserci ulteriori annotazioni) che corrisponde ad una tabella del database.
Un'istanza di tale classe (oggetto) invece corrisponde ad una riga della tabella.
Prima di tutto in ogni istante possono assumere uno dei seguenti stati:
- transient (o new)
- managed
- detached
- removed.
Partiamo dal primo stato ovvero new o transient (transiente) che è tipico di una nuova entità (che per semplicità chiameremo così anche se ad essere precisi si tratta di una istanza della classe entità).
Questo significa che si tratta di un semplice oggetto Java, manipolabile come tutti gli altri ma completamente scorrelato dal database.
Quindi i dati non saranno salvati in modo permanente e l'entità non sarà minimamente gestita da JPA.
Affinchè quest'ultimo ne prenda il controllo l'entità deve passare allo stato managed (gestito) richiamando il metodo persist(entity) dell'Entity Manager, che di fatto è un'interfaccia con cui è possibile implementare il concetto di persistence context, un'area di stage nella quale sono presenti tutte quelle entità gestite da JPA e che saranno sincronizzate con il database.
Da questo momento in poi tutte le modifiche effettuate saranno rese persistenti all'interno del db.
Bisogna sottolineare che la sincronizzazione e quindi la persistenza non avviene contestualmente alle modifiche, ma quando viene richiamato il metodo flush().
Quindi, sebbene l'entità sia sotto il controllo di JPA, un'eventuale crash dell'applicazione prima di tale operazione porterebbe alla perdita dei dati.
Vedremo in seguito che il metodo flush() viene richiamato implicitamente da altre operazioni, anche se è possibile farlo esplicitamente.
Dallo stato managed l'entità può passare in quello detached, ovvero non è più gestita da JPA, è del tutto scollegata e qualsiasi sua modifica non sarà resa persistente sul database.
Di fatto torna ad essere una comune istanza di una classe Java senza alcuna sincronizzazione con il database.
Quando un'entità viene cancellata dal database ma esiste ancora in forma di oggetto si dice che è in uno stato removed.
Questo passaggio di stato può avvenire per vari motivi: possiamo decidere volontariamente di non far più gestire l'entità da JPA oppure può essere il risultato dell'esecuzione dei metodi clear() e close().
Possiamo chiudere l'Entity Manager per cui tutte le istanze al suo interno passano nello stato detached e quindi continuano ad essere dei semplici oggetti Java non più gestiti da JPA e il cui stato non è più salvato all'interno del database.
In alternativa si può effettuare un clear() che ripulisce il contenuto del persistence context senza però chiudere l'Entity Manager.
Tuttavia un'istanza detached può essere riportata nuovamente nello stato managed utilizzando il metodo merge(entity).
Una delle domande più frequenti durante i colloqui di lavoro è proprio la differenza tra persist(entity) e merge(entity).
Il primo si utilizza quando abbiamo una nuova istanza di un'entità e vogliamo gestirla tramite JPA, per cui l'ORM andrà a creare una opportuna istruzione SQL INSERT per aggiornare il database ed inserire una nuova riga.
Il merge(entity) invece "ricollega" l'istanza dell'entità al persistence context, quindi il metodo flush() (per sincronizzare il database) genererà un'istruzione SQL UPDATE per aggiornare i dati.
L'ultimo stato è il removed e serve per marcare quelle istanze che vogliamo rimuovere dal database, per le quali verranno generate delle istruzioni SQL DELETE che andranno a cancellare quelle righe dal database.
Questo passaggio di stato viene fatto attraverso la remove(entity) ovviamente specificando la entity coinvolta come negli altri casi.
Tuttavia occorre evidenziare che si tratta di una marcatura per la cancellazione, ovvero possiamo ripristinare lo stato managed dell'entity ricorrendo al metodo persist(entity).
Questo per motivi di sicurezza nel senso che potremmo aver impostato la cancellazione per errore, per una svista e se l'operazione avvenisse immediatamente potremmo perdere dei dati preziosi.
Anche in questo caso l'operazione sarà resa effettiva a seguito di un comando flush().
Ci sono ulteriori metodi dell'Entity Manager che servono ai fini del recupero dei dati dal database.
Ad esempio la find() permette di individuare una riga specifica attraverso la sua chiave primaria specificando la classe Java su cui deve essere mappata.
In alternativa possiamo recuperare più righe in base a delle condizioni, la classica istruzione SQL SELECT, utilizzando il metodo createQuery() con cui si può ottenere un singolo risultato o una lista di risultati.
Bisogna fare attenzione al fatto che gli oggetti mappati in questo caso sono nello stato managed e non new perchè in quest'ultimo caso JPA andrebbe a generare delle istruzioni di INSERT (di fatto duplicando il tutto) in fase di sincronizzazione. Al contrario i dati sono già presenti e al massimo possono essere aggiornati o cancellati.
In conclusione vediamo alcune varianti relative a Hibernate.
Ricordiamoci sempre che JPA è la specifica e Hibernate è una delle possibili implementazioni che fanno il lavoro sporco.
Se usiamo i metodi di JPA di fatto possiamo sostituire il provider (l'implementazione, es. Hibernate) senza particolari problemi, a patto che quest'ultimo sia del tutto compliant con la specifica.
Hibernate affianca ai metodi standard di JPA altri "proprietari" che fanno più o meno la stessa cosa.
Sono presenti semplicemente perchè il prodotto è nato ben prima che venissero scritte le specifiche a cui lo stesso si è adeguato, pur affiancando i propri metodi.
Il passaggio da new a managed avviene utilizzando persist(entity) (JPA) o save(entity) (Hibernate).
Nel secondo caso (e così per tutti gli altri metodi specifici che vedremo) se cambiamo il provider, il codice non funzionerà.
Il passaggio da managed a detached è gestito da evict(entity), manca il detach(entity).
Da detached a managed abbiamo merge(entity) (JPA) e update(entity) (Hibernate).
Da managed a removed abbiamo il delete(entity) ma non è presente il remove(entity). E per l'operazione inversa il save(entity) (Hibernate) si aggiunge al persist(entity).
Il flush() è lo stesso già visto con JPA.
Ci sono invece alcune varianti per i metodi utilizzati in fase di recupero dei dati dal database, ovvero qualche metodo in più che vedremo in dettaglio nei prossimi articoli.
Ricapitolando: se utilizziamo le specifiche JPA il codice funzionerà dappertutto, se utilizziamo metodi specifici di Hibernate con molta probabilità il codice andrà modificato in base al contesto in cui ci si trova.
Infine accenniamo alle differenze tra i metodi presenti in Hibernate: ad esempio visto che abbiamo persist(entity) e save(entity) che differenza c'è tra i due?
In generale, fermo restando la finalità dei metodi, ci possono essere delle differenze in termini di implementazione, ovvero del modo in cui vengono costruite le istruzioni SQL che verranno poi eseguite sul database e di conseguenza differenze dal punto di vista delle prestazioni.


