LINQ-전체 외부 조인


202

나는 사람들의 신분증과 그들의 이름과 사람들의 신분증과 성의 목록을 가지고 있습니다. 어떤 사람들은 이름이없고 어떤 사람들은 성이 없습니다. 두 목록에서 전체 외부 조인을 수행하고 싶습니다.

따라서 다음 목록이 있습니다.

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

생산해야합니다 :

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

나는 LINQ를 처음 사용하기 때문에 (저는 절름발이 인 경우 용서하십시오) 모두 비슷하게 보이지만 실제로는 외부 조인으로 보이는 'LINQ Outer Joins'에 대한 몇 가지 솔루션을 발견했습니다.

내 시도는 지금까지 다음과 같이 진행됩니다.

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

그러나 이것은 다음을 반환합니다.

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

내가 뭘 잘못하고 있죠?


2
인 메모리 목록 또는 Linq2Sql에만 작동하려면이 기능이 필요합니까?
JamesFaix

.GroupJoin ()을 사용해보십시오. stackoverflow.com/questions/15595289/…
jdev.ninja

답변:


122

이것이 모든 경우에 적용되는지는 모르겠지만 논리적으로는 정확합니다. 아이디어는 왼쪽 외부 조인과 오른쪽 외부 조인을 취한 다음 결과를 결합하는 것입니다.

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

이것은 LINQ to Objects에 있으므로 작성된대로 작동합니다. LINQ to SQL 또는 다른 경우 쿼리 프로세서는 안전한 탐색이나 다른 작업을 지원하지 않을 수 있습니다. 조건부 값을 얻으려면 조건부 연산자를 사용해야합니다.

즉,

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };

2
노동 조합은 중복을 제거합니다. 중복이 예상되지 않거나 두 번째 쿼리를 작성하여 첫 번째에 포함 된 내용을 제외시킬 수있는 경우 대신 Concat을 사용하십시오. 이것은 UNION과 UNION ALL의 SQL 차이점입니다.
cadrell0

3
사람이 이름과 성을 가진 경우 @ cadre110 복제가 발생하므로 노조가 올바른 선택입니다.
saus

1
@saus이지만 ID 열이 있으므로 이름과 성이 중복 되어도 ID가 달라야합니다.
cadrell0

1
솔루션은 기본 유형에는 작동하지만 객체에는 작동하지 않는 것 같습니다. 필자의 경우 FirstName은 도메인 개체이고 LastName은 다른 도메인 개체입니다. 두 결과를 통합하면 LINQ에서 NotSupportedException이 발생했습니다 (Union 또는 Concat의 유형은 비호 환적으로 구성됨). 비슷한 문제가 발생 했습니까?
Candy Chiu

1
@CandyChiu : 나는 실제로 그런 경우에 결코 부딪치지 않았다. 나는 그것이 당신의 쿼리 제공자에 대한 제한이라고 생각합니다. 이 경우 AsEnumerable()Union / Concatenation을 수행하기 전에 호출하여 LINQ to Objects를 사용하는 것이 좋습니다. 그것을 시도하고 어떻게되는지보십시오. 이것이 당신이 가고 싶은 길이 아니라면, 나는 그보다 더 도움이 될 수 있을지 확신하지 못합니다.
Jeff Mercado

196

업데이트 1 : 진정으로 일반화 된 확장 방법 제공 FullOuterJoin
업데이트 2 : 선택적으로 IEqualityComparer키 유형
업데이트 3에 대한 사용자 정의 허용 :이 구현은 최근에 일부가되었습니다MoreLinq -고맙습니다!

편집 추가 FullOuterGroupJoin( ideone ). GetOuter<>구현을 재사용하여 이보다 성능이 다소 떨어졌지만 지금은 최첨단이 아닌 '고수준'코드를 목표로하고 있습니다.

http://ideone.com/O36nWc에서 실시간으로 확인하십시오

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

출력을 인쇄합니다.

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

당신은 또한 기본값을 제공 할 수 있습니다 : http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

인쇄:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

사용 된 용어 설명 :

결합은 관계형 데이터베이스 디자인에서 빌린 용어입니다.

  • A는 가입 에서 요소를 반복 할 a요소가 있기 때문에 여러 번 b 키를 대응은 (예 : 아무것도 경우는 b빈 않았다). 데이터베이스 용어는 이것을 호출합니다inner (equi)join .
  • 외부 조인 의 요소가 포함 a되는 해당하는 요소 에 존재하지 않는이 b. (즉 : b비어있는 경우에도 결과 ). 이것은 일반적으로left join .
  • 완전 외부 조인 레코드를 포함 a 할뿐만 아니라b 경우에 해당하는 요소는 다른 존재하지 않는다. (즉 a비어있는 경우에도 결과 )

