Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

NHibernate, interrogare il database con HQL

Un linguaggio molto simile a SQL per estrarre i dati mappati dall'OR/M sul database
Un linguaggio molto simile a SQL per estrarre i dati mappati dall'OR/M sul database
Link copiato negli appunti

Un ORM implementa solitamente il pattern Repository, il cui scopo è schermare l'utilizzatore dal reale supporto fisico delle istanze. Un repository viene percepito dall'esterno come il gestore della memorizzazione degli oggetti e non è assolutamente necessario preoccuparsi di come questa memorizzazione venga realmente ottenuta.

Le operazioni base di un repository dal punto di vista logico sono le solite CRUD e tra queste l'operazione di Read è indubbiamente la più impegnativa, perché si ha la necessità di fornire all'utilizzatore una sintassi con cui specificare condizioni complesse sugli oggetti da recuperare. Raramente infatti si ha la necessità di caricare tutti gli oggetti di un certo tipo, oppure una singola istanza data la sua chiave, più spesso si deve recuperare un sottoinsieme di oggetti che soddisfi una qualche condizione.

Mentre nel linguaggio SQL la sintassi per le query prevede solo un formato testuale, un repository permette di specificare condizioni direttamente sulle proprietà degli oggetti, solitamente tramite interfacce differenti. Nel caso di NHibernate ad esempio possiamo utilizzare:

  • HQL (Hibernate Query Language)
  • Criteria Query
  • Query By Example

e in cantiere vi è il supporto a LINQ per il framework 3.5.

HQL

Tra tutte, HQL è la tecnica più immediata, perché si avvicina molto al linguaggio SQL, presentando dunque una minore difficoltà di apprendimento. Il vantaggio di usare HQL sta nel fatto che la query viene espressa sul Domain Model e non sul modello fisico di memorizzazione.

Figura 1. Propagazione dell'interrogazione
Propagazione dell'interrogazione

Grazie a HQL si possono esprimere query come: «Seleziona tutti i clienti che hanno la proprietà Nome pari a 'Gian Maria'», in questo modo si crea un livello di astrazione tra la logica applicativa (costituita da oggetti) ed il supporto di memorizzazione (db transazionale). Questa separazione è resa possibile dall'ORM, che si occupa di tradurre le richieste in un formato compatibile con il supporto di memorizzazione dei dati, trasformando poi i risultati dell'interrogazione in oggetti.

La query precedente viene espressa in HQL in questo modo:

Select c from Customer c where c.Name = :param

Come anticipato, le condizioni vengono direttamente imposte sull'oggetto Customer richiedendo che la sua proprietà nome sia pari al valore :param. All'entità Customer viene assegnato l'alias c, che deve essere poi usato in tutto il resto della query. Il parametro è costituito invece da un identificatore preceduto dai due punti in stile Oracle.

Nell'Esempio1, in allegato, viene mostrato come eseguire questa query per recuperare tutti i clienti che si chiamano Gian Maria:

IQuery query = session
	.CreateQuery("select c from Customer c where c.Name = :param")
	.SetString("param", "Gian Maria");

IList<Customer> result = query.List<Customer>();

La funzione da utilizzare è la Session.CreateQuery() alla quale si passa semplicemente la stringa HQL che rappresenta la query da eseguire, successivamente si valorizzano i parametri con le funzioni SetXXX dove XXX è il tipo del parametro. A questo punto i risultati vengono recuperati invocando il metodo List, che restituisce una IList contenente tutti gli oggetti che soddisfano il criterio indicato.

Dal trace si può verificare la query realmente eseguita.

select customer0_.id as id0_, customer0_.custName as custName0_, customer0_.custSurname as custSurn3_0_ 
from dbo.AnagraficCustomer customer0_
where (customer0_.custName=@p0 ); @p0 = 'Gian Maria'

Il nome della tabella e dei campi utilizzati non sono uguali a quelli dell'oggetto, questo può accadere con database Legacy, oppure quando ci sono politiche stringenti sui nomi degli oggetti da usare nel DB. Grazie a nhibernate le query vengono però espresse sulla base delle reali proprietà dell'oggetto ignorando la reale struttura fisica dei dati.

Componenti e mapping di tipi complessi

Quando si utilizza un ORM la struttura tabellare non deve obbligatoriamente riflettere la struttura del modello ad oggetti; se si osserva l'oggetto Customer2 dell'esempio allegato, la cui struttura è mostrata in figura, si può notare che esso contiene una proprietà chiamata Address di tipo Address.

Figura 2. Class diagram Customer2-Address
Class diagram Customer2-Address

Nella modellazione object oriented è infatti uso comune creare una classe per ogni entità logica. Dato che non solo i Customer possono avere un indirizzo, è prassi standard creare un oggetto Address che può essere utilizzato da più classi.

