Bagaimana Anda melakukan gabungan luar kiri menggunakan metode ekstensi LINQ

272

Dengan asumsi saya memiliki gabung luar kiri sebagai berikut:

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 }

Bagaimana cara saya mengungkapkan tugas yang sama menggunakan metode ekstensi? Misalnya

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

Jawaban:

445

Untuk (kiri luar) bergabung dari meja Bardengan meja Foodi Foo.Foo_Id = Bar.Foo_Iddalam notasi lambda:

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});
Marc Gravell
sumber
27
Ini sebenarnya hampir tidak gila seperti yang terlihat. Pada dasarnya GroupJoinbagian luar kiri bergabung, SelectManybagian itu hanya diperlukan tergantung pada apa yang ingin Anda pilih.
George Mauer
6
Pola ini sangat bagus karena Kerangka Entitas mengenalinya sebagai Left Join, yang dulu saya yakini sebagai ketidakmungkinan
Jesan Fafon
3
@nam Yah Anda akan membutuhkan pernyataan where, x.Bar == null
Tod
2
@AbdulkarimKanaan ya - SelectMany meratakan dua lapisan 1-banyak menjadi 1 lapisan dengan entri per pasangan
Marc Gravell
1
@MarcGravell Saya menyarankan suntingan untuk menambahkan sedikit penjelasan tentang apa yang Anda lakukan dalam kode fragement Anda.
B - rian
109

Karena ini tampaknya menjadi pertanyaan SO de facto untuk gabungan luar kiri menggunakan metode (ekstensi) sintaks, saya pikir saya akan menambahkan alternatif untuk jawaban yang dipilih saat ini bahwa (dalam pengalaman saya setidaknya) telah lebih umum daripada setelah

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

Untuk menampilkan perbedaan menggunakan kumpulan data sederhana (dengan asumsi kita sendiri menggabungkan nilai-nilai itu):

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    }    

Opsi 2 berlaku untuk definisi gabungan luar kiri tipikal, tetapi seperti yang saya sebutkan sebelumnya seringkali tidak perlu tergantung pada kumpulan data.

Ocelot20
sumber
7
Saya pikir "bs.SingleOrDefault ()" tidak akan berfungsi jika Anda memiliki Gabung atau Sertakan berikut. Kami membutuhkan "bs.FirstOrDefault ()" dalam kasus ini.
Dherik
3
Benar, Entity Framework dan Linq to SQL keduanya mengharuskan karena mereka tidak dapat dengan mudah melakukan Singlepemeriksaan di tengah bergabung. SingleOrDefaultnamun merupakan cara yang lebih "benar" untuk menunjukkan IMO ini.
Ocelot20
1
Anda harus ingat untuk Memesan tabel bergabung Anda atau .FirstOrDefault () akan mendapatkan baris acak dari beberapa baris yang mungkin cocok dengan kriteria bergabung, apa pun yang ditemukan oleh database terlebih dahulu.
Chris Moschini
1
@ChrisMoschini: Urutan dan FirstOrDefault tidak perlu karena contohnya adalah untuk pertandingan 0 atau 1 di mana Anda ingin gagal pada banyak rekaman (lihat komentar kode di atas).
Ocelot20
2
Ini bukan "persyaratan tambahan" yang tidak ditentukan dalam pertanyaan, ini adalah apa yang dipikirkan banyak orang ketika mereka mengatakan "Left Outer Join". Juga, persyaratan FirstOrDefault yang dirujuk oleh Dherik adalah perilaku EF / L2SQL dan bukan L2Objects (keduanya tidak ada dalam tag). SingleOrDefault benar-benar metode yang tepat untuk memanggil dalam kasus ini. Tentu saja Anda ingin melempar pengecualian jika Anda menemukan lebih banyak catatan daripada yang mungkin untuk kumpulan data Anda alih-alih memilih yang sewenang-wenang dan mengarah ke hasil yang tidak jelas yang membingungkan.
Ocelot20
52

Metode Group Join tidak diperlukan untuk mencapai penggabungan dua set data.

Bergabunglah dengan Batin:

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

Untuk Gabung Kiri cukup tambahkan DefaultIfEmpty ()

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

EF dan LINQ menjadi SQL dengan benar bertransformasi menjadi SQL. Untuk LINQ to Objects, lebih baik bergabung menggunakan GroupJoin karena secara internal menggunakan Pencarian . Tetapi jika Anda meminta DB maka melewatkan GroupJoin adalah AFAIK sebagai pemain.

Personlay untuk saya dengan cara ini lebih mudah dibaca dibandingkan dengan GroupJoin (). SelectMany ()

Gediminas Zimkus
sumber
Ini tampil lebih baik daripada. Bergabunglah dengan saya, ditambah lagi saya bisa melakukan conditonal joint yang saya inginkan (right.FooId == left.FooId || right.FooId == 0)
Anders
linq2sql menerjemahkan pendekatan ini sebagai gabung kiri. jawaban ini lebih baik dan lebih sederhana. +1
Guido Mocha
15

Anda dapat membuat metode ekstensi seperti:

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
sumber
Ini sepertinya bekerja (untuk kebutuhan saya). Bisakah Anda memberikan contoh? Saya baru mengenal LINQ Extensions dan mengalami kesulitan membungkus kepala saya dengan situasi Gabung Kiri ini. Saya berada di ...
Shiva
@Skychan Mungkin aku perlu melihatnya, itu jawaban lama dan sedang bekerja saat itu. Kerangka mana yang Anda gunakan? Maksud saya. Versi NET?
hajirazin
2
Ini berfungsi untuk Linq to Objects tetapi tidak ketika meminta database yang Anda butuhkan untuk beroperasi pada IQuerable dan menggunakan Expressions of Funcs sebagai gantinya
Bob Vale
4

Memperbaiki jawaban Ocelot20, jika Anda memiliki tabel, Anda tidak perlu bergabung dengan tempat di mana Anda hanya ingin 0 atau 1 baris darinya, tetapi bisa memiliki beberapa, Anda harus Memesan tabel bergabung Anda:

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

Kalau tidak, baris mana yang Anda dapatkan di join akan menjadi acak (atau lebih spesifik, mana db yang terjadi untuk menemukan pertama).

Chris Moschini
sumber
Itu dia! Setiap hubungan satu ke satu yang tidak dijamin.
it3xl
2

Mengubah jawaban Marc Gravell menjadi metode ekstensi, saya membuat yang berikut ini.

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));
}
Harley Waldstein
sumber
2

Sementara jawaban yang diterima bekerja dan bagus untuk Linq to Objects, saya disadap bahwa query SQL bukan hanya Left Outer Join yang lurus.

Kode berikut ini bergantung pada Proyek LinkKit yang memungkinkan Anda untuk menyampaikan ekspresi dan memohonnya ke permintaan Anda.

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

Dapat digunakan sebagai berikut

Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});
Bob Vale
sumber
-1

Ada solusi mudah untuk ini

Cukup gunakan .HasValue di Select Anda

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

Sangat mudah, tidak perlu untuk bergabung dengan grup atau apa pun

Dale Fraser
sumber