คุณจะทำการรวมภายนอกด้านซ้ายโดยใช้วิธีการขยาย 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(???)

คำตอบ:


444

สำหรับการเข้าร่วม (ด้านนอกซ้าย) ของตาราง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ส่วนที่จำเป็นเท่านั้นขึ้นอยู่กับสิ่งที่คุณต้องการเลือก
George Mauer

6
รูปแบบนี้ดีมากเพราะ Entity Framework รับรู้ว่ามันเป็น Left Join ซึ่งฉันเคยเชื่อว่าเป็นไปไม่ได้
Jesan Fafon

3
@nam ดีที่คุณจะต้องที่คำสั่ง x.Bar == null
ท็อด

2
@AbdulkarimKanaan ใช่ - SelectMany แบนสองชั้นของ 1-many เป็น 1 เลเยอร์โดยมีค่าต่อคู่
Marc Gravell

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 ()" จะไม่ทำงานหากคุณมีการเข้าร่วมหรือรวมต่อไปนี้อีก เราต้องการ "bs.FirstOrDefault ()" ในกรณีนี้
Dherik

3
True, Entity Framework และ Linq to SQL จำเป็นต้องมีเนื่องจากพวกเขาไม่สามารถทำการSingleตรวจสอบได้อย่างง่ายดายท่ามกลางการเข้าร่วม SingleOrDefaultอย่างไรก็ตามเป็นวิธีที่ "ถูกต้อง" มากขึ้นในการสาธิต IMO นี้
Ocelot20

1
คุณต้องจำไว้ว่าให้สั่งซื้อตารางที่คุณเข้าร่วมหรือ .FirstOrDefault () กำลังจะได้แถวสุ่มจากหลายแถวที่อาจตรงกับเกณฑ์การเข้าร่วมไม่ว่าฐานข้อมูลจะเกิดขึ้นในการค้นหาครั้งแรก
Chris Moschini

1
@ChrisMoschini: การสั่งซื้อและ FirstOrDefault นั้นไม่จำเป็นเนื่องจากตัวอย่างสำหรับการจับคู่ 0 หรือ 1 ที่คุณต้องการล้มเหลวในหลาย ๆ ระเบียน (ดูความคิดเห็นด้านบนรหัส)
Ocelot20 20

2
นี่ไม่ใช่ "ข้อกำหนดพิเศษ" ที่ไม่ได้ระบุในคำถาม แต่เป็นสิ่งที่ผู้คนจำนวนมากคิดว่าเมื่อพวกเขาพูดว่า "เข้าร่วมด้านนอก" นอกจากนี้ข้อกำหนด FirstOrDefault ที่อ้างถึงโดย Dherik คือพฤติกรรมของ EF / L2SQL และไม่ใช่ L2Objects (สิ่งเหล่านี้ไม่ได้อยู่ในแท็ก) SingleOrDefault เป็นวิธีที่ถูกต้องในการโทรในกรณีนี้ แน่นอนคุณต้องการที่จะโยนข้อยกเว้นถ้าคุณพบระเบียนมากกว่าที่เป็นไปได้สำหรับชุดข้อมูลของคุณแทนที่จะเลือกหนึ่งโดยพลการและนำไปสู่ผลลัพธ์ที่ไม่ได้กำหนดสับสน
Ocelot20 20

52

วิธีการเข้าร่วมกลุ่มไม่จำเป็นต้องมีการรวมกลุ่มของข้อมูลสองชุด

เข้าร่วมภายใน:

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

สำหรับ Left Join เพียงเพิ่ม 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 เป็น SQL สามารถแปลงเป็น SQL ได้อย่างถูกต้อง สำหรับ LINQ to Objects จะต้องมีการเข้าร่วมโดยใช้ GroupJoin เนื่องจากเป็นการใช้การค้นหาภายใน แต่ถ้าคุณกำลังสืบค้น DB การข้าม GroupJoin เป็น AFAIK ในฐานะนักแสดง

Personlay สำหรับฉันด้วยวิธีนี้สามารถอ่านได้มากกว่าเมื่อเทียบกับ GroupJoin () SelectMany ()


perfomed นี้ดีกว่า. เข้าร่วมสำหรับฉันบวกฉันสามารถทำข้อต่อ conditonal ของฉันที่ฉันต้องการ (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql แปลวิธีการนี้เป็นซ้ายเข้าร่วม คำตอบนี้ดีกว่าและง่ายกว่า +1
Guido Mocha

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 และกำลังลำบากในการห่อหัวของฉันรอบสถานการณ์ด้านซ้ายนี้เข้าร่วมฉันมา ...
Shiva

@Skychan อาจเป็นเพราะฉันต้องดูมันเป็นคำตอบเก่าและทำงานในเวลานั้น คุณใช้ Framework ใด ฉันหมายถึง. รุ่น NET?
hajirazin

2
ใช้งานได้กับ Linq to Objects แต่ไม่เมื่อทำการสืบค้นฐานข้อมูลเนื่องจากคุณต้องใช้ IQuerable และใช้ Expressions 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() });

มิฉะนั้นแถวที่คุณได้รับในการเข้าร่วมจะเป็นแบบสุ่ม (หรือเฉพาะเจาะจงมากขึ้นแล้วแต่ว่าฐานข้อมูลใดที่เกิดขึ้นเพื่อค้นหาก่อน)


แค่นั้นแหละ! ใด ๆ ที่ไม่รับประกันความสัมพันธ์แบบหนึ่งต่อหนึ่ง
it3xl

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 ไม่ได้เป็นเพียงแค่ Left Left Join เข้าร่วมเท่านั้น

รหัสต่อไปนี้อาศัยโครงการ 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"
}

ง่ายมากไม่จำเป็นต้องเข้าร่วมกลุ่มหรืออะไรเลย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.