Pubblicato il 12 marzo 2013 da Alessio Stalla

Nella prima parte di questo articolo, abbiamo esaminato alcuni aspetti del modello dei dati di Hibernate. Seppur utile per capire più a fondo Hibernate, la prima parte non era sufficiente per raggiungere il nostro obiettivo di costruire programmaticamente una session factory Hibernate funzionante. Ora esploreremo due argomenti importanti che abbiamo tralasciato la volta scorsa: le chiavi primarie e le relazioni.

Chiavi primarie e identificatori

Con "chiave primaria", Hibernate si riferisce al lato fisico del modello; la sua controparte logica si chiama identificatore, o ID. Ricordiamo che ciascuna tabella deve avere una chiave primaria per poter essere mappata da Hibernate. Anche se generalmente nelle applicazioni moderne vengono usate ovunque chiavi sintetiche, Hibernate può mappare anche chiavi composite, pertanto nella maggior parte dei casi gli schemi legacy non hanno bisogno di adattamenti.

La distinzione tra modello fisico e logico è valida anche per le chiavi primarie, ma è più facile se guardiamo a entrambi gli aspetti allo stesso tempo. Le chiavi primarie si creano così:

PrimaryKey primaryKey = new PrimaryKey();
primaryKey.setName(pkName);
primaryKey.setTable(table);
table.setPrimaryKey(primaryKey);

Come vengono collegate al resto del modello varia a seconda che si tratti di chiavi semplici o composite.

Chiavi primarie semplici

Una chiave primaria è semplice se è costituita da una sola colonna. In questo caso, dobbiamo creare una Column e una Property come abbiamo fatto per la mappatura delle altre colonne, e aggiungere questa colonna all'oggetto PrimaryKey (primaryKey.addColumn(col)). Per convenienza, in Portofino creiamo la Column e la Property insieme alla chiave primaria (e quindi le saltiamo quando creiamo le normali colonne), ma come organizzare il vostro codice è lasciato a voi. Inoltre, la proprietà dovrebbe essere marcata come in sola lettura:

prop.setInsertable(false);
prop.setUpdateable(false);

Al SimpleValue associato alla proprietà deve essere fornita una strategia di generazione (generator strategy), vale a dire una politica per generare valori per la chiave primaria. Alcune delle opzioni possibili sono "assigned" (il valore viene fornito dall'utente), "identity" (la colonna è di tipo auto-increment, per quei database che lo supportano), "enhanced-sequence" (i valori della chiave vengono generati usando una sequenza del database), "enhanced-table" (i valori della chiave vengono generati usando un'altra tabella come generatore sequenziale). Vedi la documentazione di Hibernate per una lista completa delle opzioni disponibili, e vedi il codice sorgente di Portofino 4 per alcuni esempi di come configurarli programmaticamente. Il caso più semplice è lasciare la responsabilità di fornire l'id all'applicazione:

SimpleValue id = (SimpleValue) col.getValue();
id.setIdentifierGeneratorStrategy("assigned");

Infine, la chiave primaria appena creata deve essere registrata sulla tabella e sulla classe persistente di appartenenza:

table.setIdentifierValue(id);
clazz.setIdentifier(id);
clazz.setIdentifierProperty(prop);

Chiavi primarie composite

Le chiavi primarie composite, che sono costituite da colonne multiple e che si trovano più spesso nei database legacy, sono più complicate. Per mapparle, Hibernate richiede un oggetto extra che contenga tutte le proprietà che costituiscono l'identificatore. Spesso si usa una classe dedicata, come spiegato nella documentazione di Hibernate; tuttavia, in Portofino, riutilizziamo la classe persistente come il suo stesso identificatore. Questa scelta non è universalmente applicabile, ma nell'ambito del nostro design funziona bene e copre un ampio raggio di scenari. In ogni caso, per configurare una chiave composita, prima si deve impostare la classe persistente perché usi un identificatore embedded:

clazz.setEmbeddedIdentifier(true);

Successivamente, bisogna creare un Component che rappresenti l'identificatore composito:

Component component = new Component(mappings, persistentClass);
component.setRoleName(someUniqueName + ".id");
component.setEmbedded(true);
component.setNodeName("id");
component.setKey(true);
component.setNullValue("undefined");

Se la classe persistente è mappata su una classe Java, il componente non è dinamico e deve essere configurato così:

component.setComponentClassName(javaClassFullyQualifiedName);

altrimenti, si deve marcare il componente come dinamico:

component.setDynamic(true);

Per ogni colonna nella chiave primaria, bisogna avere una Column e una Property associata come di consueto, e in aggiunta bisogna collegarli rispettivamente alla PrimaryKey e al Component:

primaryKey.addColumn(col);
component.addProperty(prop);

Infine, bisogna registrare la chiave primaria sulla classe persistente:

table.setIdentifierValue(component);
clazz.setIdentifier(component);
clazz.setDiscriminatorValue(someUniqueName);
table.setPrimaryKey(primaryKey);

Si noti che someUniqueName è lo stesso valore usato in precedenza per impostare il role name del componente.

Relazioni

Le relazioni sono la caratteristica distintiva del modello relazionale; si può sostenere che siano l'aspetto più distante dal modello OO, e la sfida più grande che un ORM deve sostenere.

Hibernate può mappare tutti i tipi di relazioni: uno-ad-uno, uno-a-molti, molti-a-uno, molti-a-molti. In Portofino ci siamo limitati a mappare relazioni uno-a-molti in entrambe le direzioni (ossia, ogni relazione è mappata due volte, come collezione sul lato “uno”, e come una proprietà scalare sul lato “molti”). Questo schema è abbastanza potente da modellare gli altri tipi di relazioni, e parleremo solo di esso in questo articolo.