RDBMS에서 일반적으로 보이지 않는 것은 그룹 조인입니다 [1] :

  • 그룹 가입 , 전술 한 바와 같이 동일하지 하지만 대신 요소에서 반복 a대응 여러 대를 b, 그 그룹에 대응하는 키를 기록. 공통 키를 기준으로 '결합 된'레코드를 통해 열거하려는 경우이 방법이 더 편리합니다.

일반적인 배경 설명도 포함 된 GroupJoin 도 참조하십시오 .


[1] (Oracle과 MSSQL은 이에 대한 독점 확장 기능을 가지고 있다고 생각합니다)

전체 코드

이를위한 일반화 된 '드롭 인'확장 클래스

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}

FullOuterJoin제공된 확장 방법 의 사용법을 보여주기 위해 편집 됨
sehe

수정 : FullOuterGroupJoin 확장 방법 추가
sehe

4
사전을 사용하는 대신 헬퍼 확장 메소드에 표현 된 기능이 포함 된 Lookup을 사용할 수 있습니다 . 예를 들어, a.GroupBy(selectKeyA).ToDictionary();as a.ToLookup(selectKeyA)adict.OuterGet(key)as를 쓸 수 있습니다 alookup[key]. 그래도 키 컬렉션을 얻는 것은 조금 까다 롭습니다 alookup.Select(x => x.Keys).
Risky Martin

1
@RiskyMartin 감사합니다! 사실, 그것은 모든 것을 더욱 우아하게 만듭니다. 답변 아이디어를 업데이트했습니다 . (객체 수가 적어지기 때문에 성능을 향상시켜야한다고 가정합니다).
sehe

1
@Revious는 키가 고유하다는 것을 알고있는 경우에만 작동합니다. 그리고 / grouping /의 일반적인 경우는 아닙니다. 그 외에는 꼭 그렇습니다. 해시가 perf를 드래그하지 않을 것임을 알고 있다면 (노드 기반 컨테이너는 원칙적으로 더 많은 비용이 들며 해싱은 무료가 아니며 효율성은 해시 함수 / 버킷 확산에 달려 있습니다) 확실히 알고리즘 적으로 효율적입니다. 그래서, 작은 부하에 나는 그것이 빨리되지 않을 수도 있습니다 기대
sehe

27

너무 많은 서버 왕복과 너무 많은 데이터 반환 또는 너무 많은 클라이언트 실행으로 인해 IQueryable을 통해 Linq와 잘 작동하지 않기 때문에 허용 된 답변을 포함하여 대부분의 문제에 문제가 있다고 생각합니다.

IEnumerable의 경우 과도한 메모리 사용 (32GB 시스템의 Linqpad에서 간단한 10000000 2 목록 테스트를 실행했습니다) 때문에 Sehe의 대답이나 비슷한 것을 좋아하지 않습니다.

또한 다른 대부분은 실제로 올바른 반 외부 조인을 사용하는 Concat 대신 오른쪽 조인이있는 Union을 사용하므로 결과에서 중복 된 내부 조인 행을 제거 할뿐 아니라 적절한 전체 외부 조인을 실제로 구현하지 않습니다. 원래 왼쪽 또는 오른쪽 데이터에 존재하는 적절한 복제본

다음은 이러한 모든 문제를 처리하고, SQL을 생성하고, LINQ to SQL에서 직접 조인을 구현하고, 서버에서 실행하며, 열거 형의 다른 것보다 빠르고 메모리가 적은 확장 기능입니다.

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLgR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

Right Anti-Semi-Join의 차이점은 Linq to Objects 또는 소스와 거의 관련이 없지만 최종 답변에서 서버 (SQL) 측면에서 차이를 만들어 불필요한 것을 제거합니다. JOIN .

LinqKit을 사용하여을 람다로 Expression병합하는 데 필요한 핸드 코딩을 Expression<Func<>>향상시킬 수는 있지만 언어 / 컴파일러가 도움을 주면 좋을 것입니다. FullOuterJoinDistinctRightOuterJoin기능을 완전하게하기 위해서 포함되어 있습니다,하지만 난 구현을 다시하지 않았다FullOuterGroupJoin 아직.

에 대한 전체 외부 조인의 다른 버전 을 썼습니다IEnumerable 키가 빠르게 왼쪽 외부 결합 50 % 이상에 관한 권리 반 반이 적어도 작은 컬렉션에 가입과 함께 참여하는 주문할 수 있습니다 경우에. 한 번만 정렬 한 후 각 컬렉션을 통과합니다.

또한 사용자 지정 확장 으로 대체하여 EF와 호환되는 버전에 대한 또 다른 답변 을 추가했습니다 Invoke.


거래는 TP unusedP, TC unusedC무엇입니까? 문자 그대로 사용되지 않습니까?
Rudey

