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 ;-)
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
- Inside ModelVirtualCasting #1: Introduzione ai repository, il 27 maggio 2010 alle 07:30
- Nuova versione per EF Mapping Verifier, il 30 settembre 2009 alle 20:01
- Testare il mapping di Entity Framework, il 17 settembre 2009 alle 09:00
- Basta! Italia 2009 - I'll be there!, il 13 marzo 2009 alle 17:01
- Alcune info per chi usa NHibernate, il 6 ottobre 2008 alle 08:34