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表とFooFoo.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ます。パーツは、何を選択するかによってのみ必要になります。
ジョージマウアー2014年

6
Entity Frameworkがそれを左結合として認識しているため、このパターンは素晴らしいです。これは、以前は不可能だと信じていました
Jesan Fafon

3
@namさて、whereステートメントが必要です。x.Bar== null
Tod

2
@AbdulkarimKanaanはい-SelectManyは、1から2のレイヤーを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()」は、次のJoinまたはIncludeがある場合は機能しないと思います。この場合、「bs.FirstOrDefault()」が必要です。
Dherik、2015

3
確かに、Entity FrameworkとLinq to SQLはどちらもSingle、結合中にチェックを簡単に行うことができないため、どちらも必要です。SingleOrDefaultただし、このIMOを実証するためのより「正しい」方法です。
Ocelot20、2015

1
結合テーブルを注文することを覚えておく必要があります。そうしないと、データベースが最初に見つけたものは何でも、.FirstOrDefault()は結合基準に一致する可能性のある複数の行からランダムな行を取得します。
Chris Moschini、2015年

1
@ChrisMoschini:複数のレコードで失敗したい場合の例は0または1の一致であるため、OrderとFirstOrDefaultは不要です(コードの上のコメントを参照)。
Ocelot20、2015年

2
これは、質問で指定されていない「特別な要件」ではなく、「左外部結合」と言うときに多くの人が考えていることです。また、Dherikが参照するFirstOrDefault要件はEF / L2SQLの動作であり、L2Objectsではありません(これらはいずれもタグ内にありません)。この場合は、SingleOrDefaultが絶対に正しいメソッドです。もちろん、任意のレコードを選択して未定義の結果を混乱させるのではなく、データセットのレコードが可能な数よりも多い場合は、例外をスローする必要があります。
Ocelot20、2015年

52

2つのデータセットを結合するために、グループ結合メソッドは不要です。

内部結合:

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では、内部的にLookupを使用するため、GroupJoinを使用して参加するのが賢明です。ただし、DBにクエリを実行している場合、GroupJoinのスキップはパフォーマンスとしてAFAIKです。

この方法での私用のPersonlayは、GroupJoin()。SelectMany()に比べて読みやすくなっています。


これは、.Joinよりもパフォーマンスが優れています。さらに、私が望む条件付きジョイントを実行できました(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 Extensionsを初めて使用し、この左結合の状況に頭を抱えるのに苦労しています...
Shiva

@Skychan私はそれを見る必要があるかもしれません、それは古い答えでその時に働いていました。どのフレームワークを使用していますか?.NETバージョンですか?
ハジラジン

2
これはLinq to Objectsでは機能しますが、IQuerableを操作して代わりにFuncs of 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が最初に見つけた方)。


それでおしまい!保証されていない1対1の関係。
it3xl 2017

2

Marc Gravellの答えを拡張メソッドに変えて、次のようにしました。

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

これには簡単な解決策があります

Selectで.HasValueを使用するだけです

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

非常に簡単で、グループ参加やその他の必要はありません

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.