LINQ - Gabung Luar Penuh


203

Saya memiliki daftar ID orang dan nama depan mereka, dan daftar ID orang dan nama keluarga mereka. Beberapa orang tidak memiliki nama depan dan beberapa tidak memiliki nama keluarga; Saya ingin melakukan join luar penuh pada dua daftar.

Jadi daftar berikut ini:

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

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

Harus menghasilkan:

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

Saya baru mengenal LINQ (jadi maafkan saya jika saya lumpuh) dan telah menemukan beberapa solusi untuk 'LINQ Outer Joins' yang semuanya terlihat sangat mirip, tetapi tampaknya benar-benar menjadi gabungan luar.

Upaya saya sejauh ini berlangsung seperti ini:

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

Tapi ini kembali:

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

Apa yang saya lakukan salah?


2
Apakah Anda perlu ini bekerja hanya untuk daftar di memori, atau untuk Linq2Sql?
JamesFaix

Jawaban:


123

Saya tidak tahu apakah ini mencakup semua kasus, secara logis sepertinya benar. Idenya adalah untuk mengambil gabungan luar kiri dan bergabung luar kanan kemudian mengambil gabungan hasil.

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

Ini berfungsi seperti yang ditulis karena ada dalam LINQ to Objects. Jika LINQ ke SQL atau yang lain, prosesor kueri mungkin tidak mendukung navigasi yang aman atau operasi lainnya. Anda harus menggunakan operator kondisional untuk mendapatkan nilai secara kondisional.

yaitu,

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
Serikat pekerja akan menghilangkan duplikat. Jika Anda tidak mengharapkan duplikat, atau dapat menulis kueri kedua untuk mengecualikan apa pun yang termasuk dalam yang pertama, gunakan Concat sebagai gantinya. Ini adalah perbedaan SQL antara UNION dan UNION ALL
cadrell0

3
@ cadre110 duplikat akan terjadi jika seseorang memiliki nama depan dan nama belakang, jadi penyatuan adalah pilihan yang valid.
saus

1
@saus tetapi ada kolom ID, jadi meskipun ada duplikat nama depan dan belakang, ID harus berbeda
cadrell0

1
Solusi Anda berfungsi untuk tipe primitif, tetapi tampaknya tidak berfungsi untuk objek. Dalam kasus saya, FirstName adalah objek domain, sedangkan LastName adalah objek domain lain. Ketika saya menyatukan dua hasil, LINQ melempar NotSupportedException (Jenis dalam Union atau Concat dibangun secara tidak kompatibel). Pernahkah Anda mengalami masalah serupa?
Candy Chiu

1
@CandyChiu: Sebenarnya saya tidak pernah mengalami kasus seperti itu. Saya kira itu batasan dengan penyedia kueri Anda. Anda mungkin ingin menggunakan LINQ untuk Objek dalam kasus itu dengan menelepon AsEnumerable()sebelum Anda melakukan penyatuan / penggabungan. Coba itu dan lihat bagaimana hasilnya. Jika ini bukan rute yang ingin Anda tuju, saya tidak yakin saya bisa membantu lebih dari itu.
Jeff Mercado

196

Pembaruan 1: menyediakan metode ekstensi yang benar-benar umum. FullOuterJoin
Pembaruan 2: opsional menerima kebiasaan IEqualityCompareruntuk jenis kunci
Pembaruan 3 : implementasi ini baru-baru ini menjadi bagian dariMoreLinq - Terima kasih kawan!

Edit Added FullOuterGroupJoin( ideone ). Saya menggunakan kembali GetOuter<>implementasinya, menjadikan ini fraksi yang lebih sedikit performanya daripada yang seharusnya, tapi saya bertujuan untuk kode 'tingkat tinggi', bukan dioptimalkan, sekarang.

Lihat langsung di 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);
}

Mencetak output:

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

Anda juga dapat menyediakan default: 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)" }
        )

Pencetakan:

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

Penjelasan istilah yang digunakan:

