LINQ - เข้าร่วมเต็มด้านนอก


202

ฉันมีรายการ ID ของผู้คนและชื่อแรกของพวกเขาและรายการ ID ของผู้คนและนามสกุลของพวกเขา บางคนไม่มีชื่อและบางคนไม่มีนามสกุล ฉันต้องการเข้าร่วมเต็มรูปแบบภายนอกในสองรายการ

ดังนั้นรายการต่อไปนี้:

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

คำตอบ:


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 ไปยัง Objects หาก LINQ เป็น 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 แทน นี่คือความแตกต่างของ SQL ระหว่าง UNION และ UNION ALL
cadrell0

3
@ cadre110 ซ้ำกันจะเกิดขึ้นหากบุคคลมีชื่อและนามสกุลดังนั้นการรวมกันจึงเป็นทางเลือกที่ถูกต้อง
saus

1
@saus แต่มีคอลัมน์ ID ดังนั้นแม้ว่าจะมีชื่อและนามสกุลซ้ำกันก็ตาม ID ควรแตกต่างกัน
cadrell0

1
โซลูชันของคุณใช้งานได้กับประเภทดั้งเดิม แต่ดูเหมือนจะไม่ทำงานกับวัตถุ ในกรณีของฉัน FirstName เป็นวัตถุโดเมนในขณะที่ LastName เป็นวัตถุโดเมนอื่น เมื่อฉันรวมผลลัพธ์ทั้งสองเข้าด้วยกัน LINQ จะสร้าง NotSupportedException (ประเภทใน Union หรือ Concat จะถูกสร้างขึ้นมาร่วมกัน) คุณเคยประสบปัญหาคล้ายกันหรือไม่?
Candy Chiu

1
@CandyChiu: จริง ๆ แล้วฉันไม่เคยพบกรณีดังกล่าว ฉันเดาว่าเป็นข้อ จำกัด กับผู้ให้บริการค้นหาของคุณ คุณอาจต้องการใช้ LINQ กับวัตถุในกรณีนั้นโดยเรียกAsEnumerable()ก่อนที่จะทำการรวม / การต่อข้อมูล ลองและดูว่าจะไปอย่างไร หากนี่ไม่ใช่เส้นทางที่คุณต้องการฉันไม่แน่ใจว่าฉันสามารถช่วยเหลือได้มากกว่านี้
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หลาย ๆ ครั้งตามที่มีองค์ประกอบอยู่ใน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
แทนที่จะใช้พจนานุกรมคุณสามารถใช้การค้นหาซึ่งมีฟังก์ชันการทำงานที่แสดงในวิธีการขยายตัวช่วยของคุณ ตัวอย่างเช่นคุณสามารถเขียนa.GroupBy(selectKeyA).ToDictionary();เป็นa.ToLookup(selectKeyA)และเป็นadict.OuterGet(key) alookup[key]การได้รับชุดของคีย์นั้นเป็นเรื่องที่ยุ่งยากกว่าเล็กน้อย: alookup.Select(x => x.Keys).
Martin ที่มีความเสี่ยง

1
@RiskyMartin ขอบคุณ! นั่นทำให้สิ่งทั้งหมดสวยงามยิ่งขึ้น ฉันปรับปรุงคำตอบและ ideone-s (ฉันคิดว่าประสิทธิภาพควรเพิ่มขึ้นเนื่องจากมีการสร้างอินสแตนซ์ของวัตถุน้อยลง)
sehe

1
@Revious ที่ใช้งานได้ก็ต่อเมื่อคุณรู้ว่าคีย์นั้นไม่เหมือนใคร และนั่นไม่ใช่กรณีทั่วไปสำหรับ / การจัดกลุ่ม / นอกเหนือจากนั้นใช่โดยทั้งหมด ถ้าคุณรู้ว่าแฮชจะไม่ทำการลากแบบสมบูรณ์ (คอนเทนเนอร์ที่ใช้โหนดมีค่าใช้จ่ายสูงกว่าในหลักการและการแฮชไม่ได้ฟรีและประสิทธิภาพขึ้นอยู่กับฟังก์ชันแฮช / การแพร่กระจายของถัง) มันจะมีประสิทธิภาพมากกว่า ดังนั้นสำหรับโหลดขนาดเล็กผมคาดว่ามันอาจจะไม่เร็ว
sehe

27

