Linq genişletme yöntemlerini kullanarak sol dış birleşimi nasıl gerçekleştirirsiniz?


272

Sol bir dış birleşim olduğu varsayılarak:

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 }

Aynı görevi uzantı yöntemlerini kullanarak nasıl ifade edebilirim? Örneğin

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

Yanıtlar:


444

Bir (dış sol) bir tablonun birleştirme için Barbir tablo ile Fooilgili Foo.Foo_Id = Bar.Foo_Idlambda gösterimde:

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
Bu aslında göründüğü kadar çılgın değil. Temel olarak GroupJoinsol dış birleşim yapar, SelectManyparça sadece seçmek istediğiniz şeye bağlı olarak gereklidir.
George Mauer

6
Bu model harika çünkü Entity Framework bunu bir İmkansızlık olduğuna inandığım bir Sol Katılma olarak tanıdı
Jesan Fafon

3
@nam Nerede bir deyime ihtiyacınız var, x.Bar == null
Tod

2
@AbdulkarimKanaan yes - SelectMany 1 katın iki katmanını çift başına bir girişle 1 kat halinde düzleştirir
Marc Gravell

1
@MarcGravell Kod parçanızda yaptıklarınıza biraz açıklama eklemek için bir düzenleme önerdim.
B - rian

109

Bu yöntem (uzantı) sözdizimini kullanarak sol dış birleşimler için de facto SO soru gibi göründüğü için, (en azından benim deneyimimde) daha yaygın olarak ne olduğunu şu anda seçili cevaba bir alternatif ekleyeceğini düşündüm sonra

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

Basit bir veri kümesi kullanarak farkı görüntülemek için (değerlerin kendilerine katıldığımızı varsayarak):

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    }    

Seçenek 2, tipik sol dış birleşim tanımı için doğrudur, ancak daha önce de belirttiğim gibi, veri setine bağlı olarak genellikle gereksiz karmaşıktır.


7
"Bs.SingleOrDefault ()", Join veya Include'dan başka bir sürümünüz varsa işe yaramayacağını düşünüyorum. Bu durumlarda "bs.FirstOrDefault ()" işlevine ihtiyacımız var.
Dherik

3
Doğru, Entity Framework ve Linq to SQL, Singlebir birleşimin ortasında kontrolü kolayca yapamadıklarını gerektirir . SingleOrDefaultancak bu IMO'yu göstermenin daha "doğru" bir yoludur.
Ocelot20

1
Birleştirilen tablonuzu sipariş etmeyi veya .FirstOrDefault () yöntemini, veritabanının ilk bulduğu şey ne olursa olsun, birleştirme ölçütleriyle eşleşebilecek birden çok satırdan rastgele bir satır alacağını unutmayın.
Chris Moschini

1
@ChrisMoschini: Order ve FirstOrDefault gereksizdir çünkü örnek birden fazla kayıtta başarısız olmak istediğiniz 0 veya 1 eşleşmesi içindir (kodun üstündeki açıklamaya bakınız).
Ocelot20

2
Bu, soruda belirtilmeyen bir "ekstra gereksinim" değil, birçok insan "Sol Dış Katılma" derken böyle düşünüyor. Ayrıca, Dherik tarafından atıfta bulunulan FirstOrDefault gereksinimi L2Objects değil EF / L2SQL davranışıdır (bunların hiçbiri etiketlerde değildir). SingleOrDefault kesinlikle bu durumda çağrılacak doğru yöntemdir. Elbette, rastgele bir tane seçmek ve kafa karıştırıcı tanımlanmamış bir sonuca götürmek yerine veri kümeniz için mümkün olandan daha fazla kayıtla karşılaşırsanız bir istisna atmak istersiniz.
Ocelot20

52

İki veri kümesinin birleştirilmesi için Grup Birleştirme yöntemi gerekli değildir.

İç birleşim:

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

Left Join için sadece DefaultIfEmpty () ekleyin

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

EF ve LINQ to SQL doğru SQL'e dönüşür. LINQ to Objects için dahili olarak Lookup kullandığından GroupJoin kullanarak katılmak daha iyidir . Ama DB sorgulamak o zaman GroupJoin atlama performans olarak AFAIK olduğunu.

Personlay benim için bu şekilde GroupJoin () ile karşılaştırıldığında daha okunabilir.


Bu benim için bir .Join daha iyi perfomed, artı ben istediğim koşullu eklem yapabilir (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql bu yaklaşımı sol birleşim olarak çevirir. bu cevap daha iyi ve daha basittir. +1
Guido Mocha

15

Aşağıdakiler gibi bir uzantı yöntemi oluşturabilirsiniz:

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

Bu işe yarayacak gibi görünüyor (benim gereksinimim için). Bir örnek verebilir misiniz? LINQ Uzantıları için yeniyim ve başımı bu Sol Katılma durumunun etrafına sararak zorlanıyorum ...
Shiva

@Skychan Belki bakmam gerek, eski cevap ve o zamanlar çalışıyordu. Hangi Çerçeveyi kullanıyorsunuz? .NET sürümü mü yani?
hajirazin

2
Bu, Linq to Objects için çalışır, ancak bir veritabanını sorgularken IQuerable üzerinde çalışmanız ve bunun yerine İfadelerin İfadelerini kullanmanız gerekmez
Bob Vale

4

Ocelot20'nin cevabını iyileştirerek, dışarda sadece 0 veya 1 satır olmasını istediğiniz yerde bir masanız varsa, ancak birden fazla olabilir, birleştirilmiş tablonuzu sipariş etmeniz gerekir:

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

Aksi takdirde, birleştirmede hangi satırı elde edeceğiniz rastgele olacaktır (veya daha spesifik olarak, db hangisini ilk bulursa).


Bu kadar! Garantisiz bire bir ilişki.
it3xl

2

Marc Gravell'in cevabını bir uzatma yöntemine çevirerek aşağıdakileri yaptım.

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

Kabul edilen cevap işe yarar ve Linq to Objects için iyi olsa da, SQL sorgusu sadece düz bir Sol Dış Birleştirme değil beni uyandırdı.

Aşağıdaki kod, ifadeleri iletmenize ve bunları sorgunuza çağırmanıza izin veren LinkKit Project'e dayanır .

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

Aşağıdaki gibi kullanılabilir

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

-1

Bunun kolay bir çözümü var

Seçiminizde .HasValue kullanın

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

Çok kolay, gruplaşmaya veya başka bir şeye gerek yok

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.