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

5voto

KeithS Points 36130

Comme vous l'avez constaté, Linq n'a pas de construction de "jointure externe". Le plus proche que vous puissiez obtenir est une jointure externe gauche en utilisant la requête que vous avez indiquée. À cela, vous pouvez ajouter tous les éléments de la liste des noms de famille qui ne sont pas représentés dans la jointure :

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

0 votes

Vous avez tout faux. LINQ a une jointure externe, Enumerable.DefautIfEmpty() génère cela. Ce que LINQ n'a pas, c'est la jointure externe complète.

0 votes

Si vous connaissiez un peu le SQL, vous sauriez que FULL JOIN, FULL OUTER JOIN et OUTER JOIN sont tous synonymes dans les requêtes. Mais si vous n'avez rien de mieux à faire que de troller un message vieux de 9 ans, je ne suis pas surpris par votre ignorance.

5voto

Guido Mocha Points 544

Ma solution propre pour la situation où la clé est unique dans les deux énumérables :

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

donc

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

sorties :

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

4voto

Søren Boisen Points 88

J'aime la réponse de Sehe, mais elle n'utilise pas l'exécution différée (les séquences d'entrée sont énumérées avec empressement par les appels à ToLookup). Donc, après avoir examiné les sources .NET pour LINQ-to-objects j'ai trouvé ça :

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

Cette mise en œuvre présente les propriétés importantes suivantes :

  • Exécution différée, les séquences d'entrée ne seront pas énumérées avant que la séquence de sortie ne le soit.
  • N'énumère les séquences d'entrée qu'une fois chacune.
  • Préserve l'ordre des séquences d'entrée, dans le sens où il produira des tuples dans l'ordre de la séquence de gauche puis de la droite (pour les clés non présentes dans la séquence de gauche).

Ces propriétés sont importantes, parce qu'elles correspondent à ce que quelqu'un de nouveau à FullOuterJoin mais expérimenté avec LINQ attendra.

0 votes

Il ne préserve pas l'ordre des séquences d'entrée : Lookup ne le garantit pas, donc ces foreaches vont énumérer dans un certain ordre du côté gauche, puis un certain ordre du côté droit non présent dans le côté gauche. Mais l'ordre relationnel des éléments n'est pas préservé.

0 votes

@IvanDanilov Vous avez raison de dire que cela ne figure pas dans le contrat. L'implémentation de ToLookup, cependant, utilise une classe Lookup interne dans Enumerable.cs qui conserve les groupements dans une liste liée ordonnée par insertion et utilise cette liste pour les parcourir. Ainsi, dans la version actuelle de .NET, l'ordre est garanti, mais comme MS n'a malheureusement pas documenté cela, ils pourraient le changer dans des versions ultérieures.

0 votes

Je l'ai essayé avec .NET 4.5.1 sur Win 8.1, et il ne préserve pas l'ordre.

3voto

NetMage Points 163

J'ai décidé d'ajouter ce point dans une réponse séparée car je ne suis pas sûr qu'il soit suffisamment testé. Il s'agit d'une réimplémentation de la fonction FullOuterJoin en utilisant essentiellement une version simplifiée et personnalisée de LINQKit Invoke / Expand para Expression pour qu'il fonctionne avec Entity Framework. Il n'y a pas beaucoup d'explications car c'est à peu près la même chose que ma réponse précédente.

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

1 votes

NetMage, un codage impressionnant ! Quand je l'exécute avec un exemple simple, et quand le [NullVisitor.Visit(..) est invoqué dans [base.Visit(Node)], il jette une [System.ArgumentException : Argument Types do not match]. Ce qui est vrai, car j'utilise une TKey [Guid] et à un moment donné, le visiteur null s'attend à un type [Guid ? Il se peut que quelque chose m'échappe. J'ai un petit exemple codé pour EF 6.4.4. Merci de me dire comment je peux partager ce code avec vous. Merci.

0 votes

@Troncho J'utilise normalement LINQPad pour les tests, donc EF 6 n'est pas facile à faire. base.Visit(node) ne devrait pas lever une exception, car cela ne fait que récurer l'arbre. Je peux accéder à presque tous les services de partage de code, mais je ne peux pas configurer une base de données de test. L'exécution contre mon test LINQ to SQL semble fonctionner correctement, cependant.

0 votes

@Troncho Est-il possible que vous vous joigniez entre une Guid et une Guid? clé étrangère ?

1voto

vrluckyin Points 551

En fait, la jointure externe complète de la liste 1 et de la liste 2 pourrait être :

A. Jointure externe gauche entre List1 (tous les éléments de List1) et List2 (éléments correspondant à List1)

B. Les éléments de la liste 2 qui n'ont pas pu être inclus dans le jeu de résultats de la jointure externe A-gauche.

C. Fusionner l'ensemble de résultats B avec l'ensemble A.

Le code ci-dessous pourrait vous donner de meilleures performances. En se référant au même exemple donné par "Jeff Mercado".

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 leftData = (from first in firstNames
                join last in lastNames on first.ID equals last.ID into temp
                from last in temp.DefaultIfEmpty(new { first.ID, Name = default(string) })
                select new
                {
                   first.ID,
                   FirstName = first.Name,
                   LastName = last.Name,
                 });

var rightRemainingData = (from r in lastNames
                          where !(from a in leftData select a.ID).Contains(r.ID)
                          select new
                          {
                             r.ID,
                             FirstName = default(string),
                             LastName = r.Name
                           });

var fullOuterjoinData = leftData.Concat(rightRemainingData);

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