Laden...

[erledigt] Linq Expression Tree Problem

Erstellt von Curse4Life vor 12 Jahren Letzter Beitrag vor 12 Jahren 1.664 Views
C
Curse4Life Themenstarter:in
452 Beiträge seit 2005
vor 12 Jahren
[erledigt] Linq Expression Tree Problem

verwendetes Datenbanksystem: MS SQL 2008

Ich habe ein Problem und weiß nicht so recht woran es liegt, ich konnte es zwar auf eine Zeile eingrenzen, aber ich weiß nicht genau was da schief läuft.

Erst mal die DB/Code First Rahmeninfos.
Eine Tabelle und ein CodeFirst Model "Besuchsberichte", dieses Besuchsberichtsmodel hat ein paar Eigenschaften unter anderem die KundenNr, die aussagt bei welchem Kunden der AD war wozu er den Besuchsbericht geschrieben hat.
Die Tabelle hat ca. 38.000 Datensätze.

So weit so gut, jetzt habe ich mir eine Methode geschrieben die so aussieht:


public static IQueryable<TModel> Apply<TModel>(IQueryable<TModel> queryable, IInputModel inputModel, out Int32 allRowsCount)
		{
			var param = Expression.Parameter(typeof(TModel), "item");			
			BinaryExpression finalExpression = null;
			Expression<Func<TModel, Boolean>> finalLambdaExpression = null;


			// Jede Property durchgehen und schauen ob sie != null ist
			foreach (var loopProp in inputModel.GetType().GetProperties())
			{
				if (loopProp.Name != "OrderBy" && loopProp.Name != "OrderDescending" && loopProp.Name != "TopX" && loopProp.Name != "Skip" && loopProp.Name != "AllRowsCount" && loopProp.Name != "IncludeNavigationProperties")
				{
					Type propType = typeof(TModel).GetProperty(loopProp.Name).PropertyType;
					var propValue = loopProp.GetValue(inputModel, null);
				
					
					if (propValue != null)
					{						
						var property = Expression.Property(param, loopProp.Name);
						
						var equalExpression = Expression.Equal(property, Expression.Constant(propValue, propType));

						if (finalExpression == null)
						{
							finalExpression = equalExpression;							
						}
						else
						{
							finalExpression = Expression.AndAlso(finalExpression, equalExpression);							
						}												
					}
				}				
			}


			// Die finale LambdaExpression bilden
			finalLambdaExpression = Expression.Lambda<Func<TModel, Boolean>>(finalExpression, param);
// Übeltäter			
queryable = queryable.Where(finalLambdaExpression.Compile()).AsQueryable();


			// Anzahl aller Datensätze laden, bevor "Skip" und "TopX" angewendet wird
			allRowsCount = queryable.Count();


			// Am Ende noch sortieren wenn gewünscht
			if (!String.IsNullOrEmpty(inputModel.OrderBy))
			{
				queryable = Sort(queryable, inputModel.OrderBy, inputModel.OrderDescending);
			}


			// Skippen wenn gewünscht
			if (inputModel.Skip.HasValue && inputModel.Skip.Value > 0)
			{
				queryable = queryable.Skip(inputModel.Skip.Value);
			}


			// Nur die gewünschten Einträge zurück geben
			if (inputModel.TopX.HasValue)
			{
				queryable = queryable.Take(inputModel.TopX.Value);
			}
			

			return queryable;
		}

Wenn man sich mit Expression Trees auskennt kein Voodoo.
Diese Methode bekommt in unserem Fall jetzt eine IQueryable<Besuchsbericht> und ein InputModel, dieses InputModel hat ein Basismodel mit Eigenschaften wie Sortierung, Skip und Top und das Model selber hat in unserem Fall jetzt eine Eigenschaft mit dem Namen KundenNr.

Was ich jetzt machen will ist, schauen ob KundenNr des InputModel != null ist, wenn das so ist, einen Where Filter auf die Queryable ausführen und das natürlich dynamisch auf unendlich viele Eigenschaften.

Jetzt kommt die Krux!
Es funktioniert einwandfrei!

ABER

Uns ist jetzt aufgefallen das das laden von 4 (Gemacht durch Skip und Take) Besuchsberichten 2-3 Sekunden dauert und wenn man das SQL auf dem Server direkt ausführt ist es sofort da!
Wir also mit dem SQL Profiler geschaut was Linq abschickt und Linq holt sich ALLE 38.000 Datensätze und wendet das Skip und Take dann erst lokal an!

Die Zeile an der das liegt ist:


