Crad's .NET Blog

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

[NHibernate] Mapping di un composite-id

Alle volte (anzi, molto spesso) nel realizzare la nostra applicazione non abbiamo tutta la libertà di questo mondo, magari perché lo schema DB ci viene "imposto" da un'altra figura professionale. In questi casi non è raro trovarsi ad avere a che fare con chiavi primarie composte, che si estendono cioé su più colonne. Con NHibernate possiamo gestire egregiamente anche situazioni di questo tipo, anche se è bene sottolineare che le cose si complicano un pochino quindi, nel caso in cui si debba partire da zero, è sempre meglio utilizzare la solita surrogate key, semplice, invariante e priva di significato.

Bene, supponiamo di avere il seguente schema:

concentriamoci per il momento esclusivamente sull'oggetto fattura. In prima approssimazione, una sua semplice rappresentazione nel domain model potrebbe essere del tipo:

NHibernate, come sappiamo, identifica gli oggetti grazie al loro identityField. Affinché tutto continui a funzionare anche nel caso in cui esso non sia uno scalare, ma un tipo complesso, è necessario effettuare l'override dei metodi Equals() e GetHashCode(); in caso contrario, infatti, l'engine di persistenza non sarebbe in grado di determinare quando due oggetti si riferiscono alla medesima riga di database (e quindi, ad es., gestire correttamente l'identity map della sessione, dato che questo altri non è che un dictionary). Allora, per isolare il concetto di id della fattura dalla fattura stessa, può essere conveniente creare una classe ad-hoc:

Una piccola nota: è di estrema importanza ridefinire correttamente anche GetHashCode(), e per correttamente intendo dire "in modo coerente a Equals", cioé che due oggetti uguali restituiscano hash uguali, mentre due oggetti diversi restituiscano hash diversi. A volte mi è capitato di vedere

public override int GetHashCode()
{
    
return base.GetHashCode();
}

Bene, sappiate che quanto scritto qui sopra è il modo migliore per far sì che subdoli bachi intervengano a turbare le vostre notti.

Torniamo a noi. Come scriviamo il mapping di un domain del genere? IdFattura per NHibernate non è una entity, bensì un component, vale a dire un value type (attenzione, non nel senso di .Net) il cui lifetime coincide con quello della entity a cui esso è collegato e che ha senso solo quando collegato alla entity stessa. Definizione e mapping sulle colonne, pertanto, si trovano all'interno del nodo class della entity Fattura:

<class name="Fattura" table="Fatture">
    <composite-id 
name="Id" class="IdFattura">
        <key-property 
name="Numero" />
        <key-property 
name="Anno" />
    <
/composite-id>
    
...
</class>

Ovviamente, per ogni nodo <key-property> ho omesso di specificare anche la relativa column, visto che è omonima della proprietà a cui si riferisce. A questo punto il gioco è fatto, se vogliamo inserire una nuova fattura possiamo scrivere

Fattura f = new Fattura();
f.Id.Numero = 5;
f.Id.Anno = 2006;
f.Cliente = "Pippo";

session.Save(f);
session.Flush();

oppure tramite questo snippet

IdFattura id = new IdFattura();
id.Numero = 5;
id.Anno = 2006;
Fattura f1 = session.Get<Fattura>(id);

NHibernate saprà recuperare l'oggetto voluto proprio come nel caso di una chiave scalare. Ah, una nota, visto qualche giorno fa son stati espressi dubbi in proposito: con un'implementazione del genere, NH non reidrata l'oggetto id, che anzi viene assegnato alla proprietà Fattura.Id; in pratica, il test dell'uguaglianza di riferimento

Debug.Assert(id == f1.Id);

NON fallisce.

Per quanto riguarda il dettaglio, si può seguire lo stesso approccio, definendo quindi una classe IdDettaglio; dato che, però, la coppia di colonne Numero e Anno identificano univocamente una fattura, perché non includere il riferimento all'istanza della fattura proprio all'interno di questo identityField, piuttosto che portarci dietro la semplice chiave? Completiamo allora il nostro domain model includendo le nuove classi relative al dettaglio e inserendo una collection di dettagli all'interno di Fattura, secondo il seguente diagramma:

Anche per IdDettaglio vale quanto detto a proposito di IdFattura, vale a dire che

  • è necessario ridefinire Equals() e GetHashCode()
  • per NH non si tratta di una nuova entity ma "solo" di un component
  • come tale, il suo lifetime è il medesimo della particolare istanza a cui si riferisce.

L'unica differenza, sta nel fatto che dobbiamo descrivere, all'interno del file di mapping, la relazione many-to-one intrinseca nella chiave; lo facciamo con il seguente xml:

<class name="Dettaglio" table="DettagliFattura">
    <composite-id 
name="Id" class="IdDettaglio">
        <key-many-to-one 
class="Fattura" name="Fattura">
            <column 
name="Numero"/>
            <column 
name="Anno"/>
        <
/key-many-to-one>
        <key-property 
name="Progressivo" />
    <
/composite-id>    
    
...
</class>

Ovviamente, non sarà necessario aggiungere un nodo <many-to-one> che punti ad una fattura a livello di entity Dettaglio, dato che questo è già incluso nella chiave ed automaticamente risolto da NHibernate. Vogliamo rendere la relazione bidirezionale? dal lato della fattura possiamo includere una bag, che a sua volta avrà una composite-key al suo interno (visto che Fattura è identificata da una composite-key):

<class name="Fattura" table="Fatture">
    
...
    
<bag name="Dettagli" generic="true" inverse="true" cascade="none" >
        <key>
            <column 
name="Numero" />
            <column 
name="Anno" />
        <
/key>
        <one-to-many 
class="Dettaglio" />
    <
/bag>
<
/class>

Un'ultima nota: queste chiavi composte, essendo chiavi naturali, non possono che essere assigned. Questo vuol dire che il valore deve essere impostato prima che esse siano agganciate ad una session e, quindi, NHibernate non può in alcun modo discriminare un oggetto transient da uno detachied; quindi possiamo scordarci tanto il

session.SaveOrUpdate();

che un qualsivoglia cascade all'interno della bag di cui sopra. In un prossimo articolo, quando parlerò del versioning, spiegherò come è possibile ovviare al problema.

La solita piccola applicazione di esempio è scaricabile a questo link.

Alla prossima!

powered by IMHO 1.3

Print | posted on mercoledì 20 settembre 2006 03:59 |

Powered by:
Powered By Subtext Powered By ASP.NET