Wie mache ich eine Unterabfrage in LINQ?

73

Hier ist ein Beispiel für die Abfrage, die ich in LINQ konvertieren möchte:

SELECT *
FROM Users
WHERE Users.lastname LIKE '%fra%'
    AND Users.Id IN (
         SELECT UserId 
         FROM CompanyRolesToUsers 
         WHERE CompanyRoleId in (2,3,4) )

Es gibt eine FK-Beziehung zwischen CompanyRolesToUsersund Users, aber es ist eine Viele-zu-Viele-Beziehung und es CompanyRolesToUsershandelt sich um die Kreuzungstabelle.

Wir haben bereits den größten Teil unserer Site erstellt, und wir haben bereits den größten Teil der Filterung, indem wir Ausdrücke mit einer PredicateExtensions-Klasse erstellen.

Der Code für die einfachen Filter sieht ungefähr so ​​aus:

 if (!string.IsNullOrEmpty(TextBoxLastName.Text))
 {
     predicateAnd = predicateAnd.And(c => c.LastName.Contains(
                                     TextBoxLastName.Text.Trim()));
 }

e.Result = context.Users.Where(predicateAnd);

Ich versuche, ein Prädikat für eine Unterauswahl in einer anderen Tabelle hinzuzufügen. ( CompanyRolesToUsers)

Was ich hinzufügen möchte, ist etwas, das dies tut:

int[] selectedRoles = GetSelectedRoles();
if( selectedRoles.Length > 0 )
{
    //somehow only select the userid from here ???:
    var subquery = from u in CompanyRolesToUsers
                   where u.RoleID in selectedRoles
                   select u.UserId;

    //somehow transform this into an Expression ???:
    var subExpression = Expression.Invoke(subquery);

    //and add it on to the existing expressions ???:
    predicateAnd = predicateAnd.And(subExpression);
}

Gibt es eine Möglichkeit, dies zu tun? Es ist frustrierend, weil ich die gespeicherte Prozedur leicht schreiben kann, aber ich bin neu in dieser LINQ-Sache und habe eine Frist. Ich konnte kein passendes Beispiel finden, aber ich bin mir sicher, dass es irgendwo da ist.

marcel_g
quelle

Antworten:

80

Hier ist eine Unterabfrage für Sie!

List<int> IdsToFind = new List<int>() {2, 3, 4};

db.Users
.Where(u => SqlMethods.Like(u.LastName, "%fra%"))
.Where(u =>
    db.CompanyRolesToUsers
    .Where(crtu => IdsToFind.Contains(crtu.CompanyRoleId))
    .Select(crtu =>  crtu.UserId)
    .Contains(u.Id)
)

Zu diesem Teil der Frage:

predicateAnd = predicateAnd.And(c => c.LastName.Contains(
                                TextBoxLastName.Text.Trim()));

Ich empfehle dringend, die Zeichenfolge aus dem Textfeld zu extrahieren, bevor Sie die Abfrage erstellen.

string searchString = TextBoxLastName.Text.Trim();
predicateAnd = predicateAnd.And(c => c.LastName.Contains( searchString));

Sie möchten eine gute Kontrolle darüber behalten, was an die Datenbank gesendet wird. Im ursprünglichen Code besteht eine mögliche Lesart darin, dass eine nicht zugeschnittene Zeichenfolge zum Zuschneiden in die Datenbank gesendet wird - was für die Datenbank keine gute Arbeit ist.

Amy B.
quelle
2
Danke für den hilfreichen Code. Das zeigt sicherlich, wie Dinge miteinander verkettet werden können. Sie haben wahrscheinlich auch Recht, wenn Sie die Zeichenfolgenoperation ausführen, bevor Sie sie an Linq übergeben. Es würde sicherlich nicht schaden, die Dinge getrennt zu halten.
marcel_g
24

Für diese Anweisung, die besser geschrieben ist als, ist keine Unterabfrage erforderlich

