[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

Comments

# re: [NHibernate] Mapping di un composite-id
Gravatar ciao Marco, ottimo articolo, ho ricevuto il messaggio sugli incubi da hash ;-)

c'è ancora però qualcosa che mi sfugge: io ho un caso simile al tuo che però nonostante abbia gestito nello stesso modo non ha lo stesso esito.

io ho due tabelle CITTA e CAP, ed ovviamente per una CITTA posso avere n CAP;

CITTA
idcomune*
datavalidita*
descrizione

CAP
idcomune*
idcap*
campox

ho creato i mappings secondo indicazioni ma quando eseguo quello che ottengo è

{"Foreign key in table CAP must have same number of columns as referenced primary key in table CITTA"}
Left by Luca on 22/09/2006 14.37
# re: [NHibernate] Mapping di un composite-id
Gravatar > ciao Marco, ottimo articolo, ho ricevuto il messaggio sugli incubi da hash ;-)

ehehhehe!!

Per quanto riguarda il tuo problema, NHibernate si arrabbia per una motivazione che mi sembra abbastanza chiara: se CITTA e CAP sono in relazione parent-child, ovviamente la tabella CAP deve contenere una foreign-key sulla tabella CITTA, su questo credo non ci piova. Nel tuo caso però, ciò non avviene, dato che su CAP manca la colonna datavalidita che invece fa parte della primary key di CITTA, ergo... è errato lo schema del DB!
Left by Marco De Sanctis on 22/09/2006 14.45
# re: [NHibernate] Mapping di un composite-id
Gravatar ehm concordo pienamente, ma essendo il db legacy ed assolutamente intoccabile, mi stai dicendo che non posso usare NH ?

concettualmente però non è così errato, mettendomi nei panni di chi lo ha scritto (e preciso che io non credo nell'utilità della data di validità per i comuni!): la data di validità è utile nel caso il comune cambi nome, ma se anche domani Milano tornasse ad essere MEdiolanum, i suoi CAP sarebbero sempre 20100,20110, ecc...

perciò il vincolo sarebbe sempre codice comune e basta
IMHO
Left by Luca on 22/09/2006 14.58
# re: [NHibernate] Mapping di un composite-id
Gravatar Quella che indichi tu non è una relazione utilizzabile per il semplice fatto che non è una relazione!!
:-)

Va bene il DB legacy, ma il db sbagliato no!
Left by Marco De Sanctis on 22/09/2006 15.39
# re: [NHibernate] Mapping di un composite-id
Gravatar scusa ma non sono pienamente d'accordo : in effetti (purtroppo!) la struttura non è corretta, però a manina io posso creare una relazione tra CITTA e CAP sulla base del solo codice comune, con tanto di foreign-key

soprassedendo sul fatto che la ritengo una 'porcata' riuscirei ad andare oltre =)
Left by Luca on 22/09/2006 16.24
# re: [NHibernate] Mapping di un composite-id
Gravatar Scusa luca... forse non capisco io...
Foreign-key vuol dire che crei una relazione tra n colonne di una tabella ad UNA CHIAVE PRIMARIA di un'altra tabella (formata da esattamente n colonne). Altrimenti abbiamo inventato un modo per fare le relazioni molti a molti su un DB utilizzando solo due tabelle.

Btw... visto che il dibattito si allunga, cosa ne dici di spostarci sul forum che è un luogo più adatto?
Left by Marco De Sanctis on 22/09/2006 16.44
# re: [NHibernate] Mapping di un composite-id
Left by Luca on 22/09/2006 17.01

Leave Your Comment

Title*
Name*
Email (never displayed)
 (will show your gravatar)
Url
Comment*

Please add 3 and 2 and type the answer here:

Preview Your Comment.