Bergabung adalah istilah yang dipinjam dari desain basis data relasional:

  • Sebuah bergabung akan mengulangi unsur-unsur dari asebanyak ada unsur-unsur dalam b dengan kunci yang sesuai (yaitu: apa-apa jika byang kosong). Database panggilan istilah iniinner (equi)join .
  • Sebuah luar bergabung mencakup unsur-unsur dari ayang tidak ada unsur yang sesuai ada di b. (yaitu: hasil bahkan jika bkosong). Ini biasanya disebut sebagaileft join .
  • Sebuah penuh luar bergabung termasuk catatan dari a sertab jika ada unsur yang sesuai ada yang lain. (yaitu hasil bahkan jika akosong)

Sesuatu yang biasanya tidak terlihat di RDBMS adalah grup bergabung [1] :

  • Sebuah kelompok bergabung , melakukan hal yang sama seperti dijelaskan di atas, tapi bukannya mengulang elemen dari auntuk beberapa yang sesuai b, itu kelompok catatan dengan tombol yang sesuai. Ini sering lebih nyaman ketika Anda ingin menghitung melalui catatan 'bergabung', berdasarkan pada kunci umum.

Lihat juga GroupJoin yang berisi beberapa penjelasan latar belakang umum juga.


[1] (Saya percaya Oracle dan MSSQL memiliki ekstensi untuk ini)

Kode lengkap

Kelas Ekstensi 'drop-in' umum untuk ini

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

Diedit untuk menunjukkan penggunaan FullOuterJoinmetode ekstensi yang disediakan
lihat

Diedit: Metode ekstensi FullOuterGroupJoin ditambahkan
lihat

4
Alih-alih menggunakan Kamus, Anda dapat menggunakan Pencarian , yang berisi fungsionalitas yang dinyatakan dalam metode ekstensi pembantu Anda. Misalnya, Anda dapat menulis a.GroupBy(selectKeyA).ToDictionary();sebagai a.ToLookup(selectKeyA)dan adict.OuterGet(key)sebagai alookup[key]. Mendapatkan koleksi kunci sedikit rumit, meskipun: alookup.Select(x => x.Keys).
Risky Martin

1
@RiskyMartin Terima kasih! Itu, memang, membuat semuanya lebih elegan. Saya memperbarui jawaban dan ideone-s. (Saya kira kinerja harus ditingkatkan karena lebih sedikit objek yang dipakai).
lihat

1
@Revious yang hanya berfungsi jika Anda tahu kunci-kunci itu unik. Dan itu bukan kasus umum untuk / pengelompokan /. Selain itu, ya, tentu saja. Jika Anda tahu hash tidak akan menyeret perf (wadah berbasis node memiliki lebih banyak biaya pada prinsipnya, dan hashing tidak gratis dan efisiensi tergantung pada fungsi hash / penyebaran ember), itu pasti akan lebih efisien secara algoritma. Jadi, untuk muatan kecil, saya perkirakan mungkin tidak akan lebih cepat
lihat

27

Saya pikir ada masalah dengan sebagian besar dari ini, termasuk jawaban yang diterima, karena mereka tidak bekerja dengan baik dengan Linq atas IQueryable baik karena melakukan bolak-balik server terlalu banyak dan terlalu banyak pengembalian data, atau melakukan terlalu banyak eksekusi klien.

Untuk IEnumerable, saya tidak suka jawaban Sehe atau serupa karena memiliki penggunaan memori yang berlebihan (tes daftar sederhana 10000000 menjalankan Linqpad kehabisan memori pada mesin 32GB saya).

Juga, sebagian besar yang lain tidak benar-benar menerapkan Full Outer Join yang tepat karena mereka menggunakan Union dengan Right Join daripada Concat dengan Right Anti Semi Join, yang tidak hanya menghilangkan duplikat inner join rows dari hasil, tetapi duplikat yang tepat yang ada awalnya di data kiri atau kanan.

Jadi di sini adalah ekstensi saya yang menangani semua masalah ini, menghasilkan SQL serta mengimplementasikan bergabung dalam LINQ ke SQL secara langsung, mengeksekusi di server, dan lebih cepat dan dengan memori lebih sedikit daripada yang lain di 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));
    }
}

Perbedaan antara Right Anti-Semi-Join sebagian besar diperdebatkan dengan Linq to Objects atau di sumbernya, tetapi membuat perbedaan di sisi server (SQL) dalam jawaban akhir, menghapus yang tidak perlu JOIN .