select u.* 
from Users u, CompanyRolesToUsers c
where u.Id = c.UserId        --join just specified here, perfectly fine
and u.lastname like '%fra%'
and c.CompanyRoleId in (2,3,4)

oder

select u.* 
from Users u inner join CompanyRolesToUsers c
             on u.Id = c.UserId    --explicit "join" statement, no diff from above, just preference
where u.lastname like '%fra%'
  and c.CompanyRoleId in (2,3,4)

Davon abgesehen wäre es in LINQ

from u in Users
from c in CompanyRolesToUsers 
where u.Id == c.UserId &&
      u.LastName.Contains("fra") &&
      selectedRoles.Contains(c.CompanyRoleId)
select u

oder

from u in Users
join c in CompanyRolesToUsers 
       on u.Id equals c.UserId
where u.LastName.Contains("fra") &&
      selectedRoles.Contains(c.CompanyRoleId)
select u

Was wiederum respektable Wege sind, dies darzustellen. Ich bevorzuge in beiden Fällen die explizite "Join" -Syntax selbst, aber da ist sie ...

TheSoftwareJedi
quelle
Eine sehr schöne Antwort, aber all diese Methoden verhalten sich unterschiedlich und erzeugen normalerweise sehr unterschiedliche SQLs, SQLs, die gut oder schlecht funktionieren können. Normalerweise erzeugt die Verwendung eines Straight Joins die besten SQLs, aber nicht immer ...
Pop Catalin
1
Nach meiner Erfahrung für einigermaßen komplexe Abfragen macht die Art und Weise, wie Sie Linq-Abfragen schreiben, einen Unterschied. SQL Server 2005 führt manchmal nicht zu einem optimalen Abfrageplan für Abfragen mit einer starken Verschachtelung von Unterabfragen. Ich musste einige komplexe Linq-Abfragen mehr als optimieren ein paar Mal ...
Pop Catalin
2
Hmm, warum "filtern", wenn Sie "verbinden und unterscheiden" können. Liegt es daran, dass "Join and Different" natürliche und aufregende SQL sind, während "Filter" einfach nicht aufregend genug ist?
Amy B
2
Nun, da ich eingeladen bin ... Fakt: Kein Optimierer kann einen Distinct (n ^ 2) in einen Filter (n) verwandeln.
Amy B
1
In 99% der Fälle wird eine Abfrage, die mit "different" geschrieben wurde, falsch geschrieben. Wenn Sie eine Tabelle zum Filtern verbinden möchten, aber keine doppelten Datensätze auf der linken Seite des Joins möchten, verwenden Sie einen Semi-Join (dh eine Unterabfrage).
WCWedin
8

So habe ich Unterabfragen in LINQ durchgeführt. Ich denke, dies sollte das bekommen, was Sie wollen. Sie können die explizite CompanyRoleId == 2 ... durch eine andere Unterabfrage für die verschiedenen gewünschten Rollen ersetzen oder ebenfalls beitreten.

from u in Users
join c in (
    from crt in CompanyRolesToUsers
    where CompanyRoleId == 2
    || CompanyRoleId == 3
    || CompanyRoleId == 4) on u.UserId equals c.UserId
where u.lastname.Contains("fra")
select u;
Noah
quelle
6
Er bat um eine Unterabfrage! Aber ich bin sicher, dass LINQ sowieso alles in das gleiche SQL ändert ...
Noah
2
Ich musste "select crt" nach "== 4" setzen, um den Fehler "Ein Abfragetext muss mit einer select-Klausel oder einer Gruppenklausel enden" zu vermeiden
Danny Rancher
@Noah ich wäre mir nicht so sicher. Nach meiner Erfahrung variiert die Leistung von LINQ-Abfragen drastisch, je nachdem, wie Sie die Abfrage schreiben (wie es von jeder SQL-Abfrage zu erwarten ist).
Dan Bechard
2

