Як виконати ліве зовнішнє з'єднання за допомогою методів розширення linq


272

Припустимо, що у мене є лівий зовнішній з’єднання як такий:

from f in Foo
join b in Bar on f.Foo_Id equals b.Foo_Id into g
from result in g.DefaultIfEmpty()
select new { Foo = f, Bar = result }

Як би я висловив те саме завдання за допомогою методів розширення? Напр

Foo.GroupJoin(Bar, f => f.Foo_Id, b => b.Foo_Id, (f,b) => ???)
    .Select(???)

Відповіді:


444

Для (лівий зовнішній) приєднатися таблиці Barз таблицею Fooна Foo.Foo_Id = Bar.Foo_Idлямбда нотації:

var qry = Foo.GroupJoin(
          Bar, 
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (x,y) => new { Foo = x, Bars = y })
       .SelectMany(
           x => x.Bars.DefaultIfEmpty(),
           (x,y) => new { Foo=x.Foo, Bar=y});

27
Це насправді не так вже й шалено, як здається. По суті GroupJoin, лівий зовнішній шар з'єднується, SelectManyчастина потрібна лише залежно від того, що ви хочете вибрати.
Джордж Мауер

6
Ця модель чудова тим, що Entity Framework визнає це лівим з’єднанням, що, як я вважав, було неможливим
Джесан Фафон

3
@nam Добре вам знадобиться де заява, x.Bar == null
Тод

2
@AbdulkarimKanaan так - SelectMany згладжує два шари 1-багато в один шар із входом на пару
Марк Гравелл

1
@MarcGravell Я запропонував редагувати, щоб додати трохи пояснень того, що ви зробили в фрагменті коду.
B - rian

109

Оскільки це видається фактичним питанням SO для лівих зовнішніх приєднань із використанням синтаксису методу (розширення), я подумав, що додам альтернативу до обраної на даний момент відповіді, що (як мінімум, за моїм досвідом) частіше, ніж я після

// Option 1: Expecting either 0 or 1 matches from the "Right"
// table (Bars in this case):
var qry = Foos.GroupJoin(
          Bars,
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (f,bs) => new { Foo = f, Bar = bs.SingleOrDefault() });

// Option 2: Expecting either 0 or more matches from the "Right" table
// (courtesy of currently selected answer):
var qry = Foos.GroupJoin(
                  Bars, 
                  foo => foo.Foo_Id,
                  bar => bar.Foo_Id,
                  (f,bs) => new { Foo = f, Bars = bs })
              .SelectMany(
                  fooBars => fooBars.Bars.DefaultIfEmpty(),
                  (x,y) => new { Foo = x.Foo, Bar = y });

Щоб відобразити різницю за допомогою простого набору даних (якщо припустити, що ми приєднуємось до самих значень):

List<int> tableA = new List<int> { 1, 2, 3 };
List<int?> tableB = new List<int?> { 3, 4, 5 };

// Result using both Option 1 and 2. Option 1 would be a better choice
// if we didn't expect multiple matches in tableB.
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    }

List<int> tableA = new List<int> { 1, 2, 3 };
List<int?> tableB = new List<int?> { 3, 3, 4 };

// Result using Option 1 would be that an exception gets thrown on
// SingleOrDefault(), but if we use FirstOrDefault() instead to illustrate:
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    } // Misleading, we had multiple matches.
                    // Which 3 should get selected (not arbitrarily the first)?.

// Result using Option 2:
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    }
{ A = 3, B = 3    }    

Варіант 2 вірний типовому визначенню лівого зовнішнього з'єднання, але, як я вже згадував, часто буває зайвим складним залежно від набору даних.


7
Я думаю, що "bs.SingleOrDefault ()" не буде працювати, якщо у вас є інший наступний приєднатися або включити. У цьому випадку нам потрібен "bs.FirstOrDefault ()".
Дерік

3
Правда, Entity Framework і Linq до SQL вимагають цього, оскільки вони не можуть легко здійснити Singleперевірку серед об'єднання. SingleOrDefaultпроте є більш "правильним" способом продемонструвати цей ІМО.
Ocelot20

1
Вам потрібно пам’ятати, щоб замовити приєднану таблицю або. .FirstOrDefault () отримає випадковий рядок з декількох рядків, які можуть відповідати критеріям приєднання, незалежно від того, що в базі даних станеться спочатку.
Кріс Москіні

1
@ChrisMoschini: Порядок та FirstOrDefault непотрібні, оскільки приклад - це відповідність 0 або 1, де ви хочете провалитись у кількох записах (див. Коментар вище коду).
Ocelot20

2
Це не "додаткова вимога", не визначена у запитанні, це те, про що думають багато людей, коли вони говорять "Приєднайтесь поза зоною". Також вимогою FirstOrDefault, на яку посилається Dherik, є поведінка EF / L2SQL, а не L2Objects (жодне з них не міститься в тегах). SingleOrDefault - абсолютно правильний метод для виклику в цьому випадку. Звичайно, ви хочете викинути виняток, якщо у вас з’явиться більше записів, ніж можливо для вашого набору даних, замість того, щоб вибрати довільну та призвести до заплутаного невизначеного результату.
Окелот20