ฉันคิดว่ามีปัญหากับสิ่งเหล่านี้ส่วนใหญ่รวมถึงคำตอบที่ได้รับการยอมรับเพราะพวกเขาทำงานได้ไม่ดีกับ Linq มากกว่า IQueryable ทั้งเนื่องจากการทำเซิร์ฟเวอร์ไปกลับมากเกินไปและส่งคืนข้อมูลมากเกินไปหรือทำการประมวลผลไคลเอ็นต์มากเกินไป

สำหรับ IEnumerable ฉันไม่ชอบคำตอบของ Sehe หรือสิ่งที่คล้ายกันเพราะมีการใช้หน่วยความจำมากเกินไป (การทดสอบสองรายการ 10,000,000 รายการอย่างง่ายวิ่ง Linqpad ออกจากหน่วยความจำในเครื่อง 32GB ของฉัน)

นอกจากนี้คนอื่น ๆ ส่วนใหญ่ไม่ได้ใช้การเข้าร่วมเต็มรูปแบบภายนอกอย่างเหมาะสมเพราะพวกเขากำลังใช้สหภาพที่มีการเข้าร่วมที่ถูกต้องแทนที่จะเข้าร่วม Concat กับการเข้าร่วมกึ่งต่อต้านขวาซึ่งไม่เพียง แต่กำจัดแถวเข้าร่วมด้านในซ้ำจากผลลัพธ์ รายการซ้ำที่เหมาะสมที่มีอยู่เดิมในข้อมูลด้านซ้ายหรือด้านขวา

ดังนั้นนี่คือส่วนขยายของฉันที่จัดการกับปัญหาเหล่านี้ทั้งหมดสร้าง SQL รวมถึงการนำการเข้าร่วมใน LINQ ไปยัง SQL โดยตรงดำเนินการบนเซิร์ฟเวอร์และเร็วกว่าและมีหน่วยความจำน้อยกว่าที่อื่น ๆ บน Enumerables:

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 นั้นส่วนใหญ่เป็น moot กับ Linq ไปยัง Objects หรือในซอร์ส แต่สร้างความแตกต่างบนฝั่งเซิร์ฟเวอร์ (SQL) ในคำตอบสุดท้ายโดยไม่จำเป็น JOINในคำตอบสุดท้ายลบที่ไม่จำเป็น

การเขียนโค้ดด้วยมือExpressionเพื่อจัดการการรวมExpression<Func<>>เข้ากับแลมบ์ดานั้นสามารถปรับปรุงได้ด้วย LinqKit แต่มันจะดีถ้าภาษา / คอมไพเลอร์ได้เพิ่มความช่วยเหลือลงไป FullOuterJoinDistinctและRightOuterJoinฟังก์ชั่นจะมีอยู่ครบถ้วน แต่ผมไม่ได้ใช้อีกครั้งFullOuterGroupJoinเลย

ฉันเขียนการรวมภายนอกแบบเต็มสำหรับอีกรุ่นหนึ่งIEnumerableสำหรับกรณีที่คีย์สามารถสั่งซื้อได้ซึ่งเร็วกว่าการรวมการรวมภายนอกด้านซ้ายเข้ากับการรวมกึ่งต่อต้านขวาอย่างน้อย 50% อย่างน้อยในคอลเล็กชั่นเล็ก ๆ มันจะผ่านการรวบรวมแต่ละครั้งหลังจากเรียงลำดับเพียงครั้งเดียว

ฉันยังเพิ่มคำตอบสำหรับรุ่นที่ทำงานกับ EF ด้วยการแทนที่Invokeด้วยส่วนขยายที่กำหนดเอง


จัดการกับTP unusedP, TC unusedCอะไร พวกเขาไม่ได้ใช้อย่างแท้จริง?
Rudey

ใช่พวกเขาเป็นเพียงนำเสนอให้จับประเภทในTP, TC, การสร้างที่เหมาะสมTResult Expression<Func<>>ฉันควรฉันสามารถแทนที่พวกเขาด้วย_, __, ___แทน แต่ที่ดูเหมือนจะไม่ใด ๆ ที่ชัดเจนจนกว่า C # มีตัวแทนพารามิเตอร์ที่เหมาะสมที่จะใช้แทน
NetMage

1
@MarcL ฉันไม่แน่ใจเกี่ยวกับ 'น่าเบื่อ' - แต่ฉันเห็นด้วยกับคำตอบนี้มีประโยชน์มากในบริบทนี้ สิ่งที่น่าประทับใจ (ถึงฉันมันเป็นการยืนยันข้อบกพร่องของ Linq-to-SQL)
2560

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 ที่ไม่ซ้ำกันหากคุณต้องการป้องกันไม่ให้มีการลบรายการที่ซ้ำกัน ใช่. สหภาพเป็นเรื่องสิ้นเปลืองเพียงเล็กน้อยเว้นแต่ว่าคุณสามารถบอกได้ว่ามีรหัสที่ไม่ซ้ำกันและสหภาพเปลี่ยนไปรวมกันทั้งหมด (ผ่านการวิเคราะห์พฤติกรรมภายใน / การปรับให้เหมาะสม) แต่มันจะได้ผล
TamusJRoyce

