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

Ответы:


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 соответствует типичному определению левого внешнего соединения, но, как я уже упоминал ранее, часто излишне сложно в зависимости от набора данных.


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);
    }

Похоже, это будет работать (для моего требования). Можете ли вы привести пример? Я новичок в 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"
}

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.