queryable = queryable.Where(finalLambdaExpression.Compile()).AsQueryable();

Wenn ich diese auskommentiere habe ich zwar keine Where Bedingung mehr, aber das Skip und Take werden wirklich schon auf SQL Basis ausgeführt!

Und das .Count() ist sofort durch und das dauert mit der Zeile halt die 2-3 Sek.

Habt ihr eine Erklärung dafür, ich bleibe doch die ganze Zeit auf IQueryable Ebene und damit Linq to SQL Ebene, aber es scheint halt so als würde diese eine Zeile dafür sorgen das ich halt alle Datensätze lade und lokal erst die ganzen Filter anwende.

Ich hoffe ihr habt Rat

6.862 Beiträge seit 2003
vor 12 Jahren
  
...  
// Anzahl aller Datensätze laden, bevor "Skip" und "TopX" angewendet wird  
allRowsCount = queryable.Count();  
...  
  

Wir also mit dem SQL Profiler geschaut was Linq abschickt und Linq holt sich ALLE 38.000 Datensätze und wendet das Skip und Take dann erst lokal an!

Fällt da was auf? 😃 Um die Gesamtanzahl der Elemente zu bestimmen, müssen natürlich alle geholt werden. Der Kommentar im Code passt und ist der Grund für deine Beobachtung.

Baka wa shinanakya naoranai.

Mein XING Profil.

C
Curse4Life Themenstarter:in
452 Beiträge seit 2005
vor 12 Jahren

Nee, das ist nicht das Problem, an der Stelle will ich ja die ANZAHL aller Datensätze das ist schon OK.

Wenn ich die Zeile auskommentiert habe kriege ich ja 38.000 zurück und das sofort ohne Verzögerung, wenn ich das Where aber drin habe, dann dauert das Count 2-3 Sek. obwohl (theoretisch) nur die Zahl 117 geladen wird.

Und Count gibt ja auch nur die Anzahl zurück und dafür muss kein Datensatz geladen werden, ist ja das SQL Statement "SELECT Count(*) FROM Besuchsberichte..."

EDIT:
Zur Erklärung 38.000 Besuchsberichte insgesamt, 117 mit der KundenNr = 12345.

2.891 Beiträge seit 2004
vor 12 Jahren

Um die Gesamtanzahl der Elemente zu bestimmen, müssen natürlich alle geholt werden.

Kann man so nicht sagen. Kommt ganz auf den Query-Provider an, der die Expression bekommt (bei SQL wird üblicherweise ein count(*) draus gemacht - also nicht erst alle Datensätze geholt).

Warum kompilierst du die finalLambdaExpression (finalLambdaExpression.Compile())? Das erzeugt dir doch ein Delegat und der QueryProvider ist dadurch gezwungen, das an dieser Stelle lokal auszuführen, da der Provider keine Infos mehr über die Expression hat, um sie entsprechend in SQL umzuformen (du hast dadurch ja die eigentliche Expression "vernichtet").

Bei IQueryable solltest/musst du immer mit Expressions arbeiten. Sobald du irgendwo Delegaten verwendest (z.b. Func<> statt Expression<Func<>>), wird an dieser Stelle die Query ausgeführt und mit dem LINQ2Objects-Provider weitergemacht.

Ein queryable = queryable.Where(finalLambdaExpression); sollte doch reichen, oder?

Gruß,
dN!3L

C
Curse4Life Themenstarter:in
452 Beiträge seit 2005
vor 12 Jahren

@dN!3L

Ahhhhhh, super auf so eine Antwort hatte ich gehofft, wieder was gelernt!
Das .Compile() hatte ich mir irgendwo im Internet abgeschaut!

Funktioniert auch super, habs schon getestet, ich danke dir! Auch für die Erklärung was passiert und warum es so passiert!

16.807 Beiträge seit 2008
vor 12 Jahren

Das .Compile() hatte ich mir irgendwo im Internet abgeschaut!

Wahrscheinlich in Verbindung des Entity Frameworks und dem Repository Pattern. Das Kompilieren von Queries ist keine schlechte Sache - es fördert die Performance enorm. Aber auch nur da, wo es sinn macht - und das tut es hier in der Gesamtkonstellation nicht.
Beim EF 4.1 passiert das Kompilieren von Queries im Hintergrund automatisch beim ersten Absetzen einer Anfrage.

Einen Blick in die MSDN: CompiledQuery.Compile hätte Dir aber bestimmt einiges erläutert, statt Compile() blind zu übernehmen.