LINQ to SQL - lewe sprzężenie zewnętrzne z wieloma warunkami sprzężenia

148

Mam następujący kod SQL, który próbuję przetłumaczyć na LINQ:

SELECT f.value
FROM period as p 
LEFT OUTER JOIN facts AS f ON p.id = f.periodid AND f.otherid = 17
WHERE p.companyid = 100

Widziałem typową implementację lewego sprzężenia zewnętrznego (tj. into x from y in x.DefaultIfEmpty()Itd . ), Ale nie jestem pewien, jak wprowadzić inny warunek sprzężenia ( AND f.otherid = 17)

EDYTOWAĆ

Dlaczego AND f.otherid = 17warunek jest częścią JOIN zamiast w klauzuli WHERE? Ponieważ fmoże nie istnieć dla niektórych wierszy i nadal chcę, aby te wiersze były uwzględnione. Jeśli warunek zostanie zastosowany w klauzuli WHERE, po JOIN - wtedy nie otrzymam takiego zachowania.

Niestety to:

from p in context.Periods
join f in context.Facts on p.id equals f.periodid into fg
from fgi in fg.DefaultIfEmpty()
where p.companyid == 100 && fgi.otherid == 17
select f.value

wydaje się być równoważne z tym:

SELECT f.value
FROM period as p 
LEFT OUTER JOIN facts AS f ON p.id = f.periodid 
WHERE p.companyid = 100 AND f.otherid = 17

co nie jest tym, czego szukam.

dan
źródło
Słodkie! Szukałem tego od jakiegoś czasu, ale nie byłem pewien, jak to znaleźć. Nie wiem, jak dodać tagi do tej odpowiedzi. Oto kryteria wyszukiwania, których użyłem: filtr linq do sql w złączeniu lub z linq do sql, gdzie klauzula w złączeniu lub z
Solburn

Odpowiedzi:

243

Zanim zadzwonisz, musisz podać swój warunek dołączenia DefaultIfEmpty(). Po prostu użyłbym składni metody rozszerzenia:

from p in context.Periods
join f in context.Facts on p.id equals f.periodid into fg
from fgi in fg.Where(f => f.otherid == 17).DefaultIfEmpty()
where p.companyid == 100
select f.value

Lub możesz użyć podzapytania:

from p in context.Periods
join f in context.Facts on p.id equals f.periodid into fg
from fgi in (from f in fg
             where f.otherid == 17
             select f).DefaultIfEmpty()
where p.companyid == 100
select f.value
dahlbyk
źródło
1
Dziękujemy za udostępnienie kwalifikatora .Where w instrukcji from .... defaultifempty. Nie wiedziałem, że możesz to zrobić.
Frank Thomas
28

to też działa, ... jeśli masz wiele złączeń kolumn

from p in context.Periods
join f in context.Facts 
on new {
    id = p.periodid,
    p.otherid
} equals new {
    f.id,
    f.otherid
} into fg
from fgi in fg.DefaultIfEmpty()
where p.companyid == 100
select f.value
ZenXavier
źródło
12

Wiem, że jest " trochę późno ", ale na wszelki wypadek, gdyby ktoś musiał to zrobić w składni metody LINQ ( dlatego początkowo znalazłem ten post ), oto jak to zrobić:

var results = context.Periods
    .GroupJoin(
        context.Facts,
        period => period.id,
        fk => fk.periodid,
        (period, fact) => fact.Where(f => f.otherid == 17)
                              .Select(fact.Value)
                              .DefaultIfEmpty()
    )
    .Where(period.companyid==100)
    .SelectMany(fact=>fact).ToList();
Prokurors
źródło
2
Bardzo przydatne, aby zobaczyć wersję lambda!
Uczeń
2
.Select(fact.Value)powinno być.Select(f => f.Value)
Petr Felzmann
5

Inną prawidłową opcją jest rozłożenie sprzężeń na wiele klauzul LINQ w następujący sposób:

public static IEnumerable<Announcementboard> GetSiteContent(string pageName, DateTime date)
{
    IEnumerable<Announcementboard> content = null;
    IEnumerable<Announcementboard> addMoreContent = null;
        try
        {
            content = from c in DB.Announcementboards
              // Can be displayed beginning on this date
              where c.Displayondate > date.AddDays(-1)
              // Doesn't Expire or Expires at future date
              && (c.Displaythrudate == null || c.Displaythrudate > date)
              // Content is NOT draft, and IS published
              && c.Isdraft == "N" && c.Publishedon != null
              orderby c.Sortorder ascending, c.Heading ascending
              select c;

            // Get the content specific to page names
            if (!string.IsNullOrEmpty(pageName))
            {
              addMoreContent = from c in content
                  join p in DB.Announceonpages on c.Announcementid equals p.Announcementid
                  join s in DB.Apppagenames on p.Apppagenameid equals s.Apppagenameid
                  where s.Apppageref.ToLower() == pageName.ToLower()
                  select c;
            }

            // Add the specified content using UNION
            content = content.Union(addMoreContent);

            // Exclude the duplicates using DISTINCT
            content = content.Distinct();

            return content;
        }
    catch (MyLovelyException ex)
    {
        // Add your exception handling here
        throw ex;
    }
}
MAbraham1
źródło
czy nie byłoby to wolniejsze niż wykonanie całej operacji w jednym zapytaniu linq?
Umar T.
@ umar-t, najprawdopodobniej tak, biorąc pod uwagę, że było to ponad osiem lat temu, kiedy to pisałem. Osobiście podoba mi się skorelowane zapytanie podrzędne postulowane przez Dahlbyka tutaj stackoverflow.com/a/1123051/212950
MAbraham1
1
„Połączenie” to inna operacja niż „połączenie krzyżowe”. To jest jak dodawanie kontra mnożenie.
Suncat2000
1
@ Suncat2000, dziękuję za korektę. Wesołego Święta Dziękczynienia! 👪🦃🙏
MAbraham1
0

Można zapisać przy użyciu złożonego klucza złączenia. Ponadto jeśli istnieje potrzeba wybrania właściwości z lewej i prawej strony, LINQ można zapisać jako

var result = context.Periods
    .Where(p => p.companyid == 100)
    .GroupJoin(
        context.Facts,
        p => new {p.id, otherid = 17},
        f => new {id = f.periodid, f.otherid},
        (p, f) => new {p, f})
    .SelectMany(
        pf => pf.f.DefaultIfEmpty(),
        (pf, f) => new MyJoinEntity
        {
            Id = pf.p.id,
            Value = f.value,
            // and so on...
        });
Petr Felzmann
źródło
-1

Wydaje mi się, że rozważenie kilku zmian w kodzie SQL przed próbą przetłumaczenia ma wartość.

Osobiście napisałbym takie zapytanie jako sumę (chociaż całkowicie unikałbym wartości null!):

SELECT f.value
  FROM period as p JOIN facts AS f ON p.id = f.periodid
WHERE p.companyid = 100
      AND f.otherid = 17
UNION
SELECT NULL AS value
  FROM period as p
WHERE p.companyid = 100
      AND NOT EXISTS ( 
                      SELECT * 
                        FROM facts AS f
                       WHERE p.id = f.periodid
                             AND f.otherid = 17
                     );

Więc myślę, że zgadzam się z duchem odpowiedzi @ MAbraham1 (choć ich kod wydaje się nie mieć związku z pytaniem).

Wygląda jednak na to, że kwerenda została specjalnie zaprojektowana w celu uzyskania wyniku w jednej kolumnie zawierającego zduplikowane wiersze - w rzeczywistości zduplikowane wartości null! Trudno nie dojść do wniosku, że to podejście jest błędne.

onedaywhen
źródło