Как выполнить левое внешнее соединение, используя методы расширения 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(???)
LaserJesus
источник

Ответы:

445

Для (левый внешний) присоединиться таблицы 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
Этот шаблон
хорош,
3
@nam Ну, тебе понадобится оператор where, x.Bar == null
Tod
2
@AbdulkarimKanaan да - SelectMany объединяет два слоя по 1-многим в 1 слой с записью на пару
Марк Гравелл
1
@MarcGravell Я предложил изменить, чтобы добавить немного объяснения того, что вы сделали в своем коде.
B -
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 соответствует типичному определению левого внешнего соединения, но, как я уже упоминал ранее, часто излишне сложно в зависимости от набора данных.

Ocelot20
источник
7
Я думаю, что "bs.SingleOrDefault ()" не будет работать, если у вас есть еще один подписчик присоединиться или включить. Нам нужен "bs.FirstOrDefault ()" в этом случае.
Дерик
3
Правда, Entity Framework и Linq to SQL оба требуют этого, поскольку они не могут легко выполнить Singleпроверку в процессе объединения. SingleOrDefaultоднако это более «правильный» способ продемонстрировать это ИМО.
Ocelot20
1
Вы должны помнить, чтобы упорядочить вашу объединенную таблицу или .FirstOrDefault () получит случайную строку из нескольких строк, которая может соответствовать критериям объединения, независимо от того, какая база данных будет найдена первой.
Крис Москини
1
@ChrisMoschini: Order и FirstOrDefault не нужны, так как пример для совпадения 0 или 1, где вы захотите потерпеть неудачу на нескольких записях (см. Комментарий над кодом).
Ocelot20
2
Это не «дополнительное требование», не уточненное в вопросе, это то, о чем многие думают, когда говорят «Left Outer Join». Кроме того, требование FirstOrDefault, на которое ссылается Дерик, - это поведение EF / L2SQL, а не объекты L2Object (ни один из них не указан в тегах). SingleOrDefault - абсолютно правильный метод для вызова в этом случае. Конечно, вы хотите выбросить исключение, если вы встретите больше записей, чем возможно для вашего набора данных, вместо того, чтобы выбрать произвольную и привести к сбивающему с толку неопределенному результату.
Ocelot20
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 to SQL правильно преобразуются в SQL. Для LINQ to Objects лучше присоединиться с помощью GroupJoin, так как он внутренне использует Lookup . Но если вы запрашиваете БД, то пропуск GroupJoin считается AFAIK быстрым.

Personlay для меня этот способ более читабелен по сравнению с GroupJoin (). SelectMany ()

Гедиминас Зимкус
источник
Это было лучше, чем .Join для меня, плюс я мог сделать свой сустав, который я хотел (right.FooId == left.FooId || right.FooId == 0)
Андерс
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);
    }
hajirazin
источник
Похоже, это будет работать (для моего требования). Можете ли вы привести пример? Я новичок в LINQ Extensions и с трудом нахожу голову над ситуацией левого соединения, в которой я нахожусь ...
Шива
@Skychan Может быть, мне нужно посмотреть на это, это старый ответ и работал в то время. Какие рамки вы используете? Я имею ввиду .NET версию?
Хаджиразин
2
Это работает для Linq to Objects, но не при запросах к базе данных, так как вам нужно работать с IQuerable и использовать вместо этого выражения выражений
Боб Вейл
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() });

В противном случае строка, которую вы получите в объединении, будет случайной (или, более конкретно, в зависимости от того, какой БД будет найден первым).

Крис Москини
источник
Это оно! Любое негарантированное отношение один к одному.
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 to Objects, мне не понравилось, что 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(s => new 
{
    FooName = s.Foo_Id.HasValue ? s.Foo.Name : "Default Value"
}

Очень просто, нет необходимости в групповом присоединении или чем-либо еще

Дейл Фрейзер
источник