In LINQ esiste la possibilità di rendere piatta una gerarchia mediante l'utilizzo del costrutto Select Many, questo costrutto è particolarmente utile quando si vogliono selezionare Customers (entità padre) attuando un filtro sugli orders (entità figlio, ovvero un Customer è legato ad una collection di Orders e viceversa un Order è legato ad un solo Customer).
Ad esempio volendo filtrare tutti i Customer che hanno eseguito almeno un Order il cui totale è maggiore di 100 si può scrivere:
Select Many
1 IList<Customer> filteredCustomers =
2 from c in customers
3 from o in c.Orders
4 where o.Amount > 100
5 select c;
Ho scoperto che questo costrutto in Entity Framework tira dei brutti scherzi con il fetch eager dei riferimenti di una entità.
Per eseguire un fetche eager in EF si utilizza il metodo Include, quindi se vogliamo caricare un Customer e con un solo round trip sul DB anche tutti i suoi Orders possiamo scrivere come segue:
1 IList<Customer> allCustomers =
2 from c in context.Customers
3 .Include("Orders")
4 select c;
In questo caso ci ritroveremo ogni entità Customer con la lista degli Orders già caricata, e correttamente se si osserva la query SQL generata si potrà apprezzare una LEFT OUTER JOIN fra la tabella [CUSTOMERS] e la tabella [ORDERS] che serve per recuperare tutte le proprietà di ogni Order coinvolto.
Il problema nasce quando la Include viene utilizzata in un costrutto Select Many come quello proposto:
1 IList<Customer> filteredCustomers =
2 from c in context.Customers
3 .Include("Orders")
4 from o in c.Orders
5 where o.Amount > 100
6 select c;
7
Il risultato è ovviamente corretto, nel senso che vengono selezionati i Customer che hanno almeno un Order che soddisfa il filtro, ma il problema è che la Include non ha alcun effetto, ovvero i Customer non vedono inizializzata la propria lista di Orders. Osservando la query SQL effettivamente viene eseguita solo una INNER JOIN solo ed esclusivamente per legare i Customer agli Order su cui in effetti viene eseguito il filtro, ma non c'è traccia del caricamento dei dettagli degli ordini una volta individuati i Customer filtrati.
Per la cronaca osservando il Profiler di SQL Server nel primo caso EF lancia una semplice query T-SQL (un EventClass di tipo SQL), mentre nel secondo lancia una store procedure ExecuteSql (un EventClass di tipo RPC).