Ereditarietà e polimorfismo sono due caratteristiche
della programmazione OO estremamente potenti, che possono essere sfruttate anche
nella costruzione di domini complessi. La persistenza su un DBMS di queste
strutture, però, pone dei problemi, in quanto non sono direttamente mappabili su
un modello ER.
Supponiamo di dover persistere su DBMS il seguente dominio:
Come realizzare lo schema del DB? Beh, una trattazione completa
dell'argomento è decisamente eccessiva, per cui si rimanda ad altre fonti, a noi
basta semplicemente sapere che le scelte più usate ricadono tra:
- Table per class hierarchy, in cui una sola tabella contiene una colonna per ogni
attributo di qualsiasi classe della gerarchia, e quindi è capace di contenere
tutta la gerarchia di oggetti; i diversi tipi sono distinti dal valore di una
particolare colonna, chiamata discriminatore;
- Table per subclass, in cui si crea una tabella per ogni oggetto di qualsiasi
livello, le tabelle degli oggetti figlio contengono una FK verso le tabelle
relative agli oggetti padre;
- Table per concrete class, in cui si crea una tabella per
ogni oggetto figlio, contenente tutte le proprietà dello stesso e dell'oggetto
padre.
Il terzo caso pone anche alcune problematiche nel caso in cui esistano delle
reference (da tradurre in relazioni su DB) tra altre entity e un oggetto padre
(es. Persona -> Veicolo posseduto: sulla tabella Persone che codice metto?
ID_Motociclo? ID_Automobile? entrambi?); inoltre, non è direttamente mappabile
in NHibernate (lo è su Hibernate per Java dalla versione 3 in poi, credo) e per
questa ragione lo trascureremo. Ci concentreremo, allora, sui primi due
casi.
Table per class hierarchy
In questo caso è necessario utilizzare nel file di mapping il nodo
Discriminator, che accetta un attributo Column per indicare il nome
della colonna utilizzata per differenziare i vari tipi. La classe padre (e le
classi figlio, mappate tramite l'elemento Subclass), presentano un
attributo che indica il valore del discriminatore associato al tipo stesso:
<class name="NHSubclassTest.Veicolo, NHSubclassTest"
table="CLASS_Veicoli">
<!-- ... -->
<discriminator column="TipoVeicolo" />
<!-- properties di Veicolo -->
<subclass name="NHSubclassTest.Automobile, NHSubclassTest"
discriminator-value="A">
<!-- properties di Automobile -->
</subclass>
<subclass name="NHSubclassTest.Motociclo, NHSubclassTest"
discriminator-value="M">
<!-- properties di Motociclo -->
</subclass>
</class>
Come si può notare, ovviamente, il nome della tabella è specificato
solo nella classe padre. Proviamo ad eseguire del codice e vediamo come reagisce
NHibernate. Supponiamo di voler memorizzare una entity:
using (ISession session = SessionHelper.GetSession())
{
Automobile auto = new Automobile();
// valorizzo le properties di auto
session.SaveOrUpdate(auto);
session.Flush();
}
Come è lecito aspettarsi, NH produrrà una query di inserimento sul DB con il
valore del discriminator impostato ad "A", dato che stiamo persistendo
un'istanza del tipo Automobile, e con i campi specifici di Motociclo tutti pari
a NULL:
exec sp_executesql N'INSERT INTO CLASS_Veicoli (Furgonato, NumeroPorte, Modello,
Marca, Cilindrata, TipoVeicolo, ID_Veicolo) VALUES (@p0, @p1, @p2, @p3, @p4, ''A'',
@p5)',N'@p0 bit,@p1 int,@p2 nvarchar(4000),@p3 nvarchar(4000),@p4 int,@p5
uniqueidentifier',@p0=1,@p1=3,@p2=N'Punto',@p3=N'Fiat',@p4=1600,
@p5='D96E2365-6E84-4E75-9712-003E5EEADA9C'
Una caratteristica interessante è che possiamo caricare con una Get un
generico oggetto di tipo Veicolo, penserà poi NHibernate a costruire, a
seconda del discriminator, il tipo concreto corretto. Stesso dicasi se vogliamo
recuperare un elenco di veicoli presenti nel database: il metodo ICriteria.List
restituirà una IList<Veicolo> con all'interno i tipi concreti memorizzati
nel database:
using (ISession session = SessionHelper.GetSession())
{
// recupero un elenco di veicoli
IList<Veicolo> lista = session
.CreateCriteria(typeof(Veicolo))
.List<Veicolo>();
foreach (Veicolo v in lista)
{
Console.WriteLine("Veicolo n.{0}, tipo {1}",
lista.IndexOf(v), v.GetType().Name);
}
}
using (ISession session = SessionHelper.GetSession())
{
// corretto caricamento delle istanze
// uso una session differente per evitare il caching di primo livello
Veicolo veicolo = session.Get<Veicolo>(id);
Debug.Assert(veicolo != null,
"Grazie al polimorfismo, recupero una generica istanza di veicolo");
}
Lo svantaggio di questo approccio è quello di dover creare una tabella
su DB per sua natura un po' "sporca", visto che effettivamente memorizza
informazioni eterogenee che richiederanno, di volta in volta, di valorizzare
alcune colonne piuttosto che altre.
Table per subclass
Per utilizzare il secondo approccio, è necessario modificare leggermente il
mapping del dominio, utilizzando l'elemento joined-subclass in luogo
del precedente:
<class name="NHSubclassTest.Veicolo, NHSubclassTest"
table="SUBCLASS_Veicoli">
<!-- proprietà di Veicolo -->
<joined-subclass name="NHSubclassTest.Automobile, NHSubclassTest"
table="SUBCLASS_Automobili">
<key column="ID_Automobile" />
<property name="Furgonato" />
<property name="NumeroPorte" />
</joined-subclass>
<joined-subclass name="NHSubclassTest.Motociclo, NHSubclassTest"
table="SUBCLASS_Motocicli">
<key column="ID_Motociclo" />
<property name="Bauletto" />
<property name="TipoForcella" />
</joined-subclass>
</class>
NHibernate persisterà le informazioni separatamente, inserendo i
valori relativi alle proprietà di ogni classe della gerarchia all'interno della
tabella assegnata ed avendo cura che le chiavi primarie coincidano. Lo snippet
di codice di creazione di un automobile esposto in precedenza, questa volta
produce le seguenti query su DB:
exec sp_executesql N'INSERT INTO SUBCLASS_Veicoli ....'
exec sp_executesql N'INSERT INTO SUBCLASS_Automobili .....'
Come si può notare, questa volta la memorizzazione (e lo
stesso dicasi per l'eliminazione e per le
modifiche che coinvolgono proprietà di entrambi i livelli)
richiede l'esecuzione di due query su database, numero che
aumenta di pari passo con la profondità della gerarchia.
L'esecuzione di una Get produce invece un output differente, a seconda che si
tratti della ricerca di una classe concreta o di una astratta. Se infatti
richiediamo una istanza di Motociclo, infatti, NHibernate sa già che per
recuperare tutte le informazioni dovrà effettuare un join tra la tabella dei
Veicoli e quella dei Motocicli; quindi, lo snippet seguente
using (ISession session = SessionHelper.GetSession())
{
Motociclo moto = session.Get<Motociclo>(myId);
}