Pengkodean tangan Expressionuntuk menangani penggabungan Expression<Func<>>ke dalam lambda dapat ditingkatkan dengan LinqKit, tetapi akan lebih baik jika bahasa / kompiler menambahkan bantuan untuk itu. Fungsi FullOuterJoinDistinctdan RightOuterJointermasuk untuk kelengkapan, tapi saya tidak menerapkan kembaliFullOuterGroupJoin .

Saya menulis versi lain untuk join luar penuhIEnumerable untuk kasus-kasus di mana kuncinya dapat dipesan, yaitu sekitar 50% lebih cepat daripada menggabungkan gabungan luar kiri dengan anti-bergabung kanan, setidaknya pada koleksi kecil. Ini melewati setiap koleksi setelah memilah hanya sekali.

Saya juga menambahkan jawaban lain untuk versi yang bekerja dengan EF dengan mengganti Invokedengan ekspansi kustom.


Ada apa dengan ini TP unusedP, TC unusedC? Apakah mereka benar-benar tidak digunakan?
Rudey

Ya, mereka hanya hadir untuk menangkap jenis dalam TP, TC, TResultuntuk menciptakan yang tepat Expression<Func<>>. Aku seharusnya saya bisa menggantinya dengan _, __, ___bukan, tapi itu tampaknya tidak lebih jelas sampai C # memiliki wildcard parameter yang tepat untuk digunakan sebagai gantinya.
NetMage

1
@MarcL. Saya tidak begitu yakin tentang 'melelahkan' - tetapi saya setuju jawaban ini sangat berguna dalam konteks ini. Hal-hal yang mengesankan (meskipun bagi saya itu menegaskan kekurangan Linq-to-SQL)
sehe

3
Saya mendapatkan The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Apakah ada batasan dengan kode ini? Saya ingin melakukan FULL JOIN atas IQueryables
Learner

1
Saya telah menambahkan jawaban baru yang menggantikan Invokedengan kebiasaan ExpressionVisitoruntuk inline Invokesehingga harus bekerja dengan EF. Bisakah kamu mencobanya?
NetMage

7

Berikut adalah metode ekstensi yang melakukan itu:

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), yang berarti gabungan luar penuh = gabungan kiri luar gabungan kanan semua luar! Saya menghargai kesederhanaan dari pendekatan ini.
TamusJRoyce

1
@TamusJRoyce Kecuali Unionmenghapus duplikat, jadi jika ada baris duplikat di data asli, mereka tidak akan di hasilnya.
NetMage

Poin yang bagus! tambahkan id unik jika Anda ingin mencegah duplikat dihapus. Iya. Serikat pekerja agak boros kecuali jika Anda dapat mengisyaratkan bahwa ada id unik dan serikat pekerja beralih ke serikat pekerja semua (melalui heuristik / optimisasi internal). Tapi itu akan berhasil.
TamusJRoyce


7

Saya menduga pendekatan @ sehe lebih kuat, tetapi sampai saya memahaminya dengan lebih baik, saya mendapati diri saya melompat-lompat dari ekstensi @ MichaelSander. Saya memodifikasinya agar cocok dengan sintaks dan mengembalikan tipe metode bawaan Enumerable.Join () yang dijelaskan di sini . Saya menambahkan sufiks "berbeda" sehubungan dengan komentar @ cadrell0 di bawah solusi @ 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);
    }

}

Dalam contoh, Anda akan menggunakannya seperti ini:

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

Di masa depan, ketika saya belajar lebih banyak, saya merasa saya akan bermigrasi ke logika @hehe mengingat popularitasnya. Tetapi meskipun demikian saya harus berhati-hati, karena saya merasa penting untuk memiliki setidaknya satu kelebihan yang cocok dengan sintaksis dari metode ".Join ()" yang ada jika memungkinkan, karena dua alasan:

  1. Konsistensi dalam metode membantu menghemat waktu, menghindari kesalahan, dan menghindari perilaku yang tidak diinginkan.
  2. Jika ada metode ".FullJoin ()" yang out-of-the-box di masa depan, saya akan membayangkan itu akan mencoba untuk tetap menggunakan sintaks dari metode ".Join ()" yang ada saat ini jika bisa. Jika ya, maka jika Anda ingin bermigrasi ke sana, Anda dapat mengubah nama fungsi Anda tanpa mengubah parameter atau khawatir tentang berbagai jenis pengembalian yang melanggar kode Anda.