เช่นเดียวกับคำตอบที่ได้รับการยอมรับ
Gert Arnold

7

ฉันคาดเดาว่าวิธีของ @ sehe นั้นแข็งแกร่งกว่า แต่ก่อนที่ฉันจะเข้าใจได้ดีขึ้นฉันก็พบว่าตัวเองก้าวกระโดดจากส่วนขยายของ @ MichaelSander ผมปรับเปลี่ยนให้ตรงกับรูปแบบและผลตอบแทนประเภทของในตัว Enumerable.Join () วิธีการที่อธิบายไว้ที่นี่ ฉันผนวกส่วนต่อท้าย "ชัดเจน" ตามความคิดเห็นของ @ cadrell0 ภายใต้โซลูชันของ @ JeffMercado

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 และคุณสมบัติอื่น ๆ ดังนั้นข้อเสนอแนะยินดีต้อนรับอย่างแน่นอน

แก้ไข:ใช้เวลาไม่นานที่ฉันจะรู้ว่ามีปัญหากับรหัสของฉัน ฉันกำลังทำ. Dump () ใน LINQPad และดูประเภทผลตอบแทน มันเป็นแค่ IEnumerable ดังนั้นฉันจึงพยายามจับคู่มัน แต่เมื่อฉันทำ. Where () หรือ. Select () ในส่วนขยายของฉันฉันพบข้อผิดพลาด: "'System Collections.IEnumerable' ไม่มีคำจำกัดความสำหรับ 'Select' และ ... " ดังนั้นในที่สุดฉันก็สามารถจับคู่ไวยากรณ์อินพุตของ. Join () แต่ไม่ใช่พฤติกรรมการส่งคืน

แก้ไข:เพิ่ม "TResult" ให้กับประเภทส่งคืนสำหรับฟังก์ชัน พลาดที่เมื่ออ่านบทความ Microsoft และแน่นอนมันเหมาะสม ด้วยการแก้ไขนี้ตอนนี้ดูเหมือนว่าพฤติกรรมการส่งคืนจะเป็นไปตามเป้าหมายของฉัน


+2 สำหรับคำตอบนี้รวมถึง Michael Sanders ฉันคลิกที่นี่โดยไม่ตั้งใจและการลงคะแนนถูกล็อค กรุณาเพิ่มสอง
TamusJRoyce

@ TamusJRoyce ฉันเพิ่งเข้าไปแก้ไขรูปแบบโค้ดเล็กน้อย ฉันเชื่อว่าหลังจากแก้ไขแล้วคุณมีตัวเลือกในการลงคะแนนใหม่ ให้มันยิงถ้าคุณชอบ
pwilcox

ขอบคุณมาก!
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 แต่ไม่ได้ใช้การประมวลผลที่เลื่อนออกไป ดังนั้นหลังจากดูที่. NET แหล่งLINQ-to-objectsฉันจึงได้พบกับสิ่งนี้:

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 จะคาดหวัง


มันไม่ได้รักษาลำดับการป้อนข้อมูล: การค้นหาไม่รับประกันว่าดังนั้น foreaches เหล่านี้จะแจกแจงในลำดับของด้านซ้ายแล้วลำดับของด้านขวาบางส่วนจะไม่ปรากฏในด้านซ้าย แต่ลำดับองค์ประกอบที่สัมพันธ์กันจะไม่ถูกรักษาไว้
Ivan Danilov

@IvanDanilov คุณถูกต้องว่านี่ไม่ใช่สัญญา อย่างไรก็ตามการนำไปใช้ของ ToLookup ใช้คลาสค้นหาภายในใน Enumerable.cs ที่เก็บการจัดกลุ่มไว้ในรายการที่ลิงก์ที่เรียงลำดับโดยใช้การแทรกและใช้รายการนี้เพื่อวนซ้ำ ดังนั้นในเวอร์ชัน NET ปัจจุบันรับประกันการสั่งซื้อ แต่เนื่องจาก MS โชคไม่ดีที่ไม่ได้บันทึกสิ่งนี้พวกเขาสามารถเปลี่ยนได้ในรุ่นที่ใหม่กว่า
Søren Boisen