네,의 유형을 캡처 할 만 존재하는 TP, TC, TResult적절한를 만들 수 Expression<Func<>>. 나는 그들을 대체 할 수있는 가정 _, __, ___C # 대신 사용할 수있는 적절한 매개 변수 와일드 카드를 가질 때까지 대신,하지만 그 어떤 명확한를 보이지 않는다.
NetMage

1
@MarcL. 나는 'tidesome'에 대해 확신하지 못하지만이 답변 이이 맥락에서 매우 유용하다는 데 동의합니다. 인상적인 것들 (나에게 그것은 Linq-to-SQL의 단점을 확인하지만)
sehe

3
나는 받고있다 The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. 이 코드에 제한이 있습니까? IQueryables에 대해 전체 참여
학습자

1
나는 대체하는 새 응답 추가 한 Invoke사용자 정의와는 ExpressionVisitor를 인라인 할 수 Invoke는 EF와 함께 작동합니다 있도록합니다. 당신은 그것을 시도 할 수 있습니까?
NetMage

7

이를 수행하는 확장 방법은 다음과 같습니다.

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}

3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), 이는 전체 외부 조인 = 왼쪽 외부 조인 유니온 오른쪽 모든 외부 조인을 의미합니다! 이 접근 방식의 단순함에 감사합니다.
TamusJRoyce

1
@TamusJRoyce Union중복을 제거하므로 원본 데이터에 중복 행이 있으면 결과에 포함되지 않습니다.
NetMage

좋은 지적입니다! 중복이 제거되지 않도록하려면 고유 ID를 추가하십시오. 예. 유니언은 고유 한 ID가 있고 유니언이 내부 휴리스틱 / 최적화를 통해 모두를 통합하도록 전환하지 않는다는 것을 암시하지 않는 한 약간 낭비입니다. 그러나 작동합니다.
TamusJRoyce

허용 된 답변 과 동일합니다 .
Gert Arnold

7

@sehe의 접근 방식이 더 강력하다고 생각하지만 더 잘 이해할 때까지 @MichaelSander의 확장 기능을 뛰어 넘습니다. 여기에 설명 된 내장 Enumerable.Join () 메소드의 구문 및 반환 유형과 일치하도록 수정했습니다 . @JeffMercado의 솔루션에서 @ cadrell0의 의견과 관련하여 "고유 한"접미사를 추가했습니다.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

이 예에서는 다음과 같이 사용합니다.

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

앞으로 더 많은 것을 배우면서 나는 그것이 인기가 있기 때문에 @sehe의 논리로 마이그레이션 할 느낌이 있습니다. 그러나 가능한 경우 기존 ".Join ()"메서드의 구문과 일치하는 오버로드가 하나 이상있는 것이 중요하다고 생각하기 때문에 다음 두 가지 이유로주의해야합니다.

  1. 방법의 일관성은 시간을 절약하고 오류를 방지하며 의도하지 않은 동작을 방지합니다.
  2. 미래에 즉시 사용할 수있는 ".FullJoin ()"메서드가 있다면 현재 존재하는 ".Join ()"메서드의 구문을 유지하려고 노력할 것입니다. 그렇다면, 당신이 그것으로 이주하고 싶다면, 당신은 단순히 매개 변수를 변경하거나 코드를 깨는 다른 반환 유형에 대해 걱정하지 않고 함수의 이름을 바꿀 수 있습니다.

나는 여전히 제네릭, 확장, Func 문 및 기타 기능을 처음 사용하므로 피드백을 환영합니다.

편집하다: 내 코드에 문제가 있음을 깨닫는 데 오래 걸리지 않았습니다. LINQPad에서 .Dump ()를 수행하고 반환 유형을보고있었습니다. IEnumerable이되었으므로 일치 시키려고했습니다. 그러나 실제로 내 확장에서 .Where () 또는 .Select ()를 수행하면 " 'System Collections.IEnumerable'에 'Select'및 ..."에 대한 정의가 포함되어 있지 않습니다. 결국 나는 .Join ()의 입력 구문을 일치시킬 수 있었지만 반환 동작은 일치하지 않았습니다.

편집 : 함수의 반환 유형에 "TResult"가 추가되었습니다. Microsoft 기사를 읽을 때 그 사실을 잊어 버렸습니다. 이 수정으로, 이제 리턴 동작이 결국 내 목표와 일치하는 것 같습니다.


이 답변과 Michael Sanders는 +2 실수로 이것을 클릭하면 투표가 잠 깁니다. 두 개를 추가하십시오.
TamusJRoyce

@TamusJRoyce, 방금 코드 형식을 약간 편집했습니다. 편집 한 후에는 투표를 재개 할 수있는 옵션이 있습니다. 원한다면 주사를주세요.
pwilcox 2016 년

정말 고맙습니다!
Roshna Omer

6

아시다시피 Linq에는 "외부 조인"구문이 없습니다. 당신이 얻을 수있는 가장 가까운 것은 당신이 언급 한 쿼리를 사용하여 왼쪽 외부 조인입니다. 여기에 조인에 표시되지 않은 성 목록의 요소를 추가 할 수 있습니다.