L'oggetto Address nella logica di dominio è però molto differente da Customer, perché viene considerato come value type(da non confondere con i value type di .NET); ovvero concettualmente non ha un identità propria. Un indirizzo preso da solo non ha infatti molto senso in un ipotetico software gestionale, ma assume un significato solo quando parte di un altro oggetto, come un cliente. Questo significa che il ciclo di vita di un indirizzo è lo stesso dell'entità che lo contiene, se cancelliamo un cliente verrà automaticamente cancellato anche il suo indirizzo.

Il concetto di Value Type nella progettazione basata su Domain Model è importantissimo e dipende esclusivamente dal contesto di applicazione. Ad esempio se si scrivesse un software per la gestione delle spedizioni di un corriere, l'oggetto Address potrebbe invece essere una entità vera e propria e possedere il proprio ciclo di vita.

Fatta questa premessa si potrebbe avere la tentazione di creare una tabella Address nel database e collegarla con una foreign key alla tabella clienti, ma questo è concettualmente sbagliato; cosa succede infatti se in futuro si ha la necessità di creare la tabella fornitori ed ogni fornitore necessita anch'egli di un indirizzo? Far puntare la foreign key della tabella Address anche alla tabella fornitori è una pratica decisamente pessima, perché in questo caso poi non si può più sapere se la foreign key punta ad un cliente o ad un fornitore. Inoltre se le due tabelle non hanno tra di loro indici univoci, si finisce per farsi molto male.

In questa situazione, qualsiasi DBA concorderà che la soluzione più logica è quella di inserire i campi dell'indirizzo direttamente nelle rispettive tabelle (clienti e fornitori), in sostanza la tabella clienti assume la struttura rappresentata in figura.

Figura 3. Tabella clienti
Tabella clienti

Il mapping relativo è il seguente:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" 
                   assembly="Domain" namespace="Domain.Entities">

  <class name="Customer2" table="AnagraficCustomer2" lazy="true" dynamic-update="true">
    <id name="id" unsaved-value="0" access="field" type="System.Int32">
      <generator class="native" />
    </id>
    
    <property name="Name" column="custName" type="System.String" length="40" not-null="true"/>
    <property name="Surname" column="custSurname" type="System.String"  length="40" not-null="true"/>
    <component name="Address" class="Address">
      <property name="Street" column="custStreet" type="System.String"  length="60"/>
      <property name="City" column="custCity" type="System.String"  length="20" />
      <property name="ZipCode" column="custZipCode" type="System.String"  length="5" />
      <property name="Number" column="custNumber" type="System.Int32" />
    </component>
  </class>
</hibernate-mapping>

La novità rispetto ai mapping visti fino ad ora è che l'indirizzo viene mappato come component. In questo modo stiamo indicando a NHibernate che l'oggetto Customer2 ha una proprietà complessa di tipo Address, ma in realtà essa è un componente della classe, risiede quindi nella stessa tabella e di conseguenza ne condivide il ciclo di vita. L'aspetto più importante è che la classe Address non ha una proprietà id, non possiede un identità definita ed è a tutti gli effetti parte di un altro oggetto.

Nel listato2 osseviamo osservare come l'oggetto Customer2 possa essere gestito da nhibernate in maniera completamente trasparente e, nonostante sia effettivamente composto da due oggetti (Customer2 più Address) viene eseguita una sola insert.

Nel listato3 viene fatto un test di salvataggio di un oggetto Customer2 che non possiede un indirizzo, in questo caso nhibernate imposta tutti i campi relativi con valore nullo.

HQL su tipi complessi

Ora che si ha un oggetto con un mapping più complesso, si può apprezzare maggiormente il concetto di HQL e di query sul modello ad oggetti. Nel listato4 viene infatti eseguita la query

"select c from Customer2 c where c.Address.City like :param"

Come si può notare la query utilizza la clausola where c.Address.City like che indica un criterio sulla proprietà City dell'oggetto Address, NHibernate grazie al mapping si occupa poi di tradurla nella query

select customer2x0_.id as id1_, customer2x0_.custName as custName1_, customer2x0_.custSurname as custSurn3_1_, customer2x0_.custStreet as custStreet1_, customer2x0_.custCity as custCity1_, customer2x0_.custZipCode as custZipC6_1_, customer2x0_.custNumber as custNumber1_ 
from dbo.AnagraficCustomer2 customer2x0_ 
where (customer2x0_.custCity like @p0 ); @p0 = 'sass%'

Se in futuro il DBA decidesse di modificare la struttura della tabella Customer sarà sufficiente cambiare il mapping senza modificare nessuna query HQL, perché esiste una separazione tra il modello ad oggetti ed il modello fisico dei dati su database.

Riferimenti

Ti consigliamo anche