52

Метод групового приєднання є непотрібним для досягнення об'єднання двох наборів даних.

Внутрішнє з'єднання:

var qry = Foos.SelectMany
            (
                foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id),
                (foo, bar) => new
                    {
                    Foo = foo,
                    Bar = bar
                    }
            );

Для лівого приєднання просто додайте DefaultIfEmpty ()

var qry = Foos.SelectMany
            (
                foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id).DefaultIfEmpty(),
                (foo, bar) => new
                    {
                    Foo = foo,
                    Bar = bar
                    }
            );

EF і LINQ в SQL правильно трансформуються в SQL. Для LINQ до об'єктів краще приєднатися за допомогою GroupJoin, оскільки він використовує внутрішньо Lookup . Але якщо ви запитуєте DB, тоді пропуск GroupJoin є AFAIK як виконавцем.

Персоналіз для мене таким чином читає порівняно з GroupJoin (). SelectMany ()


Це було краще, ніж для мене. Приєднуйтесь до мене, плюс я можу зробити свій континентальний суглоб, який хотів (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql перекладає цей підхід як ліве з'єднання. ця відповідь краща і простіша. +1
Гвідо Моча

15

Ви можете створити такий метод розширення, як:

public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source, IEnumerable<TInner> other, Func<TSource, TKey> func, Func<TInner, TKey> innerkey, Func<TSource, TInner, TResult> res)
    {
        return from f in source
               join b in other on func.Invoke(f) equals innerkey.Invoke(b) into g
               from result in g.DefaultIfEmpty()
               select res.Invoke(f, result);
    }

Схоже, це спрацювало (на мою вимогу). Чи можете ви навести приклад? Я новачок у розширеннях LINQ і мені важко обернути голову навколо цієї лівої приєднатися до ситуації, в якій я перебуваю ...
Шива

@Skychan Можливо, мені потрібно на це подивитися, це стара відповідь і працював у той час. Яку рамку ви використовуєте? Я маю на увазі версію .NET?
Хаджиразін

2
Це працює для Linq to Objects, але не під час запитів у базу даних, оскільки вам потрібно працювати над IQuerable і використовувати замість цього вирази Funcs
Bob Vale

4

Удосконалюючись у відповіді Ocelot20, якщо у вас є таблиця, у вас залишилося зовнішнє об'єднання, де ви просто хочете 0 або 1 рядків з неї, але вона може бути декількома, вам потрібно замовити свою приєднану таблицю:

var qry = Foos.GroupJoin(
      Bars.OrderByDescending(b => b.Id),
      foo => foo.Foo_Id,
      bar => bar.Foo_Id,
      (f, bs) => new { Foo = f, Bar = bs.FirstOrDefault() });

В іншому випадку, який рядок ви отримаєте в об'єднанні, буде випадковим (або, точніше, залежно від того, що в першу чергу знайде db).


Це воно! Будь-яке ненадійне відношення один до одного.
it3xl

2

Перетворивши відповідь Марка Гравелла в метод розширення, я зробив наступне.

internal static IEnumerable<Tuple<TLeft, TRight>> LeftJoin<TLeft, TRight, TKey>(
    this IEnumerable<TLeft> left,
    IEnumerable<TRight> right,
    Func<TLeft, TKey> selectKeyLeft,
    Func<TRight, TKey> selectKeyRight,
    TRight defaultRight = default(TRight),
    IEqualityComparer<TKey> cmp = null)
{
    return left.GroupJoin(
            right,
            selectKeyLeft,
            selectKeyRight,
            (x, y) => new Tuple<TLeft, IEnumerable<TRight>>(x, y),
            cmp ?? EqualityComparer<TKey>.Default)
        .SelectMany(
            x => x.Item2.DefaultIfEmpty(defaultRight),
            (x, y) => new Tuple<TLeft, TRight>(x.Item1, y));
}

2

Незважаючи на те, що прийнята відповідь працює і добре для Linq для об'єктів, вона помилила мене, що SQL-запит не є просто прямим лівим зовнішнім приєднанням.

Наступний код покладається на проект LinkKit, який дозволяє передавати вирази та викликати їх у вашому запиті.

static IQueryable<TResult> LeftOuterJoin<TSource,TInner, TKey, TResult>(
     this IQueryable<TSource> source, 
     IQueryable<TInner> inner, 
     Expression<Func<TSource,TKey>> sourceKey, 
     Expression<Func<TInner,TKey>> innerKey, 
     Expression<Func<TSource, TInner, TResult>> result
    ) {
    return from a in source.AsExpandable()
            join b in inner on sourceKey.Invoke(a) equals innerKey.Invoke(b) into c
            from d in c.DefaultIfEmpty()
            select result.Invoke(a,d);
}

Його можна використовувати наступним чином

Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});

-1

Існує просте рішення цього питання

Просто використовуйте .HasValue у своєму Select

.Select(s => new 
{
    FooName = s.Foo_Id.HasValue ? s.Foo.Name : "Default Value"
}

Дуже просто, немає потреби в груповому приєднанні чи нічого іншого

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.