outerJoin = outerJoin.Concat(lastNames.Select(l=>new
                            {
                                id = l.ID,
                                firstname = String.Empty,
                                surname = l.Name
                            }).Where(l=>!outerJoin.Any(o=>o.id == l.id)));

2

나는 sehe의 대답을 좋아하지만 지연 된 실행을 사용하지 않습니다 (입력 시퀀스는 ToLookup에 대한 호출에 의해 열심히 열거됩니다). 따라서 LINQ-to-objects 의 .NET 소스를 살펴본 후 다음 을 수행했습니다.

public static class LinqExtensions
{
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator = null,
        TLeft defaultLeft = default(TLeft),
        TRight defaultRight = default(TRight))
    {
        if (left == null) throw new ArgumentNullException("left");
        if (right == null) throw new ArgumentNullException("right");
        if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
        if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        comparator = comparator ?? EqualityComparer<TKey>.Default;
        return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
    }

    internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator,
        TLeft defaultLeft,
        TRight defaultRight)
    {
        var leftLookup = left.ToLookup(leftKeySelector, comparator);
        var rightLookup = right.ToLookup(rightKeySelector, comparator);
        var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);

        foreach (var key in keys)
            foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
                foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
                    yield return resultSelector(leftValue, rightValue, key);
    }
}

이 구현에는 다음과 같은 중요한 속성이 있습니다.

  • 지연된 실행, 출력 시퀀스가 ​​열거되기 전에 입력 시퀀스가 ​​열거되지 않습니다.
  • 입력 시퀀스는 각각 한 번만 열거합니다.
  • 왼쪽 순서에 이어 순서대로 튜플을 생성한다는 의미에서 입력 순서의 순서를 유지합니다 (왼쪽 순서에없는 키의 경우).

이러한 속성은 FullOuterJoin을 처음 사용하지만 LINQ를 경험 한 사람이 기대하는 것이므로 중요합니다.


입력 순서의 순서를 유지하지는 않습니다. Lookup은이를 보장하지 않으므로 이러한 foreach는 왼쪽 순서로 나열되고 오른쪽 순서는 왼쪽에 표시되지 않습니다. 그러나 요소의 관계 순서는 유지되지 않습니다.
Ivan Danilov 2016 년

@IvanDanilov 귀하는 이것이 계약에 실제로 있지 않다는 것이 맞습니다. 그러나 ToLookup의 구현은 Enumerable.cs의 내부 조회 클래스를 사용하여 그룹화를 삽입 순서로 연결된 목록으로 유지하고이 목록을 사용하여 반복합니다. 따라서 현재 .NET 버전에서는 순서가 보장되지만 MS가이를 문서화하지 않았기 때문에 이후 버전에서 변경할 수 있습니다.
Søren Boisen 2016 년

Win 8.1의 .NET 4.5.1에서 시도했지만 순서가 유지되지 않습니다.
Ivan Danilov 2016 년

1
".. 입력 시퀀스는 ToLookup에 대한 호출로 간절히 열거됩니다." 그러나 당신의 구현은 정확히 똑같습니다. 유한 상태 머신에 대한 비용 때문에 항복은 여기에서 많은 것을주지 않습니다.
pkuderov

4
조회 호출은 반복자가 작성 될 때가 아니라 결과의 첫 번째 요소가 요청 될 때 수행됩니다. 이것이 연기 된 연기의 의미입니다. 왼쪽 Enumerable을 Lookup으로 변환하는 대신 직접 반복하여 한 입력 세트의 열거를 더 지연하여 왼쪽 세트의 순서가 유지되는 추가 이점을 얻을 수 있습니다.
Rolf

2

나는 그것이 충분히 테스트되지 않았기 때문에 이것을 별도의 답변으로 추가하기로 결정했습니다. 이것은 FullOuterJoin본질적으로 단순화 된 LINQKit Invoke/ 버전의 Expandfor를 사용 하여 메소드를 다시 구현 Expression하여 Entity Framework에서 작동하도록합니다. 이전 답변과 거의 동일하므로 설명이 많지 않습니다.

public static class Ext {
    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg");
        var parmC = Expression.Parameter(typeof(TRight), "r");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr");
        var parmC = Expression.Parameter(typeof(TLeft), "l");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right })
                         .SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    private static Expression<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // newrightrs = lgr => resultSelector(default(TLeft), lgr.right)
        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(resultSelector.Apply(argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector)  where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Cast<ParameterExpression>().Zip(args, (p, a) => (p, a))) {
            b = b.Replace(pa.p, pa.a);
        }

        return b.PropagateNull();
    }

    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
    public class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor {
        public readonly Expression from;
        public readonly Expression to;

        public ReplaceVisitor(Expression _from, Expression _to) {
            from = _from;
            to = _to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);
    public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
                return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
            else
                return base.Visit(node);
        }
    }

    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }
}