Saya masih baru dengan obat generik, ekstensi, pernyataan Func, dan fitur lainnya, jadi umpan balik tentu diterima.

EDIT: Tidak butuh waktu lama bagi saya untuk menyadari ada masalah dengan kode saya. Saya sedang melakukan .Dump () di LINQPad dan melihat jenis kembali. Itu hanya IEnumerable, jadi saya mencoba mencocokkannya. Tetapi ketika saya benar-benar melakukan .Where () atau .Pilih () pada ekstensi saya, saya mendapat kesalahan: "'System Collections.IEnumerable' tidak mengandung definisi untuk 'Pilih' dan ...". Jadi pada akhirnya saya bisa mencocokkan sintaks input .Gabung (), tetapi bukan perilaku pengembalian.

EDIT: Menambahkan "TResult" ke jenis kembali untuk fungsi. Kehilangan itu ketika membaca artikel Microsoft, dan tentu saja itu masuk akal. Dengan perbaikan ini, sekarang tampaknya perilaku pengembalian sejalan dengan tujuan saya.


+2 untuk jawaban ini dan juga Michael Sanders. Saya tidak sengaja mengklik ini dan suara terkunci. Silakan tambahkan dua.
TamusJRoyce

@TamusJRoyce, saya baru saja masuk untuk mengedit format kode sedikit. Saya percaya setelah pengeditan dilakukan, Anda memiliki opsi untuk menyusun kembali suara Anda. Cobalah jika Anda mau.
pwilcox

Terima kasih banyak!
Roshna Omer

6

Seperti yang Anda temukan, Linq tidak memiliki konstruksi "gabungan luar". Yang terdekat yang bisa Anda dapatkan adalah gabungan luar kiri menggunakan kueri yang Anda nyatakan. Untuk ini, Anda dapat menambahkan elemen apa pun dari daftar nama belakang yang tidak terwakili dalam bergabung:

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

Saya suka jawaban sehe, tetapi tidak menggunakan eksekusi yang ditunda (urutan input dengan bersemangat disebutkan oleh panggilan ke ToLookup). Jadi setelah melihat sumber .NET untuk objek -LINQ , saya datang dengan ini:

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

Implementasi ini memiliki sifat-sifat penting berikut:

  • Eksekusi yang ditunda, urutan input tidak akan disebutkan sebelum urutan output dihitung.
  • Hanya menghitung urutan input masing-masing satu kali.
  • Mempertahankan urutan urutan input, dalam arti akan menghasilkan tupel dalam urutan urutan kiri dan kemudian kanan (untuk kunci yang tidak ada dalam urutan kiri).

Properti ini penting, karena itulah yang diharapkan seseorang dari FullOuterJoin tetapi berpengalaman dengan LINQ.


Itu tidak mempertahankan urutan urutan input: Pencarian tidak menjamin itu, jadi foreaches ini akan menghitung dalam beberapa urutan sisi kiri, maka beberapa urutan sisi kanan tidak ada di sisi kiri. Tetapi tatanan unsur-unsur relasional tidak dipertahankan.
Ivan Danilov

@IvanDanilov Anda benar bahwa ini sebenarnya tidak ada dalam kontrak. Implementasi ToLookup, bagaimanapun, menggunakan kelas pencarian internal di Enumerable.cs yang membuat pengelompokan dalam daftar tertaut yang diperintahkan penyisipan dan menggunakan daftar ini untuk beralih melalui mereka. Jadi dalam versi .NET saat ini, pesanan dijamin, tetapi karena MS sayangnya belum mendokumentasikan ini, mereka dapat mengubahnya di versi yang lebih baru.
Søren Boisen

Saya mencobanya di .NET 4.5.1 pada Win 8.1, dan tidak mempertahankan pesanan.
Ivan Danilov

1
".. urutan input dengan penuh semangat disebutkan oleh panggilan ke ToLookup". Tetapi implementasi Anda melakukan hal yang persis sama. Menghasilkan tidak memberi banyak di sini karena biaya pada mesin negara-terbatas.
pkuderov

4
Panggilan pencarian dilakukan ketika elemen pertama dari hasil diminta, dan bukan ketika iterator dibuat. Itulah arti eksekusi yang ditangguhkan. Anda dapat menunda penghitungan satu set input lebih jauh, dengan mengulangi Enumerable kiri secara langsung alih-alih mengubahnya menjadi Lookup, menghasilkan manfaat tambahan bahwa urutan set kiri dipertahankan.
Rolf

