Kleine LINQ-Falle – Queryable vs. Enumerable

10. Juni 2010

Es gibt eine kleine Falle beim Zugriff auf die Datenbank mittels LINQ (und ich habe schon mehr wie einen gesehen, der darauf hereingefallen ist): Es ist nicht immer offensichtlich welcher Teil der LINQ-Abfrage im Speicher durchgeführt wird, und welcher in der Datenbank.

Das generelle Problem ist, daß der C# Compiler nicht darauf hinweist (von einer deutlichen Warnung ganz zu schweigen) ob er im Einzelfall LINQ to SQL (bzw. Entity Framework) verwendet, oder LINQ to Objects.

Ein einfaches Beispiel: Per LINQ to SQL sollen Daten abgeholt werden, wobei das Filterkriterium als Lambda übergeben wird:

   1: public static IEnumerable<Customer> GetCustomers(Func<Customer, bool> predicate) 
   2: { 
   3:     using (MyDataContext dc = new MyDataContext()) 
   4:     { 
   5:         IQueryable<Customer> query = from customer in dc.Customers select customer;
   6:  
   7:         if (predicate!=null) 
   8:             query = query.Where(predicate).AsQueryable();
   9:  
  10:         return query.ToList(); 
  11:     } 
  12: }

Und der Aufruf:

   1: void Foo() 
   2: { 
   3:         var customers= DataAccess.GetCustomers(c=>c.City=="Mainz"); 
   4:         ... 
   5: }

Man packe 5 Mio Kunden in die Datenbank und selektiere dann einen kleinen Teilausschnitt, sagen wir 100 Datensätze…

Wer jetzt davon ausgeht, dass nur 100 Datensätze aus der Datenbank gelesen werden, den muß ich leider enttäuschen. Tatsächlich werden alle 5 Mio Kunden in den Speicher gesaugt, erst dort werden die unerwünschten Daten dann aussortiert.

Das Warnsignal ist der notwendige Aufruf von AsQueryable(). Problem ist, daß mit dem Where nicht die Extension-Methode in Queryable aufgerufen wird (die würde die Bedingung an LINQ2SQL weitergeben und den Filter in der DB anwenden), sondern an Enumerable. Dadurch wird aber die ganze Abfrage – ohne Filter – an die Datenbank geschickt, Enumerable bastelt lediglich einen Filter-Iterator über das Resultset, der ganz normal als Funktionalität im Speicher auf beliebigen Listen arbeitet.

Der entscheidende Knackpunkt im obigen Beispiel ist die Deklaration des predicate, Queryable.Where(Expression<Func<T, bool>>) erwarter hier eine Expression; ohne diese kleine Feinheit greift der Compiler auf Enumerable.Where(Func<T, bool>) zurück. Also:

   1: public static IEnumerable<Customer> GetCustomers( Expression< Func<Customer, bool> > predicate)

Dann klappt’s auch mit dem Nachbarn.

Das ist eine Falle, in die man leicht mal aus Unachtsamkeit reinlaufen kann. Sowohl wegen solche Fallstricke, als auch grundsätzlich lohnt es sich beim Einsatz von LINQ2SQL bzw EF den SQL Profiler nebenher laufen zu lassen, um sich anzuschauen, welche Abfragen tatsächlich in der Datenbank ankommen.