NetMage, 인상적인 코딩! 간단한 예를 사용하여 실행하고 [base.Visit (Node)]에서 [NullVisitor.Visit (..)가 호출되면 [System.ArgumentException : Argument Types not match]가 발생합니다. [Guid] TKey를 사용하고 있으며 어느 시점에서 null 방문자는 [Guid?] 유형을 기대하므로 어느 것이 맞습니까? 뭔가 빠졌을 수도 있습니다. EF 6.4.4에 대한 간단한 예제가 있습니다. 이 코드를 어떻게 공유 할 수 있는지 알려주십시오. 감사!
Troncho

@Troncho 저는 보통 테스트에 LINQPad를 사용하므로 EF 6을 쉽게 수행 할 수 없습니다. base.Visit(node)예외는 트리 아래로 반복되므로 예외를 던져서는 안됩니다. 거의 모든 코드 공유 서비스에 액세스 할 수 있지만 테스트 데이터베이스는 설정할 수 없습니다. LINQ to SQL 테스트에 대해 실행하면 정상적으로 작동하는 것 같습니다.
NetMage

@Troncho Guid키와 Guid?외래 키 사이에 참여할 수 있습니까?
NetMage

LinqPad를 사용하여 테스트하고 있습니다. 내 쿼리에서 ArgumentException이 발생하여 [.Net Framework 4.7.1] 및 최신 EF 6에서 VS2019에서 디버깅하기로 결정했습니다. 실제 문제를 추적해야합니다. 코드를 테스트하기 위해 동일한 [Persons] 테이블에서 시작하는 2 개의 개별 데이터 세트를 생성하고 있습니다. 두 레코드를 모두 필터링하여 일부 레코드가 각 세트에 고유하고 일부 레코드가 두 세트 모두에 존재하도록합니다. [PersonId]는 [Primary Key] Guid (c #) / Uniqueidentifier (SqlServer)이며 null [PersonId] 값을 생성하지 않습니다. 공유 코드 : github.com/Troncho/EF_FullOuterJoin
Troncho

1

두 입력 모두에 대해 메모리 내 스트리밍 열거를 수행하고 각 행의 선택기를 호출합니다. 현재 반복에서 상관 관계가 없으면 선택기 인수 중 하나가 null입니다. 입니다.

예:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • 상관 유형에 대해 IComparer가 필요하며, Comparer.Default를 제공하지 않으면 기본값을 사용하십시오.

  • 입력 열거 형에 'OrderBy'가 적용되어야합니다.

    /// <summary>
    /// Performs a full outer join on two <see cref="IEnumerable{T}" />.
    /// </summary>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <param name="leftKeySelector"></param>
    /// <param name="rightKeySelector"></param>
    /// <param name="selector">Expression defining result type</param>
    /// <param name="keyComparer">A comparer if there is no default for the type</param>
    /// <returns></returns>
    [System.Diagnostics.DebuggerStepThrough]
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TValue> leftKeySelector,
        Func<TRight, TValue> rightKeySelector,
        Func<TLeft, TRight, TResult> selector,
        IComparer<TValue> keyComparer = null)
        where TLeft: class
        where TRight: class
        where TValue : IComparable
    {
    
        keyComparer = keyComparer ?? Comparer<TValue>.Default;
    
        using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator())
        using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator())
        {
    
            var hasLeft = enumLeft.MoveNext();
            var hasRight = enumRight.MoveNext();
            while (hasLeft || hasRight)
            {
    
                var currentLeft = enumLeft.Current;
                var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue);
    
                var currentRight = enumRight.Current;
                var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue);
    
                int compare =
                    !hasLeft ? 1
                    : !hasRight ? -1
                    : keyComparer.Compare(valueLeft, valueRight);
    
                switch (compare)
                {
                    case 0:
                        // The selector matches. An inner join is achieved
                        yield return selector(currentLeft, currentRight);
                        hasLeft = enumLeft.MoveNext();
                        hasRight = enumRight.MoveNext();
                        break;
                    case -1:
                        yield return selector(currentLeft, default(TRight));
                        hasLeft = enumLeft.MoveNext();
                        break;
                    case 1:
                        yield return selector(default(TLeft), currentRight);
                        hasRight = enumRight.MoveNext();
                        break;
                }
            }
    
        }
    
    }

1
그것은 "스트리밍"을 만들기위한 영웅적인 노력입니다. 안타깝게도 첫 단계에서 모든 이익을 잃게됩니다 OrderBy. OrderBy명백한 이유로 전체 시퀀스를 버퍼링합니다 .
sehe

@sehe 당신은 Linq to Objects의 확실히 옳습니다. IEnumerable <T>이 IQueryable <T>이면 소스를 정렬해야합니다. 테스트 할 시간이 없습니다. 내가 잘못하면 입력 IEnumerable <T>을 IQueryable <T>로 바꾸면 소스 / 데이터베이스에서 정렬해야합니다.
James Caradoc-Davies

