256 votes

LINQ - Jointure externe complète

J'ai une liste d'identifiants de personnes et de leur prénom, et une liste d'identifiants de personnes et de leur nom de famille. Certaines personnes n'ont pas de prénom et d'autres n'ont pas de nom de famille ; j'aimerais faire une jointure externe complète sur les deux listes.

Donc les listes suivantes :

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

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

Devrait produire :

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

Je suis nouveau dans le domaine de LINQ (donc pardonnez-moi si je suis nul) et j'ai trouvé plusieurs solutions pour les "LINQ Outer Joins" qui se ressemblent toutes, mais qui semblent être des jointures externes gauches.

Mes tentatives jusqu'à présent ressemblent à ceci :

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

Mais ceci revient :

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

Qu'est-ce que je fais de mal ?

2 votes

Avez-vous besoin que cela fonctionne pour les listes en mémoire uniquement, ou pour Linq2Sql ?

0 votes

1voto

H7O Points 1

J'ai écrit cette classe d'extensions pour une application il y a peut-être 6 ans, et je l'ai utilisée depuis dans de nombreuses solutions sans problème. J'espère que cela vous aidera.

edit : J'ai remarqué que certains ne savent peut-être pas comment utiliser une classe d'extension.

Pour utiliser cette classe d'extension, il suffit de référencer son espace de nom dans votre classe en ajoutant la ligne suivante en utilisant joinext ;

Ceci devrait vous permettre de voir l'intellisense des fonctions d'extension sur toute collection d'objets IEnumerable que vous utilisez.

J'espère que cela vous aidera. Faites-moi savoir si ce n'est toujours pas clair, et j'espère pouvoir écrire un exemple d'utilisation.

Maintenant, voici la classe :

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 votes

Malheureusement, il semble que la fonction dans SelectMany ne peut pas être converti en un arbre d'expression digne de LINQ2SQL, il semble.

0 votes

Edc65. Je sais que c'est peut-être une question stupide si vous l'avez déjà fait. Mais juste au cas où (comme j'ai remarqué que certains ne le savent pas), il suffit de référencer l'espace de nom joinext.

0 votes

O. R. Mapper, faites-moi savoir avec quel type de collection vous vouliez qu'il fonctionne. Cela devrait fonctionner correctement avec n'importe quelle collection IEnumerable.

1voto

Effectue une énumération en continu en mémoire sur les deux entrées et invoque le sélecteur pour chaque ligne. S'il n'y a pas de corrélation à l'itération actuelle, l'un des arguments du sélecteur sera nul .

Ejemplo:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Requiert un IComparer pour le type de corrélation, utilise Comparer.Default s'il n'est pas fourni.

  • Requiert que 'OrderBy' soit appliqué aux énumérables d'entrée.

    /// <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 votes

C'est un effort héroïque pour rendre les choses "fluides". Malheureusement, tout le gain est perdu à la première étape, où vous effectuez OrderBy sur les deux projections clés. OrderBy met en mémoire tampon la séquence entière, pour des raisons évidentes .

0 votes

@sehe Vous avez tout à fait raison pour Linq to Objects. Si les IEnumerable<T> sont des IQueryable<T>, la source devrait trier - je n'ai pas eu le temps de tester. Si je me trompe, le simple fait de remplacer l'entrée IEnumerable<T> par IQueryable<T> devrait permettre de trier dans la source/base de données.

0voto

Jointure externe complète pour deux tables ou plus : Commencez par extraire la colonne sur laquelle vous voulez faire la jointure.

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

Ensuite, utilisez la jointure externe gauche entre la colonne extraite et les tables principales.

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

0voto

Sharunas Bielskis Points 478

Je pense que la clause de jointure LINQ n'est pas la bonne solution à ce problème, parce que le but de la clause de jointure n'est pas d'accumuler les données de la manière requise pour la solution de cette tâche. Le code pour fusionner les collections séparées créées devient trop compliqué, peut-être que c'est OK pour l'apprentissage, mais pas pour les applications réelles. L'une des façons de résoudre ce problème est le code ci-dessous :

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

Si les collections réelles sont grandes pour la formation de HashSet au lieu de boucles foreach peut être utilisé le code ci-dessous :

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

0voto

Merci à tous pour ces messages intéressants !

J'ai modifié le code car dans mon cas j'avais besoin de

  • a prédicat de jointure personnalisé
  • a Comparateur distinct d'union personnalisé

Pour ceux qui sont intéressés voici mon code modifié (en VB, désolé)

    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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X