Inoltre, in Portofino usiamo solo i Bag – collezioni non ordinate che ammettono duplicati. Hibernate supporta altri tipi di collezioni come i set (che non contengono duplicati) e le liste (che hanno un ordinamento ben definito). E' banale rimpiazzare l'uso dei Bag con altri tipi di collezione se necessario.

Ognuna delle relazioni che stiamo considerano coinvolge due classi persistenti; come abbiamo detto, ci siamo limitati alle relazioni uno-a-molti, per cui usiamo i nomi classOne e classMany per chiarezza. Naturalmente, è possibile avere classOne == classMany (relazioni ricorsive). In modo analogo, ci riferiremo a oneEntityName e manyEntityName, ecc. Invece, useremo onePropertyName per indicare la proprietà sul lato “molti” (cioè in classMany) che mantiene un riferimento all'oggetto “uno”, e analogamente manyPropertyName come la proprietà sul lato “uno” che mantiene una collezione dei “molti” oggetto.

Il lato “uno”

Su questo lato della relazione manteniamo una collezione di entità collegate, quindi il primo passo è creare tale collezione (un Bag, come abbiamo visto prima):

Bag coll = new Bag(mappings, classOne);
coll.setRole(oneEntityName + "." + manyPropertyName);
coll.setNodeName(onePropertyName);
coll.setCollectionTable(classMany.getTable());
OneToMany oneToMany = new OneToMany(mappings, coll.getOwner());
coll.setElement(oneToMany);
oneToMany.setReferencedEntityName(manyEntityName);
oneToMany.setAssociatedClass(classMany);
oneToMany.setEmbedded(true);
coll.setSorted(false);

Si può controllare la modalità (lazy o eager) e la politica di fetch della collezione impostando le relative proprietà.

Il secondo passo è assegnare una chiave alla collezione.

DependantValue dv = ...;
List<Column> oneColumns = ...; //Relationship columns in tableOne
List<Column> manyColumns = ...; //Relationship columns in tableMany
tableMany.createForeignKey(
    relationship.getName(), manyColumns, oneEntityName, oneColumns);
dv.setNullable(false);
coll.setKey(dv);

Ritorneremo in seguito sul DependantValue che lega la collezione alla o alle colonne chiave sulla tabella nel lato "uno".

Una volta che abbiamo una collezione configurata in modo appropriato, bisogna registrarla e creare un proprietà per essa.

mappings.addCollection(set);
Property prop = new Property();
prop.setName(manyPropertyName);
prop.setNodeName(manyPropertyName);
prop.setValue(set);
classOne.addProperty(prop);

Si può controllare la politica di cascade settando prop.cascade appropriatamente.

Vediamo brevemente come si può creare il DependantValue che abbiamo menzionato in precedenza. Si tratta di un altro tipo di Value il cui tipo è determinato da un altro valore (per esempio, una chiave esterna è tipata secondo la chiave primaria a cui si riferisce). Come per le chiavi primarie, il valore è configurato in modo diverso a seconda che la chiave sia singola o composita.

Se la chiave è semplice, si riferisce ad una sola colonna e pertanto ad una sola proprietà; chiamiamo quest'ultima refProp. Il DependantValue si crea semplicemente così:

dv = new DependantValue(
    mappings, classMany.getTable(), refProp.getPersistentClass().getKey());
dv.setNullable(true);
dv.setUpdateable(true);
for(Column col : manyColumns) {
    dv.addColumn(col);
}

Se la chiave è composita, come nel caso della chiave primaria, serve un Component intermedio per modellarla.

Component component = new Component(mappings, collection);
component.setDynamic(manyEntityJavaClass == null);
component.setEmbedded(true);
dv = new DependantValue(mappings, classMany.getTable(), component);
for(Property refProp : oneProperties) {
    component.addProperty(refProp);
}
dv.setNullable(true);
dv.setUpdateable(true);
for(Column col : manyColumns) {
    dv.addColumn(col);
}

Questo è quanto.

Il lato “molti”

Su questo lato manteniamo un riferimento indietro all'oggetto “uno”, che è concettualmente il proprietario della relazione. La configurazione in questo caso è più facile di quanto abbiamo visto prima. Continuiamo ad usare gli stessi nomi di variabile del caso “uno”.

In prima battuta, creiamo una ManyToOne:

ManyToOne m2o = new ManyToOne(mappings, tableMany);
m2o.setLazy(lazy);
m2o.setReferencedEntityName(oneEntityName);
m2o.createPropertyRefConstraints(Collections.singletonMap(oneEntityName, classOne));
for(Column col : manyColumns) {
    m2o.addColumn(col);
}

Poi, aggiungiamo una Property alla classe persistente:

Property prop = new Property();
prop.setName(onePropertyName);
prop.setNodeName(onePropertyName);
prop.setValue(m2o);
prop.setCascade(...);
prop.setInsertable(false);
prop.setUpdateable(false);
classMany.addProperty(prop);

E abbiamo finito.

Conclusioni

Partendo dalla nostra esperienza pratica con Portofino, abbiamo dato una panoramica del modello dei dati di Hibernate. Sebbene questo non sia un manuale di riferimento ed abbiamo sorvolato su alcuni dettagli, speriamo che sarete in grado di adattare quanto abbiamo mostrato ai vostri casi d'uso, ed utilizzarlo come base per ulteriori esplorazioni. Crediamo che questo argomento, seppur non propriamente popolare, non fosse adeguatamente rappresentato nella documentazione e negli articoli esistenti.

 

comments powered by Disqus