Anonymní delegáti vo foreach

Dnes to budú 2 tipy v jednom článku. Prvý problém je, ako načítať v LINQ 2 SQL asociované kolekcie. Mám entity Customer a Contact. A samozrejme Customer má kolekciu Contacts. A potrebujem v jednej metóde načítať zákazníkov aj s ich kontaktmi. Dá sa na to použiť trieda DataLoadOptions. Asi nasledovne:

var dlo = new DataLoadOptions();

dlo.LoadWith<Customer>(c => c.Contacts);

dataContext.LoadOptions = dlo;

return dataContext.Customers.ToList();

Toto zabezpečí, že v jednom SQL príkaze sa načítajú zákazníci aj ich kontakty. Čiže iba jeden dotaz na databázu, čo je oveľa efektívnejšie ako keby sa pre každého zákazníka nahrávali kontakty pomocou lazy-load metódy. Problém je ale v tom, že kontakty nepotrebujem načítať vždy, ale len v určitej metóde. A vlastnosť LoadOptions má nasledovné obmedzenia:

  1. Ak objekt DataLoadOptions priradím vlastnosti LoadOptions, tak tento objekt nemôžem meniť. To znamená, že nemôžem zavolať metódu LoadWith.
  2. Ak sa na danom DataContext objekte spustilo nejaké query, tak vlastnosť LoadOptions nie je možné zmeniť.

Našťastie kolekcia Contacts má metódu SetSource, čiže kontakty pre zákazníka môžem načítať z ľubovoľného zdroja typu IEnumerable<Contact>. Takže najprv načítam zákazníkov. Potom načítam všetky kontakty týchto zákazníkov do kolekcie List<Contact>. Sú to síce 2 dotazy na databázu namiesto jedného, ale stále je to konštantný počet oproti lineárnemu pri lazy-load. No a potom pre každého zákazníka stačí nastaviť zdroj kontaktov nie z databázy, ale z tohto listu.

/// <summary>

/// Loads the contacts for customers in one SQL statement.

/// </summary>

/// <param name="customers">The customers to load contacts for.</param>

/// <param name="customerQuery">The customer query. If null, then customers are selected from the list.</param>

public void LoadCustomerContacts(IList<Customer> customers, IQueryable<Customer> customerQuery)

{

if (customers == null)

{

throw new ArgumentNullException("customers");

}

IQueryable<Contact> contactsQuery = null;

if (customerQuery != null)

{

contactsQuery = Context.Contacts.Where(c => customerQuery.Any(cus => cus == c.Customer));

}

else

{

int[] customerIds = customers.Select(c => c.Id).ToArray();

contactsQuery = Context.Contacts.Where(c => customerIds.Contains(c.CustomerId));

}

var contacts = contactsQuery.ToList();

foreach (var customer in customers)

{

if (!customer.Contacts.HasLoadedOrAssignedValues)

{

customer.Contacts.SetSource(contacts.Where(c => c.CustomerId == customer.Id));

}

}

}

A tu som narazil na problém s anonymným delegátom alebo lambda funkciou vo foreach cykle. Teda sa dostávam k druhému bodu v tomto článku. Keď som túto metódu spustil, tak všetci zákazníci mali rovnakú množinu kontaktov a to boli kontakty posledného zákazníka. Síce som hneď pochopil, v čom je problém, ale aby som ho mohol vysvetliť, tak najprv vysvetlím ako fungujú anonymní delegáti. Lambda funkcia je len iný zápis anonymného delegáta, takže pre ňu to platí rovnako.

Predchádzajúca funkcia sa dá zapísať a preloží sa nasledovne. Najprv sa vygeneruje pomocná trieda, ktorá udržuje stav.

private class DelegateClass

{

public Customer customer;

 

public bool DelegateMethod(Contact c)

{

return c.CustomerId == customer.Id;

}

}

Potom sa foreach cyklus preloží nasledovne.

DelegateClass delegateStatus = new DelegateClass();

IEnumerator<Customer> enumerator = customers.GetEnumerator();

enumerator.Reset();

while (enumerator.MoveNext())

{

delegateStatus.customer = enumerator.Current;

if (!delegateStatus.customer.Contacts.HasLoadedOrAssignedValues)

{

delegateStatus.Contacts.SetSource(contacts.Where(delegateStatus.DelegateMethod));

}

}

Samozrejme, že tento môj preklad je len približný a hlavne som si vymyslel rozumnejšie názvy ako tie, čo generuje C# kompilátor. Ale dôležité je, že objekt typu DelegateClass sa vytvára iba raz pred vykonaním cyklu. Takže na konci majú všetky zdroje kontaktov v podmienke Where odkaz na ten istý objekt DelegateClass a teda toho istého zákazníka. A preto sa vždy načítajú len kontakty posledného zákazníka. Riešením je samozrejme presunúť riadok "DelegateClass delegateStatus = new DelegateClass()" do vnútra cyklu. Takže stačilo prepísať foreach cyklus nasledovne.