Sie könnten so etwas für Ihren Fall tun - (Syntax kann etwas abweichen). Schauen Sie sich auch diesen Link an

subQuery = (from crtu in CompanyRolesToUsers where crtu.RoleId==2 || crtu.RoleId==3 select crtu.UserId).ToArrayList();

finalQuery = from u in Users where u.LastName.Contains('fra')  && subQuery.Contains(u.Id) select u;
Perpetualcoder
quelle
3
Ich wollte nur zeigen, dass er es tun wollte ... meine eigene Meinung nicht durchsetzen
Perpetualcoder
@TheSoftwareJedi Ich sehe, dass Sie diesen Kommentar auf mehrere Antworten hier als Spam gesendet haben, aber denken Sie daran, dass dieser Thread wahrscheinlich von Personen gefunden wird, die nach Hilfe suchen, um Unterabfragen in linq durchzuführen, deren Umstände wahrscheinlich nicht genau mit denen von OPs übereinstimmen.
Bwerks
2

Ok, hier ist eine grundlegende Join-Abfrage, die die richtigen Datensätze erhält:

   int[] selectedRolesArr = GetSelectedRoles();
    if( selectedRolesArr != null && selectedRolesArr.Length > 0 ) 
    {

    //this join version requires the use of distinct to prevent muliple records
        //being returned for users with more than one company role.
    IQueryable retVal = (from u in context.Users
                        join c in context.CompanyRolesToUsers
                          on u.Id equals c.UserId
                        where u.LastName.Contains( "fra" ) &&
                            selectedRolesArr.Contains( c.CompanyRoleId )
                        select  u).Distinct();
}

Aber hier ist der Code, der sich am einfachsten in den Algorithmus integrieren lässt, den wir bereits installiert haben:

int[] selectedRolesArr = GetSelectedRoles(); 
if ( useAnd ) 
       { 
          predicateAnd = predicateAnd.And( u => (from c in context.CompanyRolesToUsers 
                       where selectedRolesArr.Contains(c.CompanyRoleId) 
                       select c.UserId).Contains(u.Id)); 
        } 
        else 
        { 
           predicateOr = predicateOr.Or( u => (from c in context.CompanyRolesToUsers 
                          where selectedRolesArr.Contains(c.CompanyRoleId) 
                         select c.UserId).Contains(u.Id) ); 
        } 

Das ist einem Poster im LINQtoSQL-Forum zu verdanken

marcel_g
quelle
1

Hier ist eine Version von SQL, die die richtigen Datensätze zurückgibt:

select distinct u.* 
from Users u, CompanyRolesToUsers c
where u.Id = c.UserId        --join just specified here, perfectly fine
and u.firstname like '%amy%'
and c.CompanyRoleId in (2,3,4)

Beachten Sie außerdem, dass (2,3,4) eine Liste ist, die vom Benutzer der Web-App aus einer Kontrollkästchenliste ausgewählt wurde, und ich habe vergessen zu erwähnen, dass ich dies der Einfachheit halber nur fest codiert habe. Wirklich, es ist ein Array von CompanyRoleId-Werten, also kann es (1) oder (2,5) oder (1,2,3,4,6,7,99) sein.

Das andere, was ich klarer spezifizieren sollte, ist, dass die PredicateExtensions verwendet werden, um Prädikatklauseln dynamisch zum Where für die Abfrage hinzuzufügen, abhängig davon, welche Formularfelder der Web-App-Benutzer ausgefüllt hat. Der schwierige Teil für mich ist also, wie um die Arbeitsabfrage in einen LINQ-Ausdruck umzuwandeln, den ich an die dynamische Liste der Ausdrücke anhängen kann.

Ich werde einige der Beispiel-LINQ-Abfragen ausprobieren und prüfen, ob ich sie in unseren Code integrieren kann, und dann meine Ergebnisse veröffentlichen. Vielen Dank!

marcel

marcel_g
quelle