Làm thế nào để bạn thực hiện nối ngoài trái bằng các phương thức mở rộng linq


272

Giả sử tôi có một tham gia bên ngoài bên trái như vậy:

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 }

Làm thế nào tôi có thể diễn tả cùng một nhiệm vụ bằng cách sử dụng các phương thức mở rộng? Ví dụ

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

Câu trả lời:


443

Đối với một (bên trái bên ngoài) tham gia của một bảng Barvới một bảng Footrên Foo.Foo_Id = Bar.Foo_Idký hiệu 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});

27
Điều này thực sự không gần như điên rồ như nó có vẻ. Về cơ bản GroupJoinkhông tham gia bên ngoài bên trái, SelectManyphần chỉ cần thiết tùy thuộc vào những gì bạn muốn chọn.
George Mauer

6
Mô hình này rất tuyệt vời vì Entity Framework nhận ra nó là một Tham gia trái, điều mà tôi từng tin là không thể thực hiện được
Jesan Fafon

3
@nam Vâng, bạn cần một câu lệnh where, x.Bar == null
Tod

2
@AbdulkarimKanaan có - ChọnMany làm phẳng hai lớp 1 thành nhiều lớp với một mục nhập cho mỗi cặp
Marc Gravell

1
@MarcGravell Tôi đã đề xuất một chỉnh sửa để thêm một chút giải thích về những gì bạn đã làm trong đoạn mã của mình.
B - rian

108

Vì đây có vẻ là câu hỏi SO thực tế cho các phép nối ngoài trái bằng cách sử dụng cú pháp phương thức (phần mở rộng), tôi nghĩ rằng tôi sẽ thêm một câu trả lời cho câu trả lời hiện được chọn mà ít nhất là theo kinh nghiệm của tôi sau

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

Để hiển thị sự khác biệt bằng cách sử dụng một tập dữ liệu đơn giản (giả sử chúng ta đang tham gia vào chính các giá trị):

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    }    

Tùy chọn 2 đúng với định nghĩa nối ngoài bên trái điển hình, nhưng như tôi đã đề cập trước đó thường phức tạp không cần thiết tùy thuộc vào tập dữ liệu.


7
Tôi nghĩ rằng "bs.SingleOrDefault ()" sẽ không hoạt động nếu bạn có một mục khác sau Tham gia hoặc Bao gồm. Chúng tôi cần "bs.FirstOrDefault ()" trong trường hợp này.
Dherik

3
Đúng, Entity Framework và Linq to SQL đều yêu cầu rằng vì họ không thể dễ dàng thực hiện Singlekiểm tra trong khi tham gia. SingleOrDefaulttuy nhiên là một cách "chính xác" hơn để chứng minh IMO này.
Ocelot20

1
Bạn cần nhớ để đặt hàng bảng đã tham gia của bạn hoặc .FirstOrDefault () sẽ nhận được một hàng ngẫu nhiên từ nhiều hàng có thể phù hợp với tiêu chí tham gia, bất kể cơ sở dữ liệu nào xảy ra trước tiên.
Chris Moschini

1
@ChrisMoschini: Đặt hàng và FirstOrDefault là không cần thiết vì ví dụ này là cho một trận đấu 0 hoặc 1 trong đó bạn muốn thất bại trên nhiều bản ghi (xem mã nhận xét ở trên).
Ocelot20

2
Đây không phải là một "yêu cầu bổ sung" không được chỉ định trong câu hỏi, đó là điều mà nhiều người nghĩ đến khi họ nói "Left Outer Join". Ngoài ra, yêu cầu FirstOrDefault được Dherik đề cập là hành vi của EF / L2Query chứ không phải L2Objects (cả hai điều này đều không có trong các thẻ). SingleOrDefault hoàn toàn là phương thức chính xác để gọi trong trường hợp này. Tất nhiên, bạn muốn đưa ra một ngoại lệ nếu bạn gặp nhiều bản ghi hơn mức có thể cho tập dữ liệu của mình thay vì chọn một bản ghi tùy ý và dẫn đến một kết quả không xác định khó hiểu.
Ocelot20

52

Phương pháp nhóm tham gia là không cần thiết để đạt được tham gia của hai bộ dữ liệu.

Tham gia nội bộ:

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

Đối với phần còn lại, chỉ cần thêm Default IfEmpty ()

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

EF và LINQ to SQL chuyển đổi chính xác thành SQL. Đối với LINQ to Object, việc tham gia bằng GroupJoin là không chính xác khi nó sử dụng Tra cứu nội bộ . Nhưng nếu bạn đang truy vấn DB thì bỏ qua GroupJoin là AFAIK với tư cách là người biểu diễn.

Personlay đối với tôi theo cách này dễ đọc hơn so với GroupJoin (). ChọnMany ()


Đây perfomed tốt hơn so với một .Join đối với tôi, cộng với tôi có thể làm doanh conditonal của tôi mà tôi muốn (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql dịch cách tiếp cận này là tham gia trái. Câu trả lời này tốt hơn và đơn giản hơn. +1
Guido Mocha

15

Bạn có thể tạo phương thức mở rộng như:

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

Điều này có vẻ như nó sẽ làm việc (đối với yêu cầu của tôi). bạn có thể cung cấp một ví dụ? Tôi chưa quen với Tiện ích mở rộng LINQ và đang gặp khó khăn trong việc xoay quanh tình huống Tham gia còn lại này Tôi đang ở ...
Shiva

@Skychan Có thể tôi cần xem lại, đó là câu trả lời cũ và đang hoạt động vào thời điểm đó. Bạn đang sử dụng Framework nào? Ý tôi là phiên bản .NET?
hajirazin

2
Điều này hoạt động cho Linq to Object nhưng không phải khi truy vấn cơ sở dữ liệu vì bạn cần phải hoạt động trên IQuerable và sử dụng Biểu thức của Funcs
Bob Vale

4

Cải thiện câu trả lời của Ocelot20, nếu bạn có một bảng bạn còn lại bên ngoài tham gia với nơi bạn chỉ muốn 0 hoặc 1 hàng trong số đó, nhưng nó có thể có nhiều bảng, bạn cần phải đặt thứ tự bảng đã tham gia của mình:

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

Mặt khác, hàng nào bạn nhận được trong phép nối sẽ là ngẫu nhiên (hoặc cụ thể hơn, tùy theo trường hợp db xảy ra trước tiên).


Đó là nó! Bất kỳ mối quan hệ không đảm bảo một đến một.
it3xl

2

Biến câu trả lời của Marc Gravell thành một phương pháp mở rộng, tôi đã làm như sau.

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

Trong khi câu trả lời được chấp nhận hoạt động và rất tốt cho Linq đối với các đối tượng, nó đã nói với tôi rằng truy vấn SQL không chỉ là một kết nối bên ngoài bên trái.

Đoạn mã sau dựa trên Dự án LinkKit cho phép bạn truyền biểu thức và gọi chúng vào truy vấn của bạn.

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

Nó có thể được sử dụng như sau

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

-1

Có một giải pháp dễ dàng cho việc này

Chỉ cần sử dụng .HasValue trong Chọn của bạn

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

Rất dễ dàng, không cần tham gia nhóm hay bất cứ điều gì khác

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.