Gli ORM e la many to one: 3 approcci differenti

di Marco De Sanctis, in ORM,

Un paio di giorni fa ho ricevuto una domanda in mail da un lettore circa l'approccio da utilizzare quando si gestiscono le lookup in un Domain Model. Supponiamo di avere la classica relazione tra Order e Customer. La domanda era più o meno così:

Nella form di creazione ordini, ho una DropDownList con l'elenco dei clienti; ovviamente questa mi restituisce solo l'Id di quello selezionato, mentre per costruire correttamente la mia classe Order avrei bisogno dell'intera istanza. Possibile che sia pertanto vincolato ad effettuare una query, sebbene a me, alla fine della solfa, serva solo l'Id?

E' un tipico caso di impedence mismatch tra mondo relazionale e Domain Model, ed è questo il motivo per cui la soluzione è strettamente legata all'ORM utilizzato. Vediamo quali sono state le scelte e soprattutto quali sono le differenze tra i 3 ORM più "in voga" del momento, ossia Linq To SQL, NHibernate e Entity Framework.

Linq To SQL

Il primo ORM made in Microsoft utilizza un approccio molto semplice per risolvere il problema. La classe Order prodotta dal designer, infatti, possiede sia la reference al Customer corrispondente, sia la proprietà CustomerID, che può essere valorizzata direttamente. Il codice pertanto è estremamente immediato e rispecchia in pieno la natura magari "meno rigorosa", ma sicuramente con un gradino d'apprendimento estremamente smussato, di Linq To SQL:

Order order = new Order();
order.Description = "New order";
order.CustomerId = ddlCustomers.SelectedValue; 
ctx.Orders.InsertOnSubmit(order);
ctx.SubmitChanges();

NHibernate

La scelta implementativa di Linq To SQL presenta però un problema: l'istanza di Order non è collegata ad un *vero* customer, è stato fornito un semplice identificativo e, finché non si procede al salvataggio (e quindi alla rilettura dell'order), Order.Customer vale null. Ovviamente quindi uno statement tipo

Console.Writeline(Order.Customer.Name)

solleverebbe una NullReferenceException.

NHibernate è invece più rigoroso e non consente di "aggirare" la specifica della reference fornendo solo un identificativo, ma ha bisogno di un'istanza di Customer. Quindi strada obbligata verso la inutile query? La risposta fortunatamente è no, o almeno è no se è stato attivato il lazy load per la classe Customer nel relativo mapping:

<class name="..." lazy="true">
....
</class>

Il codice della nostra form di gestione ordini diventa qualcosa tipo

Order order = new Order();
order.Description = "New order";
order.Customer = session.Load<Customer>(ddlCustomers.SelectedValue);

session.SaveOrUpdate(order);
session.Flush();

In caso di lazy load attivo, session.Load<Customer> non effettua alcuna query verso il DB, ma restituisce un proxy di Customer, ossia una classe generata dinamicamente, che eredita da Customer, perfettamente valida nel caso di assegnazione alla proprietà Order.Customer.

Il vantaggio di questo approccio è che, in qualsiasi momento proviamo ad eseguire

Console.Writeline(Order.Customer.Name)

NHibernate in maniera del tutto trasparente si connette al database, carica l'istanza del cliente e ne stampa a video il nome. Il tutto si paga però con una complessità architetturale maggiore (session attiva in ogni momento, necessità di gestire i casi di errore nel caricamento dati, ecc.).

Entity Framework

Entity Framework, come accade in diversi altri frangenti, è invece meno "automatico" e ciò, se da un lato obbliga l'utente ad un maggior intervento nell'utilizzo (basta pensare a come è gestito il caricamento delle relazioni), dall'altro consente in qualche modo dei workaround per gestire situazioni per le quali non è presente un'implementazione specifica, come questo caso.

Entity Framework richiede infatti che la relazione sia rappresentata con un'istanza di un customer, ma non è necessario recuperarlo dal database, a patto di popolarne correttamente la proprietà EntityKey (grazie a Stefano per la dritta!!):

Order o = new Order();
o.Description = "New order"; 
// creo il customer "fittizio"
Customer customer = new Customer();
customer.Id = 1;
customer.EntityKey = new EntityKey();
customer.EntityKey.EntitySetName = "CustomerSet";
customer.EntityKey.EntityKeyValues = new[] { new EntityKeyMember("Id", ddlCustomers.SelectedValue) };
customer.EntityKey.EntityContainerName = "testdbEntities";
// aggancio il customer all'ObjectContext
ctx.Attach(customer); 
// aggiorno la relazione e salvo l'ordine
o.Customer = customer;
ctx.AddToOrderSet(o); 
ctx.SaveChanges(); 

Ovviamente in questo caso, il cliente non è valorizzato, e se si prova ad accedere al nome si ottiene un bel null, anche se non viene sollevato alcun errore. Un'ultima nota: attenzione a rispettare l'ordine dei passaggi, ed in particolare ad agganciare il customer al context prima di associarlo all'ordine, altrimenti non funziona più nulla.

Ciao ;-)

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese