Komplexe Abfrageoperatoren

LINQ (Language Integrated Query) enthält viele komplexe Operatoren, die mehrere Datenquellen kombinieren oder komplexe Verarbeitungen durchführen. Nicht für alle LINQ-Operatoren gibt es geeignete Übersetzungen auf der Serverseite. Manchmal wird eine Abfrage in einem Format für den Server übersetzt, aber in einem anderen Format nicht, obwohl das Ergebnis gleich ist. Auf dieser Seite werden einige der komplexen Operatoren und ihre unterstützten Variationen beschrieben. Möglicherweise werden in zukünftigen Releases mehr Muster erkannt und deren entsprechenden Übersetzungen hinzugefügt. Es ist auch wichtig, zu beachten, dass die Übersetzungsunterstützung je nach Anbieter variiert. Eine bestimmte Abfrage, die für SQL Server übersetzt wird, funktioniert möglicherweise nicht für SQLite-Datenbanken.

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Join

Mit dem LINQ-Join-Operator können Sie zwei Datenquellen anhand des Schlüsselselektors für die jeweilige Quelle verbinden und ein Tupel von Werten erstellen, wenn der Schlüssel übereinstimmt. Dies wird für relationale Datenbanken natürlich in INNER JOIN übersetzt. Während der LINQ-Join-Operator äußere und innere Schlüsselselektoren aufweist, erfordert die Datenbank eine einzelne Verknüpfungsbedingung. Daher generiert EF Core eine Verknüpfungsbedingung mithilfe eines Vergleichs der Gleichheit des äußeren und inneren Schlüsselselektors.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on photo.PersonPhotoId equals person.PhotoId
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId]

Des Weiteren generiert EF Core eine Verknüpfungsbedingung, um die Gleichheit der Komponenten zu vergleichen, wenn die Schlüsselselektoren einen anonymen Typ aufweisen.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
                equals new { Id = person.PhotoId, Caption = "SN" }
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN'))

GroupJoin

Mit dem LINQ-GroupJoin-Operator können Sie zwei Datenquellen ähnlich wie mit dem Join-Operator verbinden, jedoch wird dabei eine Gruppe innerer Werte zum Vergleichen äußerer Elemente erstellt. Das Ausführen einer Abfrage ähnlich dem folgenden Beispiel führt zum Ergebnis Blog und IEnumerable<Post>. Da Datenbanken (insbesondere relationale Datenbanken) keine Möglichkeit zum Darstellen einer Sammlung von clientseitigen Objekten aufweisen, können GroupJoin-Operatoren in vielen Fällen nicht auf den Server übersetzt werden. Hierzu ist erforderlich, dass Sie alle Daten für GroupJoin ohne einen speziellen Selektor vom Server abrufen (die erste Abfrage unten). Wenn der Selektor jedoch die Auswahl von Daten einschränkt und dann alle Daten vom Server abruft, können Leistungsprobleme auftreten (die zweite Abfrage unten). Daher übersetzt EF Core GroupJoin nicht.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, grouping };
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, Posts = grouping.Where(p => p.Content.Contains("EF")).ToList() };

SelectMany

Mit dem LINQ-SelectMany-Operator können Sie einen Sammlungsselektor für äußere Elemente aufzählen und Tupel von Werten aus einzelnen Datenquellen generieren. Auf gewisse Zeit ist dies ein Join, der jedoch keine Bedingungen aufweist, sodass alle äußeren Elemente mit einem Element aus der Sammlungsquelle verknüpft werden. Je nachdem, wie der Sammlungsselektor im Bezug mit der äußeren Datenquelle steht, kann SelectMany in verschiedene Abfragen auf der Serverseite übersetzt werden.

Sammlungsselektor referenziert nicht die äußere Quelle

Wenn der Sammlungsselektor nichts von der äußeren Quelle referenziert, ist das Ergebnis ein kartesisches Produkt beider Datenquellen. Dies wird für relationale Datenbanken in CROSS JOIN übersetzt.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
CROSS JOIN [Posts] AS [p]

Sammlungsselektor referenziert die äußere Datenquelle in einer WHERE-Klausel

Wenn der Sammlungsselektor eine WHERE-Klausel enthält, die das äußere Element referenziert, dann übersetzt EF Core sie in einen Join der Datenbank und verwendet das Prädikat als Joinbedingung. Normalerweise tritt dieser Fall auf, wenn die Sammlungsnavigation im äußeren Element als Sammlungsselektor verwendet wird. Wenn die Sammlung für ein äußeres Element leer ist, werden keine Ergebnisse für dieses generiert. Wenn DefaultIfEmpty jedoch auf den Sammlungsselektor angewendet wird, wird das äußere Element mit einem Standardwert des inneren Elements verknüpft. Aufgrund dieser Unterscheidung wird diese Art von Abfrage bei Abwesenheit von DefaultIfEmpty in INNER JOIN übersetzt und in LEFT JOIN, wenn DefaultIfEmpty angewendet wurde.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

Sammlungsselektor referenziert die äußere Datenquelle in einem Fall ohne WHERE-Klausel

Wenn der Sammlungsselektor das äußere Element referenziert, das sich nicht in einer WHERE-Klausel befindet (wie im obigen Fall), wird es nicht in einen Join der Datenbank übersetzt. Daher muss der Sammlungsselektor für alle äußeren Elemente ausgewertet werden. Für viele relationale Datenbanken wird dies in APPLY-Vorgänge übersetzt. Wenn die Sammlung für ein äußeres Element leer ist, werden keine Ergebnisse für dieses generiert. Wenn DefaultIfEmpty jedoch auf den Sammlungsselektor angewendet wird, wird das äußere Element mit einem Standardwert des inneren Elements verknüpft. Aufgrund dieser Unterscheidung wird diese Art von Abfrage bei Abwesenheit von DefaultIfEmpty in CROSS APPLY übersetzt und in OUTER APPLY, wenn DefaultIfEmpty angewendet wurde. Bestimmte Datenbanken wie SQLite unterstützen APPLY-Operatoren nicht. Daher wird diese Art von Abfrage möglicherweise nicht übersetzt.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
CROSS APPLY [Posts] AS [p]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
OUTER APPLY [Posts] AS [p]

GroupBy

LINQ-GroupBy-Operatoren erstellen ein Ergebnis vom Typ IGrouping<TKey, TElement>, wobei TKey und TElement einen beliebigen arbiträren Typ aufweisen können. Außerdem wird IEnumerable<TElement> von IGrouping implementiert, d. h. Sie können es nach der Gruppierung mithilfe eines beliebigen LINQ-Operators zusammensetzen. Da keine Datenbankstruktur eine IGrouping-Schnittstelle darstellen kann, gibt es in den meisten Fällen keine Übersetzung für GroupBy-Operatoren. Wenn ein Aggregate-Operator auf alle Gruppen angewendet wird, die einen Skalarwert zurückgeben, kann dieser für relationale Datenbanken in die SQL-Anweisung GROUP BY übersetzt werden. Die SQL-Anweisung GROUP BY ist ebenfalls einschränkend. Sie erfordert, dass Sie nur nach Skalarwerten gruppieren. Die Projektion kann nur gruppierende Schlüsselspalten oder auf eine Spalte angewendete Aggregate-Operatoren enthalten. EF Core identifiziert dieses Muster und übersetzt es wie im folgenden Beispiel für den Server:

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]

EF Core übersetzt Abfragen auch, wenn ein Aggregate-Operator für die Gruppierung in einem WHERE- oder OrderBy-LINQ-Operator (oder in anderen Order-Operatoren) vorliegt. Für die WHERE-Klausel wird in SQL eine HAVING-Klausel verwendet. Der Teil der Abfrage, bevor der GroupBy-Operator angewendet wird, kann einer beliebigen komplexen Abfrage entsprechen, solange diese für den Server übersetzt werden kann. Sobald Sie Aggregate-Operatoren in einer Gruppierungsabfrage anwenden, um Gruppierungen aus der resultierenden Quelle zu entfernen, können Sie sie außerdem wie jede andere Abfrage zusammensetzen.

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            where g.Count() > 0
            orderby g.Key
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]
HAVING COUNT(*) > 0
ORDER BY [p].[AuthorId]

Die von EF Core unterstützten Aggregate-Operatoren lauten wie folgt:

.NET SQL
Average(x => x.Property) AVG(Property)
Count() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) MAX(Property)
Min(x => x.Property) MIN(Property)
Sum(x => x.Property) SUM(Property)

Zusätzliche Aggregatoperatoren werden möglicherweise unterstützt. Überprüfen Sie Ihre Anbieterdokumentation auf weitere Funktionszuordnungen.

Obwohl es keine Datenbankstruktur gibt, die eine IGroupingdarstellt, können EF Core 7.0 und höher in einigen Fällen die Gruppierungen erstellen, nachdem die Ergebnisse aus der Datenbank zurückgegeben wurden. Dies ähnelt der Funktionsweise des Include-Operators beim Einschließen verwandter Auflistungen. Die folgende LINQ-Abfrage verwendet den GroupBy-Operator, um die Ergebnisse nach dem Wert ihrer Price-Eigenschaft zu gruppieren.

var query = context.Books.GroupBy(s => s.Price);
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

In diesem Fall wird der GroupBy-Operator nicht direkt in eine GROUP BY-Klausel in der SQL-Abfrage übersetzt, sondern EF Core erstellt die Gruppierungen, nachdem die Ergebnisse vom Server zurückgegeben wurden.

Left Join

„Left Join“ ist zwar kein LINQ-Operator, jedoch verfügen relationale Datenbanken über das Konzept von „Left Join“, das häufig in Abfragen verwendet wird. Ein bestimmtes Muster in LINQ-Abfragen führt zu demselben Ergebnis wie ein LEFT JOIN auf dem Server. EF Core identifiziert solche Muster und generiert den entsprechenden LEFT JOIN auf dem Server. Das Muster umfasst die Erstellung eines GroupJoin-Operators auf beiden Datenquellen sowie die anschließende Vereinfachung der Gruppierung mithilfe des SelectMany-Operators mit DefaultIfEmpty für die Gruppierungsquelle, um eine Übereinstimmung mit NULL zu suchen, wenn die innere Quelle kein zugehöriges Element aufweist. Im folgenden Beispiel wird das Muster und dessen Ergebnis veranschaulicht.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            from p in grouping.DefaultIfEmpty()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

Das obige Muster erstellt eine komplexe Struktur in der Ausdrucksbaumstruktur. Daher erfordert EF Core, dass Sie die Gruppierungsergebnisse des GroupJoin-Operators in dem Schritt vereinfachen, der direkt auf den Operator folgt. Selbst wenn GroupJoin-DefaultIfEmpty-SelectMany in einem anderen Muster verwendet wird, wird dies möglicherweise nicht als Left Join identifiziert.