Le classi persistenti sono quelle che in un'applicazione implementano le entità del problema di business (ad esempio Customer e Order in una applicazione di e-commerce). Le classi persistenti hanno, come implica il nome, istanze transienti ed istanze persistenti memorizzate nel database.
Hibernate funziona meglio se queste classi seguono alcune semplici regole, conosciute anche come il modello di programmazione dei "cari vecchi oggetti java" (in inglese e nel seguito si usa l'acronimo POJO che sta per "Plain Old Java Object").
La maggior parte delle applicazioni java richiede una classe persistente che rappresenti dei felini...
package eg; import java.util.Set; import java.util.Date; public class Cat { private Long id; // identificatore private String name; private Date birthdate; private Cat mate; private Set kittens private Color color; private char sex; private float weight; private void setId(Long id) { this.id=id; } public Long getId() { return id; } void setName(String name) { this.name = name; } public String getName() { return name; } void setMate(Cat mate) { this.mate = mate; } public Cat getMate() { return mate; } void setBirthdate(Date date) { birthdate = date; } public Date getBirthdate() { return birthdate; } void setWeight(float weight) { this.weight = weight; } public float getWeight() { return weight; } public Color getColor() { return color; } void setColor(Color color) { this.color = color; } void setKittens(Set kittens) { this.kittens = kittens; } public Set getKittens() { return kittens; } // addKitten non è richiesto da Hibernate public void addKitten(Cat kitten) { kittens.add(kitten); } void setSex(char sex) { this.sex=sex; } public char getSex() { return sex; } }
Ci sono quattro regole principali da seguire, qui:
Cat dichiara metodi di accesso per tutti i suoi campi persistenti. Molti altri strumenti di mappaggio OR rendono direttamente persistenti le variabili di istanza. Crediamo che sia molto meglio disaccoppiare questo dettaglio implementativo dal meccanismo di persistenza. Hibernate rende persistenti le proprietà nello stile dei JavaBeans, e riconosce i nomi di metodo nella forma getFoo, isFoo e setFoo.
Non è necessario che le proprietà siano dichiarate "public" - Hibernate può rendere persistenti proprietà con coppie di metodi get/ set a visibilità default, protected o private.
Cat ha un costruttore di default (senza argomenti) implicito. Tutte le classi persistenti devono avere un costruttore di default (che può non essere pubblico), in modo tale che Hibernate possa costruirle usando Constructor.newInstance().
Cat ha una proprietà chiamata id. Questa proprietà contiene il valore della chiave primaria di una tabella di database. La proprietà avrebbe potuto essere chiamata in un modo qualunque, e il suo tipo poteva essere un qualsiasi tipo primitivo, un "incapsulatore" ("wrapper") di tipi primitivi, java.lang.String o java.util.Date. (Se la vostra tabella di database preesistente ha chiavi composte, potete anche usare una classe definita da voi con le proprietà dei tipi delle colonne usate nella chiave - leggete la sezione sugli identificatori composti più avanti.)
La proprietà identificatore è opzionale. Potete farne a meno, e Hibernate terrà traccia internamente degli identificatori degli oggetti. In ogni caso, per molte applicazioni è comunque una buona (e molto popolare) decisione di progetto.
In più, alcune funzionalità sono possibili solo per classi che dichiarano una proprietà identificatore:
Aggiornamenti a cascata (vedete "oggetti del ciclo di vita")
Session.saveOrUpdate()
Vi raccomandiamo di dichiarare proprietà identificatore con nomi coerenti per le classi persistenti, e che usiate un tipo annullabile (cioè non primitivo).
Una funzionalità centrale di Hibernate, ovvero i mediatori ("proxy" in inglese), dipende dal fatto che la classe persistente sia non-final o che sia l'implementazione di un'interfaccia che dichiara tutti i suoi metodi pubblici.
Potete rendere persistenti classi final che non implementino un'interfaccia, con hibernate, ma non potrete usare i mediatori - cosa che limiterà in qualche modo le vostre opzioni per l'ottimizzazione delle prestazioni.
Anche una sottoclasse deve osservare la prima e la seconda regola. Eredita la sua proprietà di identificazione da Cat.
package eg; public class DomesticCat extends Cat { private String name; public String getName() { return name; } protected void setName(String name) { this.name=name; } }
Dovete sovrascrivere i metodi equals() e hashCode() se volete mischiare oggetti di classi persistenti (ad esempio in un Set).
Questo vale solo se questi oggetti vengono caricati in due Sessioni differenti, poiché Hibernate garantisce l'uguaglianza degli oggetti ( a == b , l'implementazione di default di equals()) solo all'interno di una singola Session!
Anche se entrambi gli oggetti a e b sono la stessa riga di database (hanno come identificatore lo stesso valore della chiave primaria), al di fuori del contesto di una particolare Session non possiamo garantire che siano la stessa istanza di oggetto Java.
La maniera più ovvia è di implementare equals()/hashCode() confrontando il valore di identificazione di entrambi gli oggetti. Se il valore è lo stesso, deve trattarsi della stessa riga di database, e quindi sono uguali (cioè se vengono entrambe aggiunte ad un Set, avremo solo un elemento al suo interno, dopo). Sfortunatamente, non possiamo usare questo approccio. Hibernate assegna valori di identificazione solo agli oggetti che sono persistenti, mentre una istanza appena creata non avrà alcun valore di identificatore! Quello che consigliamo, è di implementare equals() e hashCode() usando un concetto di chiave di uguaglianza di business.
"Chiave di uguaglianza di business" significa che il metodo equals() confronta solo le proprietà che formano la chiave di business, una chiave che identificherebbe la nostra istanza nel mondo reale (cioè una chiave candidata naturale):
public class Cat { ... public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof Cat)) return false; final Cat cat = (Cat) other; if (!getName().equals(cat.getName())) return false; if (!getBirthday().equals(cat.getBirthday())) return false; return true; } public int hashCode() { int result; result = getName().hashCode(); result = 29 * result + getBirthday().hashCode(); return result; } }
Ricordatevi che la nostra chiave candidata (in questo caso si tratta della composizione di nome e data di nascita) deve essere valida solo per una particolare operazione di confronto (magari solo in un singolo caso d'uso). Non abbiamo bisogno dei parametri di stabilità che solitamente si applicano ad una vera chiave primaria!
Una classe persistente può implementare in via opzionale l'interfaccia Lifecycle che fornisce alcuni punti di aggancio che consentono all'oggetto persistente di effettuare operazioni di inizializzazione/pulizia dopo un salvataggio o un caricamento, e prima di una cancellazione o un aggiornamento.
La classe Interceptor offre comunque una alternativa meno instrusiva, comunque.
public interface Lifecycle { public boolean onSave(Session s) throws CallbackException; (1) public boolean onUpdate(Session s) throws CallbackException; (2) public boolean onDelete(Session s) throws CallbackException; (3) public void onLoad(Session s, Serializable id); (4) }
(1) | onSave - chiamato subito prima che l'oggetto venga salvato o inserito |
(2) | onUpdate - chiamato subito prima che un oggetto venga aggiornato (quando viene passato a Session.update()) |
(3) | onDelete - chiamato subito prima che un oggetto venga cancellato |
(4) | onLoad - chiamato subito dopo che un oggetto è caricatocalled just after an object is loaded |
onSave(), onDelete() e onUpdate() possono essere usati per propagare salvataggi e cancellazioni degli oggetti dipendenti. È un'alternativa alla dichiarazione di operazioni di cascata nel file di mappaggio. onLoad() può essere usato per inizializzare proprietà dell'oggetto dal suo stato persistente. Non può essere usato per caricare oggetti dipendenti poiché l'interfaccia Session non può venire chiamata dall'interno del metodo. L'utilizzo ulteriore di onLoad(), onSave() e onUpdate() è per memorizzare un riferimento alla Session corrente per utilizzi successivi.
Notate che onUpdate() non viene chiamato ogni volta che lo stato persistente dell'oggetto viene modificato, ma solo quando l'oggetto transiente viene passato a Session.update().
Se onSave(), onUpdate() o onDelete() restituiscono true, l'operazione viene silenziosamente impedita. Se viene lanciata una CallbackException, l'operazione è proibita e l'eccezione viene restituita all'applicazione.
Notate che onSave() viene chiamata dopo che un identificatore sia assegnato all'oggetto, eccetto quando viene usata la strategia di generazione di chiavi nativa.
Se la classe persistente ha bisogno di controllare degli invarianti prima che il suo stato sia reso persistente, può implementare l'interfaccia seguente:
public interface Validatable { public void validate() throws ValidationFailure; }
L'oggetto dovrebbe lanciare una ValidationFailure se è stato violato qualche invariante. Un'istanza di Validatable non dovrebbe però cambiare il suo stato, dall'interno di validate().
A differenza dei metodi di richiamo dell'interfaccia Lifecycle, validate() potrebbe venire chiamata in momenti imprevisti. L'applicazione non dovrebbe affidarsi alle chiamate a validate() per implementare funzionalità di business.
Nel prossimo capitolo mostreremo come i mappaggi di Hibernate possano venire espressi usando un formato XML semplice e leggibile. Molti utenti di Hibernate preferiscono inserire l'informazione di mappaggio direttamente nel codice sorgente usando gli @hibernate.tags di XDoclet. Non parleremo qui di questo approccio poiché viene considerato strettamente parte di XDoclet. Tuttavia includiamo l'esempio seguente della classe Cat con i mappaggi di XDoclet.
package eg; import java.util.Set; import java.util.Date; /** * @hibernate.class * table="CATS" */ public class Cat { private Long id; // identifier private Date birthdate; private Cat mate; private Set kittens private Color color; private char sex; private float weight; /** * @hibernate.id * generator-class="native" * column="CAT_ID" */ public Long getId() { return id; } private void setId(Long id) { this.id=id; } /** * @hibernate.many-to-one * column="MATE_ID" */ public Cat getMate() { return mate; } void setMate(Cat mate) { this.mate = mate; } /** * @hibernate.property * column="BIRTH_DATE" */ public Date getBirthdate() { return birthdate; } void setBirthdate(Date date) { birthdate = date; } /** * @hibernate.property * column="WEIGHT" */ public float getWeight() { return weight; } void setWeight(float weight) { this.weight = weight; } /** * @hibernate.property * column="COLOR" * not-null="true" */ public Color getColor() { return color; } void setColor(Color color) { this.color = color; } /** * @hibernate.set * lazy="true" * order-by="BIRTH_DATE" * @hibernate.collection-key * column="PARENT_ID" * @hibernate.collection-one-to-many */ public Set getKittens() { return kittens; } void setKittens(Set kittens) { this.kittens = kittens; } // addKitten not needed by Hibernate public void addKitten(Cat kitten) { kittens.add(kitten); } /** * @hibernate.property * column="SEX" * not-null="true" * update="false" */ public char getSex() { return sex; } void setSex(char sex) { this.sex=sex; } }