1

키가 두 열거 형에서 고유 한 상황에 대한 나의 깨끗한 솔루션 :

 private static IEnumerable<TResult> FullOuterJoin<Ta, Tb, TKey, TResult>(
            IEnumerable<Ta> a, IEnumerable<Tb> b,
            Func<Ta, TKey> key_a, Func<Tb, TKey> key_b,
            Func<Ta, Tb, TResult> selector)
        {
            var alookup = a.ToLookup(key_a);
            var blookup = b.ToLookup(key_b);
            var keys = new HashSet<TKey>(alookup.Select(p => p.Key));
            keys.UnionWith(blookup.Select(p => p.Key));
            return keys.Select(key => selector(alookup[key].FirstOrDefault(), blookup[key].FirstOrDefault()));
        }

그래서

    var ax = new[] {
        new { id = 1, first_name = "ali" },
        new { id = 2, first_name = "mohammad" } };
    var bx = new[] {
        new { id = 1, last_name = "rezaei" },
        new { id = 3, last_name = "kazemi" } };

    var list = FullOuterJoin(ax, bx, a => a.id, b => b.id, (a, b) => "f: " + a?.first_name + " l: " + b?.last_name).ToArray();

출력 :

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi

0

둘 이상의 테이블에 대한 전체 외부 조인 : 먼저 조인하려는 열을 추출하십시오.

var DatesA = from A in db.T1 select A.Date; 
var DatesB = from B in db.T2 select B.Date; 
var DatesC = from C in db.T3 select C.Date;            

var Dates = DatesA.Union(DatesB).Union(DatesC); 

그런 다음 추출 된 열과 기본 테이블 사이에 왼쪽 외부 조인을 사용하십시오.

var Full_Outer_Join =

(from A in Dates
join B in db.T1
on A equals B.Date into AB 

from ab in AB.DefaultIfEmpty()
join C in db.T2
on A equals C.Date into ABC 

from abc in ABC.DefaultIfEmpty()
join D in db.T3
on A equals D.Date into ABCD

from abcd in ABCD.DefaultIfEmpty() 
select new { A, ab, abc, abcd })
.AsEnumerable();

0

나는 아마도 6 년 전에 앱을 위해이 확장 클래스를 작성했으며 그 이후로 많은 솔루션에서 문제없이 사용하고 있습니다. 도움이 되길 바랍니다.

편집 : 일부는 확장 클래스를 사용하는 방법을 모른다는 것을 알았습니다.

이 확장 클래스를 사용하려면 joinext를 사용하여 다음 줄을 추가하여 클래스에서 네임 스페이스를 참조하십시오.

^ 사용하면 IEnumerable 객체 컬렉션에서 확장 기능의 지능을 볼 수 있습니다.

도움이 되었기를 바랍니다. 여전히 명확하지 않은지 알려 주시면 사용 방법에 대한 샘플 예제를 작성하겠습니다.

이제 수업이 있습니다.

namespace joinext
{    
public static class JoinExtensions
    {
        public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
            where TInner : class
            where TOuter : class
        {
            var innerLookup = inner.ToLookup(innerKeySelector);
            var outerLookup = outer.ToLookup(outerKeySelector);

            var innerJoinItems = inner
                .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem)))
                .Select(innerItem => resultSelector(null, innerItem));

            return outer
                .SelectMany(outerItem =>
                {
                    var innerItems = innerLookup[outerKeySelector(outerItem)];

                    return innerItems.Any() ? innerItems : new TInner[] { null };
                }, resultSelector)
                .Concat(innerJoinItems);
        }


        public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return outer.GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (o, i) =>
                    new { o = o, i = i.DefaultIfEmpty() })
                    .SelectMany(m => m.i.Select(inn =>
                        resultSelector(m.o, inn)
                        ));

        }



        public static IEnumerable<TResult> RightJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return inner.GroupJoin(
                outer,
                innerKeySelector,
                outerKeySelector,
                (i, o) =>
                    new { i = i, o = o.DefaultIfEmpty() })
                    .SelectMany(m => m.o.Select(outt =>
                        resultSelector(outt, m.i)
                        ));

        }

    }
}

1
불행히도, 함수를 SelectManyLINQ2SQL에 적합한 표현식 트리로 변환 할 수없는 것 같습니다.
또는 매퍼

edc65. 당신이 이미 그렇게했다면 어리석은 질문 일 수 있다는 것을 알고 있습니다. 그러나 (아무도 모르는 것처럼) 네임 스페이스 joinext를 참조하면됩니다.
H7O

또는 Mapper, 어떤 유형의 컬렉션을 사용하길 원하는지 알려주세요. IEnumerable 컬렉션에서 잘 작동합니다
H7O

