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