2

Saya memutuskan untuk menambahkan ini sebagai jawaban terpisah karena saya tidak yakin itu sudah cukup diuji. Ini adalah implementasi ulang dari FullOuterJoinmetode yang menggunakan versi LINQKit Invoke/ Expanduntuk Expressionyang pada dasarnya disederhanakan dan disesuaikan sehingga harus bekerja dengan Entity Framework. Tidak ada banyak penjelasan karena hampir sama dengan jawaban saya sebelumnya.

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, pengkodean yang mengesankan! Ketika saya menjalankannya dengan contoh sederhana, dan ketika [NullVisitor.Visit (..) dipanggil dalam [base.Visit (Node)], ia melempar [System.ArgumentException: Jenis Argumen tidak cocok]. Yang benar, karena saya menggunakan TKK [Guid] dan pada titik tertentu pengunjung nol mengharapkan Tipe [Guid?]. Mungkin saya kehilangan sesuatu. Saya punya contoh singkat kode untuk EF 6.4.4. Tolong beri tahu saya bagaimana saya bisa membagikan kode ini kepada Anda. Terima kasih!
Troncho

@ Krono Saya biasanya menggunakan LINQPad untuk pengujian, jadi EF 6 tidak mudah dilakukan. base.Visit(node)tidak boleh melempar pengecualian karena itu hanya berulang turun pohon. Saya dapat mengakses hampir semua layanan berbagi kode, tetapi tidak menyiapkan database pengujian. Menjalankannya terhadap LINQ to SQL test saya tampaknya berfungsi dengan baik.
NetMage

@ Krono Mungkinkah Anda menggabungkan antara Guidkunci dan Guid?kunci asing?
NetMage

Saya menggunakan LinqPad untuk pengujian juga. Permintaan saya melempar ArgumentException jadi saya memutuskan untuk men-debug-nya di VS2019 di [.Net Framework 4.7.1] dan EF terbaru 6. Di sana saya harus melacak masalah sebenarnya. Untuk menguji kode Anda, saya membuat 2 set data terpisah yang berasal dari tabel [Orang] yang sama. Saya memfilter kedua set sehingga beberapa catatan unik untuk setiap set dan beberapa ada pada kedua set. [PersonId] adalah [Primary Key] Guid (c #) / Uniqueidentifier (SqlServer) dan tidak ada set yang menghasilkan nilai [PersonId] nol. Kode bersama: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Melakukan enumerasi streaming dalam memori pada kedua input dan memanggil pemilih untuk setiap baris. Jika tidak ada korelasi pada iterasi saat ini, salah satu argumen pemilih akan menjadi nol .

Contoh:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Membutuhkan IComparer untuk tipe korelasi, menggunakan Comparer.Default jika tidak disediakan.

  • Mengharuskan 'OrderBy' diterapkan pada input enumerables

    /// <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
Itu upaya heroik untuk membuat hal-hal "mengalir". Sayangnya, semua perolehan hilang pada langkah pertama, di mana Anda tampil OrderBydi kedua proyeksi utama. OrderBybuffer seluruh urutan, karena alasan yang jelas .
lihat

@sehe Anda pasti benar untuk Linq to Objects. Jika IEnumerable <T> adalah IQueryable <T>, sumber harus mengurutkan - tidak ada waktu untuk menguji. Jika saya salah tentang ini, cukup mengganti input IEnumerable <T> dengan IQueryable <T> harus mengurutkan dalam sumber / database.
James Caradoc-Davies

1

Solusi bersih saya untuk situasi yang penting adalah kunci di kedua enumerables:

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

begitu

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

output:

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

0

Gabung luar penuh untuk dua atau lebih tabel: Pertama, ekstrak kolom yang ingin Anda gabungkan.

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

Kemudian gunakan gabungan luar kiri antara kolom yang diekstraksi dan tabel utama.

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

Saya telah menulis kelas ekstensi ini untuk aplikasi mungkin 6 tahun yang lalu, dan telah menggunakannya sejak itu dalam banyak solusi tanpa masalah. Semoga ini bisa membantu.

sunting: Saya perhatikan beberapa mungkin tidak tahu cara menggunakan kelas ekstensi.

Untuk menggunakan kelas ekstensi ini, cukup referensi namespace-nya di kelas Anda dengan menambahkan baris berikut menggunakan joinext;

^ ini akan memungkinkan Anda untuk melihat intellisense fungsi ekstensi pada setiap koleksi objek IEnumerable yang kebetulan Anda gunakan.

Semoga ini membantu. Beri tahu saya jika masih belum jelas, dan saya harap saya akan menulis contoh contoh tentang cara menggunakannya.

Sekarang inilah kelasnya:

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
Sayangnya, tampaknya fungsi di SelectManytidak dapat dikonversi ke pohon ekspresi LINQ2SQL-layak, tampaknya.
ATAU Mapper

edc65. Saya tahu ini mungkin pertanyaan konyol jika Anda sudah melakukannya. Tapi untuk berjaga-jaga (seperti yang saya perhatikan beberapa tidak tahu), Anda hanya perlu referensi namespace joinext.
H7O

ATAU Mapper, beri tahu saya jenis koleksi yang Anda inginkan. Ini harus bekerja dengan baik dengan koleksi IEnumerable
H7O

0

Saya pikir bahwa LINQ join clause bukanlah solusi yang tepat untuk masalah ini, karena tujuan join clause bukanlah untuk mengakumulasi data sedemikian rupa seperti yang diperlukan untuk solusi tugas ini. Kode untuk menggabungkan koleksi terpisah yang dibuat menjadi terlalu rumit, mungkin itu OK untuk tujuan pembelajaran, tetapi tidak untuk aplikasi nyata. Salah satu cara untuk mengatasi masalah ini adalah dalam kode di bawah ini:

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

Jika koleksi asli besar untuk pembentukan HashSet, bukan foreach loop dapat menggunakan kode di bawah ini:

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

Terima kasih semuanya atas posting yang menarik!

Saya memodifikasi kode karena dalam kasus saya, saya perlu

  • sebuah Personalized bergabung predikat
  • sebuah serikat pribadi comparer yang berbeda

Bagi yang berminat ini adalah kode saya yang dimodifikasi (dalam VB, maaf)

    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

Namun bergabung dengan luar penuh lainnya

Karena tidak begitu senang dengan kesederhanaan dan keterbacaan proposisi lain, saya berakhir dengan ini:

Ia tidak memiliki pretensi untuk menjadi cepat (sekitar 800 ms untuk bergabung dengan 1000 * 1000 pada CPU 2020m: 2.4ghz / 2cores). Bagi saya, itu hanya gabungan luar yang ringkas dan kasual.

Ia bekerja sama dengan SQL FULL OUTER JOIN (duplikat konservasi)

Bersulang ;-)

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

Idenya adalah untuk

  1. Build Id berdasarkan pada pembangun fungsi utama yang disediakan
  2. Memproses item yang tersisa saja
  3. Proses bergabung dalam
  4. Hanya memproses barang yang benar

Berikut ini adalah tes singkat yang menyertainya:

Tempatkan break point di akhir untuk memverifikasi secara manual bahwa itu berperilaku seperti yang diharapkan

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

Saya sangat membenci ekspresi LINQ ini, inilah sebabnya SQL ada:

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

Buat ini sebagai tampilan sql dalam database dan impor sebagai entitas.

Tentu saja, gabungan (kiri) dari gabungan kiri dan kanan juga akan berhasil, tetapi itu bodoh.


11
Mengapa tidak hanya menjatuhkan abstraksi sebanyak mungkin dan melakukan ini dalam kode mesin? (Petunjuk: karena abstraksi tingkat tinggi membuat hidup lebih mudah bagi programmer). Ini tidak menjawab pertanyaan dan bagi saya lebih seperti kata-kata kasar terhadap LINQ.
pemboros

8
Siapa bilang data berasal dari database?
user247702

1
Tentu saja, ini adalah basis data, ada kata-kata "gabung luar" yang dimaksud :) google.cz/search?q=outer+join
Milan Švec

1
Saya mengerti bahwa ini adalah solusi "mode lama", tetapi sebelum downvoting, bandingkan kerumitannya dengan solusi lain :) Kecuali yang diterima, itu tentu saja yang benar.
Milan Švec

Tentu saja bisa menjadi database atau tidak. Saya mencari solusi dengan gabungan luar antara daftar di memori
edc65
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.