0

LINQ join 절은이 문제에 대한 올바른 해결책이 아니라고 생각합니다 .join 절 목적은이 작업 솔루션에 필요한 방식으로 데이터를 축적하지 않기 때문입니다. 생성 된 개별 컬렉션을 병합하는 코드가 너무 복잡해 지므로 학습 목적으로는 괜찮지 만 실제 응용 프로그램에는 적합하지 않을 수 있습니다. 이 문제를 해결하는 방법 중 하나는 아래 코드에 있습니다.

class Program
{
    static void Main(string[] args)
    {
        List<FirstName> firstNames = new List<FirstName>();
        firstNames.Add(new FirstName { ID = 1, Name = "John" });
        firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

        List<LastName> lastNames = new List<LastName>();
        lastNames.Add(new LastName { ID = 1, Name = "Doe" });
        lastNames.Add(new LastName { ID = 3, Name = "Smith" });

        HashSet<int> ids = new HashSet<int>();
        foreach (var name in firstNames)
        {
            ids.Add(name.ID);
        }
        foreach (var name in lastNames)
        {
            ids.Add(name.ID);
        }
        List<FullName> fullNames = new List<FullName>();
        foreach (int id in ids)
        {
            FullName fullName = new FullName();
            fullName.ID = id;
            FirstName firstName = firstNames.Find(f => f.ID == id);
            fullName.FirstName = firstName != null ? firstName.Name : string.Empty;
            LastName lastName = lastNames.Find(l => l.ID == id);
            fullName.LastName = lastName != null ? lastName.Name : string.Empty;
            fullNames.Add(fullName);
        }
    }
}
public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}
class FullName
{
    public int ID;

    public string FirstName;

    public string LastName;
}

실제 컬렉션이 HashSet 구성에 대해 큰 경우 foreach 루프를 아래 코드로 사용할 수 있습니다.

List<int> firstIds = firstNames.Select(f => f.ID).ToList();
List<int> LastIds = lastNames.Select(l => l.ID).ToList();
HashSet<int> ids = new HashSet<int>(firstIds.Union(LastIds));//Only unique IDs will be included in HashSet

0

흥미로운 게시물에 대해 모두 감사합니다!

내 경우에는 코드가 필요했기 때문에 코드를 수정했습니다.

  • 조인 조건 개인
  • 개인 노조 별개의 비교 자

관심있는 사람들에게 이것은 내 수정 된 코드입니다 (VB에서 죄송합니다)

    Module MyExtensions
        <Extension()>
        Friend Function FullOuterJoin(Of TA, TB, TResult)(ByVal a As IEnumerable(Of TA), ByVal b As IEnumerable(Of TB), ByVal joinPredicate As Func(Of TA, TB, Boolean), ByVal projection As Func(Of TA, TB, TResult), ByVal comparer As IEqualityComparer(Of TResult)) As IEnumerable(Of TResult)
            Dim joinL =
                From xa In a
                From xb In b.Where(Function(x) joinPredicate(xa, x)).DefaultIfEmpty()
                Select projection(xa, xb)
            Dim joinR =
                From xb In b
                From xa In a.Where(Function(x) joinPredicate(x, xb)).DefaultIfEmpty()
                Select projection(xa, xb)
            Return joinL.Union(joinR, comparer)
        End Function
    End Module

    Dim fullOuterJoin = lefts.FullOuterJoin(
        rights,
        Function(left, right) left.Code = right.Code And (left.Amount [...] Or left.Description.Contains [...]),
        Function(left, right) New CompareResult(left, right),
        New MyEqualityComparer
    )

    Public Class MyEqualityComparer
        Implements IEqualityComparer(Of CompareResult)

        Private Function GetMsg(obj As CompareResult) As String
            Dim msg As String = ""
            msg &= obj.Code & "_"
            [...]
            Return msg
        End Function

        Public Overloads Function Equals(x As CompareResult, y As CompareResult) As Boolean Implements IEqualityComparer(Of CompareResult).Equals
            Return Me.GetMsg(x) = Me.GetMsg(y)
        End Function

        Public Overloads Function GetHashCode(obj As CompareResult) As Integer Implements IEqualityComparer(Of CompareResult).GetHashCode
            Return Me.GetMsg(obj).GetHashCode
        End Function
    End Class

0

또 다른 완전 외부 조인

다른 제안의 단순성과 가독성에 만족하지 않았기 때문에 나는 이것으로 끝났습니다.

빠른 척력은 없습니다 (2020m CPU에서 1000 * 1000에 합류하는 데 약 800ms : 2.4ghz / 2 코어). 나에게, 그것은 작고 캐주얼 한 완전 외부 조인입니다.

SQL FULL OUTER JOIN과 동일하게 작동합니다 (보존 중복).

건배 ;-)