foreach (var customer in customers)

{

if (!customer.Contacts.HasLoadedOrAssignedValues)

{

int customerId = customer.Id;

customer.Contacts.SetSource(contacts.Where(c => c.CustomerId == customerId));

}

}

Alebo druhá možnosť je hneď za SetSource nahrať kolekciu kontaktov.

foreach (var customer in customers)

{

if (!customer.Contacts.HasLoadedOrAssignedValues)

{

customer.Contacts.SetSource(contacts.Where(c => c.CustomerId == customer.Id));

customer.Contacts.Load();

}

}

Záver je jednoduchý. Pri použití anonymných delegátov v cykle si treba vždy uvedomiť, kde sa vytvára inštancia stavového objektu delegáta. Aby sa nestalo, že rôzni delegáti zdieľajú ten istý stav.

Bookmark and Share

Komentáre

# vlko said:

Caf, nie je lepsie pri linq2sql pouzit join?

Aj ked mne trosku vadi, ze takyto eager loading musi explicitne definovat fk prepojenie, hql v nhibernate postacuje definicia join a typ triedy

Wednesday, October 22, 2008 2:59 PM
# duracellko said:

no skusil som pouzit GroupJoin

var customers = dataContext.Customers.GroupJoin(dataContext.Contacts, cus => cus.Id, con => con.Id, (customer, contacts) => { customer.Contacts.SetSource(contacts); return customer });

ale compilator mi vyhlasil, ze taku lambda funkciu nevie prelozit do expression tree.

no tiez netusim, ze preco sa neda v LINQ2SQL definovat eager loading per query, ale iba raz a navzdy pri inicializacii. v ASP.NET EF to ide tiez cez Include metodu.

Wednesday, October 22, 2008 4:28 PM
# rebro said:

Cau,

nema to byt nahodou:

dlo.LoadWith<Customer>(c => c.Contacts);

?

Thursday, October 23, 2008 9:31 AM
# vlko said:

to duracellko: vyraz s normalnou C# linq syntaxou nemas?:)

nieco ako  

var q = from c in customers

           join cont in c.Contacts on c.Key equals cont .Key

           select new {c.Name, cont.Name};

a potom sa pozriet na vygenerovany expression tree a z toho uz poznas, ako mas spravne napisat ten tvoj prikaz v tvojom oblubenom nelinq tvare:)

Thursday, October 23, 2008 11:12 AM
# duracellko said:

to rebro.. ano mas pravdu.. uz som to opravil.. vdaka

Thursday, October 23, 2008 11:15 AM
# duracellko said:

to vlko.. obycajny join v nelinq tvare viem zapisat.. len vysledkom je zoznam anonymnych objektov.. co zrovna nechcem, lebo anonymne objekty nemozu opustit metodu. A navyse naco by som definoval novu triedu, kde je spojeny kontakt aj zakaznik, ked uz to raz mam zadefinovane.

Thursday, October 23, 2008 11:20 AM
# vlko said:

to duracellko: tu anonymnu triedu som dal ako priklad. Uff teda som si myslel, ze ked das var q = from c in customers join cont in c.Contacts on c.Key equals cont .Key select c; tak sa do c nacitaju aj Contacts podla key vazby, ale ked sa nad tym zamyslam, tak som zasa kurnik ovplyvneny hql syntaxov:) Ale pretoze sa lahko nevzdavam podla tychto testov linq to nhibernate (http://nhcontrib.svn.sourceforge.net/viewvc/nhcontrib/trunk/src/NHibernate.Linq/src/NHibernate.Linq.Tests/LinqQuerySamples.cs?revision=516&view=markup) sa taky join medzi tabulkami vytvarajuci eager loading mal pisat: var q = from c in db.Customers from o in c.Orders.Cast() select o; A dva from za sebou su predsa SelectMany podla cheat table zo spravicky (http://www.aspnet.sk/TIP-Linq-to-Query-Expression-Translation-Cheat-Sheet-100364.aspx)

Thursday, October 23, 2008 2:52 PM
# vlko said:

Tak teraz neviem, poslal som pred 15 minutami prispevok, alebo neposlal, tak este raz: Pozeral som sa do linq to nhibernate unit testov (http://nhcontrib.svn.sourceforge.net/viewvc/nhcontrib/trunk/src/NHibernate.Linq/src/NHibernate.Linq.Tests/LinqQuerySamples.cs?revision=516&view=markup) a spravny zapis eager loadingu by mal byt: var q = from c in db.Customers from o in c.Orders.Cast() select o; samozrejme to c.Orders.Cast bude v linq2sql rozdielne ale urcite podobne a zo spravicky http://www.aspnet.sk/TIP-Linq-to-Query-Expression-Translation-Cheat-Sheet-100364.aspx vieme, ze dva from za sebou sa prekladaju na SelectMany

Thursday, October 23, 2008 3:26 PM
Prihlásiť | Registrovať | Pomoc