Crad's .NET Blog

L'UGIblog di Marco De Sanctis
posts - 190, comments - 457, trackbacks - 70

[NHibernate] Inheritance mapping

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);    
}

produce una query con un join (generalizzando, con un join per ogni livello di profondità della gerarchia ad esclusione del primo):

exec sp_executesql N'SELECT motociclo0_.ID_Motociclo as ID1_0_, ....
FROM SUBCLASS_Motocicli motociclo0_ inner join 
     SUBCLASS_Veicoli motociclo0_1_ on .....'

Nel caso in cui, invece, si recuperi un generico veicolo dato un Id, la situazione si fa più complessa. Lo snippet

using (ISession session = SessionHelper.GetSession())
{
    Veicolo veicolo = session.Get<Veicolo>(id);
}

produce infatti la seguente query

exec sp_executesql N'SELECT ....colonne di 3 tabelle...., 
case 
    when veicolo0_1_.ID_Automobile is not null then 1 
    when veicolo0_2_.ID_Motociclo is not null then 2 
    when veicolo0_.ID_Veicolo is not null then 0 
end ...
FROM     SUBCLASS_Veicoli veicolo0_ left outer join 
        SUBCLASS_Automobili veicolo0_1_ on ... left outer join 
        SUBCLASS_Motocicli veicolo0_2_ on ... '

NHibernate ora è costretto ad ispezionare tutta la struttura di tabelle per capire la tipologia di oggetto che corrisponde ad una determinata riga. Purtroppo questo tipo di interrogazione produce una query le cui dimensioni aumentano molto velocemente (viene introdotto un ulteriore join per ogni entity esistente nella gerarchia, a qualsiasi livello) e quindi rischia di diventare ben presto parecchio onerosa per il DB.

Applicazione di esempio

Come per l'articolo precedente, anche questa volta ho sviluppato una piccola applicazione di esempio scaricabile a questo link. Consiglio di eseguirla con il profiler alla mano, per verificare effettivamente cosa accade sul database. Per provare le differenze tra le due soluzioni implementative, è sufficiente settare la proprietà Build Action del file di mapping voluto come Embedded Resource, avendo cura di impostare invece l'altro come Content.

powered by IMHO 1.3

Print | posted on giovedì 24 agosto 2006 05:12 |

Powered by:
Powered By Subtext Powered By ASP.NET