using System;
using System.Collections.Generic;
using System.Linq;
namespace NS
{
public static class DataReunion
{
    public static List<Tuple<T1, T2>> FullJoin<T1, T2, TKey>(List<T1> List1, Func<T1, TKey> KeyFunc1, List<T2> List2, Func<T2, TKey> KeyFunc2)
    {
        List<Tuple<T1, T2>> result = new List<Tuple<T1, T2>>();

        Tuple<TKey, T1>[] identifiedList1 = List1.Select(_ => Tuple.Create(KeyFunc1(_), _)).OrderBy(_ => _.Item1).ToArray();
        Tuple<TKey, T2>[] identifiedList2 = List2.Select(_ => Tuple.Create(KeyFunc2(_), _)).OrderBy(_ => _.Item1).ToArray();

        identifiedList1.Where(_ => !identifiedList2.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(_.Item2, default(T2)));
        });

        result.AddRange(
            identifiedList1.Join(identifiedList2, left => left.Item1, right => right.Item1, (left, right) => Tuple.Create<T1, T2>(left.Item2, right.Item2)).ToList()
        );

        identifiedList2.Where(_ => !identifiedList1.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(default(T1), _.Item2));
        });

        return result;
    }
}
}

아이디어는

  1. 제공된 주요 기능 빌더를 기반으로 ID 빌드
  2. 남은 항목 만 처리
  3. 내부 조인 처리
  4. 올바른 항목 만 처리

간결한 테스트는 다음과 같습니다.

마지막에 중단 점을 배치하여 예상대로 작동하는지 수동으로 확인하십시오.

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NS;

namespace Tests
{
[TestClass]
public class DataReunionTest
{
    [TestMethod]
    public void Test()
    {
        List<Tuple<Int32, Int32, String>> A = new List<Tuple<Int32, Int32, String>>();
        List<Tuple<Int32, Int32, String>> B = new List<Tuple<Int32, Int32, String>>();

        Random rnd = new Random();

        /* Comment the testing block you do not want to run
        /* Solution to test a wide range of keys*/

        for (int i = 0; i < 500; i += 1)
        {
            A.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "A"));
            B.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "B"));
        }

        /* Solution for essential testing*/

        A.Add(Tuple.Create(1, 2, "B11"));
        A.Add(Tuple.Create(1, 2, "B12"));
        A.Add(Tuple.Create(1, 3, "C11"));
        A.Add(Tuple.Create(1, 3, "C12"));
        A.Add(Tuple.Create(1, 3, "C13"));
        A.Add(Tuple.Create(1, 4, "D1"));

        B.Add(Tuple.Create(1, 1, "A21"));
        B.Add(Tuple.Create(1, 1, "A22"));
        B.Add(Tuple.Create(1, 1, "A23"));
        B.Add(Tuple.Create(1, 2, "B21"));
        B.Add(Tuple.Create(1, 2, "B22"));
        B.Add(Tuple.Create(1, 2, "B23"));
        B.Add(Tuple.Create(1, 3, "C2"));
        B.Add(Tuple.Create(1, 5, "E2"));

        Func<Tuple<Int32, Int32, String>, Tuple<Int32, Int32>> key = (_) => Tuple.Create(_.Item1, _.Item2);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        var res = DataReunion.FullJoin(A, key, B, key);
        watch.Stop();
        var elapsedMs = watch.ElapsedMilliseconds;
        String aser = JToken.FromObject(res).ToString(Formatting.Indented);
        Console.Write(elapsedMs);
    }
}

}


-4

나는이 linq 표현식을 정말로 싫어합니다. 이것이 SQL이 존재하는 이유입니다.

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname
   from firstnames fn
   full join lastnames ln on ln.id=fn.id

이것을 데이터베이스에서 SQL보기로 만들고 엔티티로 가져옵니다.

물론, 왼쪽과 오른쪽 조인의 (명확한) 결합으로도 만들 수 있지만 어리 석습니다.


11
가능한 많은 추상화를 삭제하고 머신 코드에서이를 수행하는 이유는 무엇입니까? (힌트 : 고차 추상화는 프로그래머의 삶을 편하게하기 때문에) 이것은 질문에 대답하지 않으며 LINQ에 대한 열렬한 태도로 보입니다.
쓰셨

8
누가 데이터베이스에서 데이터를 가져 왔습니까?
user247702

1
물론, 그것은 데이터베이스, 질문에 "외부 조인"이 있습니다 :) google.cz/search?q=outer+join
Milan Švec

1
나는 이것이 "구식"솔루션이라는 것을 이해하지만, downvoting하기 전에 다른 솔루션과의 복잡성을 비교하십시오 :) 허용되는 솔루션을 제외하고는 물론 올바른 솔루션입니다.
Milan Švec

물론 데이터베이스 일 수도 있고 아닐 수도 있습니다. 나는 외부와 솔루션을 찾고 있어요 메모리에 목록 간의 조인
edc65
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.