ฉันลองใน. NET 4.5.1 บน Win 8.1 และไม่รักษาลำดับ
Ivan Danilov

1
".. ลำดับการป้อนข้อมูลจะถูกระบุโดยการเรียก ToLookup อย่างกระตือรือร้น" แต่การนำไปปฏิบัติของคุณนั้นเหมือนกันทุกประการการให้ผลผลิตไม่ได้ให้ที่นี่มากเพราะค่าใช้จ่ายในเครื่องจักร จำกัด
pkuderov

4
การเรียกการค้นหาจะกระทำเมื่อมีการร้องขอองค์ประกอบแรกของผลลัพธ์และไม่ใช่เมื่อสร้างตัววนซ้ำ นั่นคือสิ่งที่การดำเนินการรอการตัดบัญชีหมายถึง คุณสามารถเลื่อนการแจงนับของอินพุตหนึ่งชุดให้ดียิ่งขึ้นโดยการวนซ้ำซ้ายไปโดยตรงแทนการแปลงเป็นการค้นหาส่งผลให้ได้รับประโยชน์พิเศษที่ลำดับของชุดซ้ายจะถูกเก็บไว้
Rolf

2

ฉันตัดสินใจที่จะเพิ่มคำตอบนี้เป็นคำตอบแยกต่างหากเนื่องจากฉันไม่แน่ใจว่ามีการทดสอบเพียงพอ นี่คือการนำFullOuterJoinวิธีการไปใช้อีกครั้งโดยใช้รุ่นที่เรียบง่ายและเป็นแบบกำหนดเองของLINQKit Invoke/ Expandเพื่อ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 การเข้ารหัสที่น่าประทับใจ! เมื่อฉันรันมันด้วยตัวอย่างง่ายๆและเมื่อ [NullVisitor.Visit (.. ) ถูกเรียกใช้ใน [base.Visit (Node)] มันจะโยน [System.ArgumentException: Argument Types ไม่ตรงกัน] สิ่งใดเป็นความจริงขณะที่ฉันใช้ [Guid] TKey และในบางจุดผู้เยี่ยมชมที่ไม่คาดคิดก็จะพิมพ์ [Guid?] อาจเป็นเพราะฉันคิดถึงบางสิ่ง ฉันมีตัวอย่างสั้น ๆ สำหรับ EF 6.4.4 โปรดแจ้งให้ฉันทราบว่าฉันจะแบ่งปันรหัสนี้กับคุณได้อย่างไร ขอบคุณ!
Troncho

@Troncho ปกติแล้วฉันจะใช้ LINQPad ในการทดสอบดังนั้น EF 6 จึงไม่ง่าย base.Visit(node)ไม่ควรโยนข้อยกเว้นเนื่องจากจะทำให้ต้นไม้เกิดขึ้นซ้ำ ฉันสามารถเข้าถึงบริการแบ่งปันรหัสได้ แต่ไม่สามารถตั้งค่าฐานข้อมูลทดสอบได้ แม้ว่าการทดสอบกับ LINQ กับ SQL ของฉันดูเหมือนว่าจะทำงานได้ดี
NetMage

@Troncho เป็นไปได้หรือไม่ว่าคุณจะเข้าร่วมระหว่างGuidคีย์และGuid?คีย์ต่างประเทศ
NetMage

ฉันใช้ LinqPad เพื่อทดสอบเช่นกัน เคียวรีของฉันขว้าง ArgumentException ดังนั้นฉันตัดสินใจที่จะดีบั๊กมันใน VS2019 บน [.Net Framework 4.7.1] และ EF 6 รุ่นล่าสุดที่นั่นฉันต้องติดตามปัญหาจริง เพื่อทดสอบรหัสของคุณฉันกำลังสร้างชุดข้อมูลแยกกัน 2 ชุดซึ่งมีต้นกำเนิดจากตาราง [บุคคล] เดียวกัน ฉันกรองทั้งสองชุดเพื่อให้บางระเบียนไม่ซ้ำกันสำหรับแต่ละชุดและบางรายการอยู่ในทั้งสองชุด [PersonId] คือ [คีย์หลัก] Guid (c #) / Uniqueidentifier (SqlServer) และไม่มีการตั้งค่าใด ๆ ที่สร้างค่า null [PersonId] รหัสที่ใช้ร่วมกัน: github.com/Troncho/EF_FullOuterJoin
Troncho

1

ดำเนินการแจงนับการสตรีมในหน่วยความจำผ่านทั้งอินพุตและเรียกใช้ตัวเลือกสำหรับแต่ละแถว หากมีความสัมพันธ์ที่ซ้ำปัจจุบันไม่มีหนึ่งของการขัดแย้งเลือกจะเป็นโมฆะ

ตัวอย่าง:

   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

^ สิ่งนี้จะช่วยให้คุณเห็น Intellisense ของฟังก์ชั่นการขยายในการรวบรวมวัตถุ 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
แต่น่าเสียดายที่ดูเหมือนว่าฟังก์ชั่นในSelectManyไม่สามารถแปลงเป็นต้นไม้นิพจน์ LINQ2SQL ที่คุ้มค่าดูเหมือนว่า
หรือผู้ทำแผนที่

edc65 ฉันรู้ว่ามันอาจเป็นคำถามที่โง่ถ้าคุณทำไปแล้ว แต่ในกรณี (ตามที่ฉันสังเกตเห็นบางคนไม่ทราบ) คุณเพียงแค่ต้องอ้างอิง namespace joinext
H7O

หรือแมปเปอร์แจ้งให้เราทราบด้วยคอลเล็กชันประเภทที่คุณต้องการให้ทำงาน ควรทำงานได้ดีกับคอลเลกชัน IEnumerable ใด ๆ
H7O

0

ฉันคิดว่า LINQ join clause ไม่ได้เป็นทางออกที่ถูกต้องสำหรับปัญหานี้เนื่องจากวัตถุประสงค์ของการรวม clause ไม่ได้เป็นการรวบรวมข้อมูลในลักษณะที่จำเป็นสำหรับโซลูชันงานนี้ รหัสที่จะผสานสร้างแยกคอลเลกชันที่ซับซ้อนเกินไปมันอาจจะโอเคสำหรับวัตถุประสงค์การเรียนรู้ แต่ไม่ใช่สำหรับการใช้งานจริง อีกวิธีหนึ่งในการแก้ปัญหานี้อยู่ในรหัสด้านล่าง:

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 loops สามารถใช้รหัสด้านล่าง:

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

ขอบคุณทุกคนสำหรับกระทู้ที่น่าสนใจ!

ฉันแก้ไขรหัสเพราะในกรณีที่ฉันต้องการ

  • ส่วนบุคคลเข้าร่วมกริยา
  • สหภาพส่วนบุคคล Comparer ที่แตกต่างกัน

สำหรับผู้ที่สนใจนี่คือรหัสที่ฉันแก้ไข (ใน 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

อีกหนึ่งการเข้าร่วมเต็มรูปแบบนอก

เนื่องจากไม่มีความสุขกับความเรียบง่ายและความสามารถในการอ่านของข้อเสนออื่น ๆ ฉันจึงจบลงด้วยสิ่งนี้:

ไม่ได้มีข้ออ้างใด ๆ ที่จะเร็ว (ประมาณ 800 มิลลิวินาทีในการเข้าร่วม 1,000 * 1,000 บน CPU 2020m: 2.4 กิกะเฮิร์ตซ์ / 2 แกน) สำหรับฉันมันเป็นเพียงแค่การเข้าร่วมด้านนอกเต็มรูปแบบขนาดกะทัดรัดและไม่เป็นทางการ

มันทำงานเหมือนกับ SQL FULL OUTER OOTER 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. Build Ids ตามผู้สร้างฟังก์ชั่นหลักที่ให้ไว้
  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
ทำไมไม่ลองวาง abstractions ให้ได้มากที่สุดและทำในรหัสเครื่อง? (คำแนะนำ: เพราะ abstractions คำสั่งที่สูงกว่าทำให้ชีวิตง่ายขึ้นสำหรับโปรแกรมเมอร์) สิ่งนี้ไม่ตอบคำถามและดูเหมือนฉันจะพูดจาโผงผางกับ LINQ มากกว่า
อะไรต่อมิอะไร

8
ใครบอกว่าข้อมูลมาจากฐานข้อมูล
247702

1
แน่นอนว่ามันเป็นฐานข้อมูลมีคำว่า "outer join" ในคำถาม :) google.cz/search?q=outer+join
Milan Švec

1
ฉันเข้าใจว่านี่เป็นวิธีการแก้ปัญหา "แบบเก่า" แต่ก่อน downvoting เปรียบเทียบความซับซ้อนกับโซลูชันอื่น ๆ :) ยกเว้นสิ่งที่ยอมรับแล้วแน่นอนว่าเป็นวิธีที่ถูกต้อง
Milan Švec

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