LINQ - Jointure externe complète


204

J'ai une liste des pièces d'identité et de leur prénom, ainsi qu'une liste des pièces d'identité et leur nom de famille. Certaines personnes n'ont pas de prénom et d'autres n'ont pas de nom de famille; Je voudrais 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 sur LINQ (alors pardonnez-moi si je suis boiteux) et j'ai trouvé pas mal de solutions pour les `` jointures externes LINQ '' qui semblent toutes assez similaires, mais semblent vraiment rester des jointures externes.

Mes tentatives jusqu'à présent vont quelque chose comme 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 cela revient:

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

Qu'est-ce que je fais mal?


2
Avez-vous besoin de cela pour fonctionner uniquement pour les listes en mémoire ou pour Linq2Sql?
JamesFaix

Réponses:


123

Je ne sais pas si cela couvre tous les cas, logiquement cela semble correct. L'idée est de prendre une jointure externe gauche et une jointure externe droite puis de prendre l'union des résultats.

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

Cela fonctionne comme écrit car il est dans LINQ to Objects. Si LINQ to SQL ou autre, le processeur de requêtes peut ne pas prendre en charge la navigation sécurisée ou d'autres opérations. Vous devez utiliser l'opérateur conditionnel pour obtenir conditionnellement les valeurs.

c'est à dire,

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
Union supprimera les doublons. Si vous ne vous attendez pas à des doublons ou si vous pouvez écrire la deuxième requête pour exclure tout ce qui était inclus dans la première, utilisez Concat à la place. Ceci est la différence SQL entre UNION et UNION ALL
cadrell0

3
@ cadre110 des doublons se produiront si une personne a un prénom et un nom, donc l'union est un choix valide.
saus

1
@saus mais il y a une colonne ID, donc même s'il y a un prénom et un nom en double, l'ID doit être différent
cadrell0

1
Votre solution fonctionne pour les types primitifs, mais ne semble pas fonctionner pour les objets. Dans mon cas, FirstName est un objet de domaine, tandis que LastName est un autre objet de domaine. Lorsque j'unis les deux résultats, LINQ a lancé une exception NotSupportedException (les types dans Union ou Concat sont construits de manière incompétente). Avez-vous rencontré des problèmes similaires?
Candy Chiu

1
@CandyChiu: En fait, je n'ai jamais rencontré un tel cas. Je suppose que c'est une limitation avec votre fournisseur de requêtes. Vous voudrez probablement utiliser LINQ to Objects dans ce cas en appelant AsEnumerable()avant d'effectuer l'union / la concaténation. Essayez cela et voyez comment cela se passe. Si ce n'est pas la route que vous souhaitez emprunter, je ne suis pas sûr de pouvoir vous être plus utile que cela.
Jeff Mercado

196

Mise à jour 1: fournir une méthode d'extension vraiment généralisée FullOuterJoin
Mise à jour 2: accepter éventuellement une personnalisation IEqualityComparerpour le type de clé
Mise à jour 3 : cette implémentation a récemment fait partie deMoreLinq - Merci les gars!

Édition ajoutée FullOuterGroupJoin( ideone ). J'ai réutilisé leGetOuter<> implémentation, ce qui en fait une fraction moins performante qu'elle ne pourrait l'être, mais je vise pour le moment un code de `` haut niveau '', pas optimisé à la pointe du progrès.

Regardez-le en direct sur 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);
}

Imprime la sortie:

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

Vous pouvez également fournir des valeurs par défaut: 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)" }
        )

Impression:

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

Explication des termes utilisés:

Rejoindre est un terme emprunté à la conception de bases de données relationnelles:

  • Une jointure répétera les éléments aautant de fois qu'il y a d'éléments b avec la clé correspondante (c'est-à-dire: rien si bétait vide). Le jargon de la base de données appelle celainner (equi)join .
  • Une jointure externe comprend des éléments apour lesquels aucun élément correspondant n'existe b. (c.-à-d. même les résultats bétaient vides). Ceci est généralement appeléleft join .
  • Une jointure externe complète inclut les enregistrements de a ainsi queb s'il n'existe aucun élément correspondant dans l'autre. (c'est-à-dire même si les résultats aétaient vides)

Quelque chose que l'on ne voit généralement pas dans le SGBDR est une jointure de groupe [1] :

  • Une jointure de groupe , fait la même chose que décrite ci-dessus, mais au lieu de répéter les éléments de apour plusieurs correspondants b, elle regroupe les enregistrements avec les clés correspondantes. C'est souvent plus pratique lorsque vous souhaitez énumérer des enregistrements «joints», sur la base d'une clé commune.

Voir aussi GroupJoin qui contient quelques explications de base générales aussi bien.


[1] (je crois qu'Oracle et MSSQL ont des extensions propriétaires pour cela)

Code complet

Une classe d'extension «drop-in» généralisée pour cette

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

Sous la direction de montrer l'utilisation de la FullOuterJoinméthode d'extension fourni
sehe

Modifié: FullOuterGroupJoin méthode d'extension ajoutée
sehe

4
Au lieu d'utiliser un dictionnaire, vous pouvez utiliser une recherche , qui contient les fonctionnalités exprimées dans vos méthodes d'extension d'assistance. Par exemple, vous pouvez écrire au a.GroupBy(selectKeyA).ToDictionary();fur a.ToLookup(selectKeyA)et à adict.OuterGet(key)mesure alookup[key]. Obtenir la remise des clés est un peu plus délicat, si: alookup.Select(x => x.Keys).
Risky Martin

1
@RiskyMartin Merci! Cela rend en effet le tout plus élégant. J'ai mis à jour la réponse et les idéone-s. (Je suppose que les performances devraient être augmentées car moins d'objets sont instanciés).
sehe

1
@Revious qui ne fonctionne que si vous savez que les clés sont uniques. Et ce n'est pas le cas commun pour / grouping /. À part ça, oui, certainement. Si vous savez que le hachage ne va pas faire glisser la perf (les conteneurs basés sur les nœuds ont en principe plus de coûts, et le hachage n'est pas gratuit et l'efficacité dépend de la fonction de hachage / de la répartition du compartiment), il sera certainement plus efficace sur le plan algorithmique. Donc, pour les petites charges je pense qu'il pourrait ne pas être plus rapide
sehe

27

Je pense qu'il y a des problèmes avec la plupart de ceux-ci, y compris la réponse acceptée, car ils ne fonctionnent pas bien avec Linq sur IQueryable soit en raison de trop d'allers-retours sur le serveur et de trop de retours de données, soit de l'exécution excessive du client.

Pour IEnumerable, je n'aime pas la réponse de Sehe ou similaire car elle utilise trop de mémoire (un simple test de 10000000 deux listes a entraîné une perte de mémoire de Linqpad sur ma machine de 32 Go).

En outre, la plupart des autres n'implémentent pas réellement une jointure complète complète appropriée car ils utilisent une union avec une jointure droite au lieu de concaténer avec une anti-semi-jointure droite, ce qui élimine non seulement les lignes de jointure interne en double du résultat, mais tous les doublons appropriés qui existaient à l'origine dans les données de gauche ou de droite.

Voici donc mes extensions qui gèrent tous ces problèmes, génèrent du SQL ainsi que l'implémentation de la jointure dans LINQ to SQL directement, s'exécutant sur le serveur, et sont plus rapides et avec moins de mémoire que d'autres sur 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));
    }
}

La différence entre une anti-semi-jointure droite est principalement théorique avec Linq to Objects ou dans la source, mais fait une différence côté serveur (SQL) dans la réponse finale, supprimant un JOIN .

Le codage manuel de Expressionpour gérer la fusion d'un Expression<Func<>>dans un lambda pourrait être amélioré avec LinqKit, mais ce serait bien si le langage / compilateur avait ajouté de l'aide pour cela. Les fonctions FullOuterJoinDistinctet RightOuterJoinsont incluses pour être complet, mais je n'ai pas réimplémentéFullOuterGroupJoin encore réimplémenté.

J'ai écrit une autre version d'une jointure externe complète pourIEnumerable pour les cas où la clé est commandable, ce qui est environ 50% plus rapide que de combiner la jointure externe gauche avec la demi-jointure anti droite, au moins sur les petites collections. Il passe par chaque collection après avoir été trié une seule fois.

J'ai également ajouté une autre réponse pour une version qui fonctionne avec EF en remplaçant le Invokepar une extension personnalisée.


Quel est le problème TP unusedP, TC unusedC? Sont-ils littéralement inutilisés?
Rudey

Oui, ils sont juste présents pour capturer les types dans TP, TC, TResultpour créer le bon Expression<Func<>>. Je croyais que je pouvais les remplacer par _, __, ___au contraire, mais cela ne semble pas plus clair jusqu'à ce que C # a un caractère générique de paramètre correct à utiliser à la place.
NetMage

1
@MarcL. Je ne suis pas sûr de «fatigant» - mais je suis d'accord que cette réponse est très utile dans ce contexte. Impressionnant choses (bien que pour moi , il confirme les lacunes de LINQ to SQL)
sehe

3
Je reçois The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Y a-t-il des restrictions avec ce code? Je veux effectuer un FULL JOIN sur IQueryables
Learner

1
J'ai ajouté une nouvelle réponse qui remplace Invokepar une coutume ExpressionVisitorpour aligner le Invokeafin qu'il fonctionne avec EF. Pouvez-vous l'essayer?
NetMage

7

Voici une méthode d'extension qui fait cela:

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), ce qui signifie une jointure externe complète = union de la jointure externe gauche toute la jointure externe droite! J'apprécie la simplicité de cette approche.
TamusJRoyce

1
@TamusJRoyce Except Unionsupprime les doublons, donc s'il y a des lignes en double dans les données d'origine, elles ne seront pas dans le résultat.
NetMage

Bon point! ajoutez un identifiant unique si vous devez empêcher la suppression des doublons. Oui. L'union est un peu inutile sauf si vous pouvez laisser entendre qu'il existe un identifiant unique et que l'union passe à l'union tout (via des heuristiques / optimisations internes). Mais ça marchera.
TamusJRoyce

Identique à la réponse acceptée .
Gert Arnold

7

Je suppose que l'approche de @ sehe est plus forte, mais jusqu'à ce que je la comprenne mieux, je me retrouve à sauter le pas sur l'extension de @ MichaelSander. Je l'ai modifié pour qu'il corresponde à la syntaxe et au type de retour de la méthode Enumerable.Join () intégrée décrite ici . J'ai ajouté le suffixe "distinct" en ce qui concerne le commentaire de @ cadrell0 sous la solution de @ 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);
    }

}

Dans l'exemple, vous l'utiliseriez comme ceci:

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

À l'avenir, au fur et à mesure que j'apprends, j'ai le sentiment que je vais migrer vers la logique de @ sehe compte tenu de sa popularité. Mais même dans ce cas, je dois faire attention, car je pense qu'il est important d'avoir au moins une surcharge qui correspond à la syntaxe de la méthode ".Join ()" existante si possible, pour deux raisons:

  1. La cohérence des méthodes permet de gagner du temps, d'éviter les erreurs et d'éviter les comportements involontaires.
  2. S'il y a une méthode prête à l'emploi ".FullJoin ()" à l'avenir, j'imagine qu'elle essaiera de respecter la syntaxe de la méthode ".Join ()" actuellement existante si elle le peut. Si c'est le cas, alors si vous souhaitez y migrer, vous pouvez simplement renommer vos fonctions sans modifier les paramètres ni vous soucier des différents types de retour cassant votre code.

Je suis encore nouveau avec les génériques, les extensions, les instructions Func et d'autres fonctionnalités, donc les commentaires sont certainement les bienvenus.

EDIT: Il ne m'a pas fallu longtemps pour réaliser qu'il y avait un problème avec mon code. Je faisais un .Dump () dans LINQPad et regardais le type de retour. C'était juste IEnumerable, alors j'ai essayé de le faire correspondre. Mais quand j'ai fait un .Where () ou .Select () sur mon extension, j'ai eu une erreur: "'System Collections.IEnumerable' ne contient pas de définition pour 'Select' et ...". Donc, à la fin, j'ai pu faire correspondre la syntaxe d'entrée de .Join (), mais pas le comportement de retour.

EDIT: Ajout de "TResult" au type de retour pour la fonction. Manqué que lors de la lecture de l'article Microsoft, et bien sûr, cela a du sens. Avec ce correctif, il semble maintenant que le comportement de retour soit conforme à mes objectifs après tout.


+2 pour cette réponse ainsi que Michael Sanders. J'ai accidentellement cliqué dessus et le vote est verrouillé. Veuillez en ajouter deux.
TamusJRoyce

@TamusJRoyce, je viens de modifier un peu les formats de code. Je pense qu'après une modification, vous avez la possibilité de refondre votre vote. Essayez-le si vous le souhaitez.
pwilcox

Merci beaucoup!
Roshna Omer

6

Comme vous l'avez trouvé, Linq n'a pas de construction de "jointure externe". Le plus proche que vous pouvez obtenir est une jointure externe gauche en utilisant la requête que vous avez indiquée. Pour cela, vous pouvez ajouter tous les éléments de la liste des noms 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)));

2

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

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 implémentation a 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 soit énumérée.
  • N'énumère les séquences d'entrée qu'une seule fois.
  • 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 à droite (pour les touches non présentes dans la séquence de gauche).

Ces propriétés sont importantes, car elles sont ce à quoi s'attendent les débutants de FullOuterJoin mais expérimentés avec LINQ.


Il ne conserve pas l'ordre des séquences d'entrée: la recherche ne garantit pas cela, donc ces foreaches énuméreront 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 conservé.
Ivan Danilov

@IvanDanilov Vous avez raison, ce n'est pas réellement dans le contrat. Cependant, l'implémentation de ToLookup utilise une classe de recherche interne dans Enumerable.cs qui conserve les regroupements dans une liste liée par ordre d'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 les versions ultérieures.
Søren Boisen

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

1
msgstr "..les séquences d 'entrée sont énormément énumérées par les appels à ToLookup". Mais votre implémentation fait exactement la même chose. Le rendement ne donne pas grand-chose ici à cause des dépenses sur la machine à états finis.
pkuderov

4
Les appels de recherche sont effectués lorsque le premier élément du résultat est demandé et non lorsque l'itérateur est créé. C'est ce que signifie une exécution différée. Vous pouvez différer encore plus l'énumération d'un ensemble d'entrées en itérant directement l'énumérateur gauche au lieu de le convertir en recherche, ce qui offre l'avantage supplémentaire que l'ordre de l'ensemble gauche est préservé.
Rolf

2

J'ai décidé d'ajouter ceci comme une réponse distincte car je ne suis pas certain qu'il soit suffisamment testé. Il s'agit d'une réimplémentation de la FullOuterJoinméthode utilisant essentiellement une version simplifiée et personnalisée de LINQKit Invoke/ Expandfor Expressionafin qu'elle fonctionne avec Entity Framework. Il n'y a pas beaucoup d'explications car c'est à peu près la même 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));
        }
    }
}

NetMage, un codage impressionnant! Lorsque je l'exécute avec un exemple simple et lorsque [NullVisitor.Visit (..) est invoqué dans [base.Visit (Node)], il lève une [System.ArgumentException: les types d'arguments ne correspondent pas]. Ce qui est vrai, car j'utilise une clé [Guid] et à un moment donné, le visiteur nul attend un type [Guid?]. Peut-être que je manque quelque chose. J'ai un court exemple codé pour EF 6.4.4. Veuillez me faire savoir comment puis-je partager ce code avec vous. Merci!
Troncho

@Troncho J'utilise normalement LINQPad pour les tests, donc EF 6 n'est pas facile à faire. base.Visit(node)ne devrait pas lever d'exception car cela revient simplement dans l'arbre. Je peux accéder à pratiquement n'importe quel service de partage de code, mais pas configurer une base de données de test. L'exécuter avec mon test LINQ to SQL semble cependant fonctionner correctement.
NetMage

@Troncho Est-il possible que vous joigniez une Guidclé à une clé Guid?étrangère?
NetMage

J'utilise également LinqPad pour les tests. Ma requête a lancé l'argumentException, j'ai donc décidé de le déboguer sur VS2019 sur [.Net Framework 4.7.1] et le dernier EF 6. Là, j'ai pu tracer le vrai problème. Afin de tester votre code, je génère 2 ensembles de données distincts provenant de la même table [Persons]. Je filtre les deux ensembles afin que certains enregistrements soient uniques à chaque ensemble et que certains existent sur les deux ensembles. [PersonId] est un [Clé primaire] Guid (c #) / Uniqueidentifier (SqlServer) et aucun des deux ensembles ne génère de valeur [PersonId] nulle. Code partagé: github.com/Troncho/EF_FullOuterJoin
Troncho

1

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

Exemple:

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

  • Nécessite que «OrderBy» soit appliqué aux énumérateurs 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
C'est un effort héroïque pour rendre les choses "en streaming". Malheureusement, tout le gain est perdu à la première étape, où vous effectuez OrderBysur les deux projections clés. OrderBytampon la séquence entière, pour les raisons évidentes .
sehe

@sehe Vous avez certainement raison pour Linq to Objects. Si les IEnumerable <T> sont IQueryable <T>, la source doit trier - pas de temps pour tester cependant. Si je me trompe, remplacer simplement l'entrée IEnumerable <T> par IQueryable <T> devrait trier dans la source / base de données.
James Caradoc-Davies

1

Ma solution propre pour la situation de cette 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()));
        }

alors

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

les sorties:

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

0

Jointure externe complète pour deux tables ou plus: commencez par extraire la colonne sur laquelle vous souhaitez vous joindre.

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

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

0

J'ai écrit cette classe d'extensions pour une application il y a peut-être 6 ans, et je l'utilise depuis dans de nombreuses solutions sans problèmes. J'espère que ça aide.

edit: j'ai remarqué que certains ne savaient pas comment utiliser une classe d'extension.

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

^ cela devrait vous permettre de voir l'intellisense des fonctions d'extension sur n'importe quelle collection d'objets IEnumerable que vous utilisez.

J'espère que cela t'aides. Faites-moi savoir si ce n'est toujours pas clair, et j'espère écrire un exemple d'exemple sur la façon de l'utiliser.

Voici maintenant 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
Malheureusement, il semble que la fonction SelectManyne puisse pas être convertie en une arborescence d'expression digne de LINQ2SQL, semble-t-il.
OR Mapper

edc65. Je sais que ce pourrait ê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 vous suffit de référencer l'espace de noms joinext.
H7O

OU Mapper, faites-moi savoir avec quel type de collection vous voulez qu'elle fonctionne. Cela devrait fonctionner
correctement

0

Je pense que la clause de jointure LINQ n'est pas la bonne solution à ce problème, car le but de la clause de jointure n'est pas d'accumuler des données de la manière requise pour cette solution de tâche. Le code pour fusionner les collections séparées créées devient trop compliqué, peut-être est-il correct à des fins d'apprentissage, mais pas pour de vraies applications. L'une des façons de résoudre ce problème est dans 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 HashSet, les boucles foreach peuvent être utilisées à la place, 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

0

Merci à tous pour les articles intéressants!

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

  • un prédicat de jointure personnalisé
  • un comparateur distinct syndical personnalisé

Pour ceux intéressés c'est 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

0

Encore une autre jointure externe complète

Comme je n'étais pas très satisfait de la simplicité et de la lisibilité des autres propositions, je me suis retrouvé avec ceci:

Il n'a pas la prétention d'être rapide (environ 800 ms pour rejoindre 1000 * 1000 sur un processeur 2020m: 2.4ghz / 2cores). Pour moi, c'est juste une jointure extérieure complète compacte et décontractée.

Il fonctionne de la même manière qu'un SQL FULL OUTER JOIN (conservation des doublons)

À votre santé ;-)

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

L'idée est de

  1. Créer des identifiants basés sur les générateurs de fonctions clés fournis
  2. Traiter les éléments laissés uniquement
  3. Processus de jointure interne
  4. Traiter uniquement les éléments de droite

Voici un test succinct qui va avec:

Placez un point d'arrêt à la fin pour vérifier manuellement qu'il se comporte comme prévu

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

Je déteste vraiment ces expressions linq, c'est pourquoi SQL existe:

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

Créez ceci en tant que vue SQL dans la base de données et importez-le en tant qu'entité.

Bien sûr, l'union (distincte) des jointures gauche et droite le fera aussi, mais c'est stupide.


11
Pourquoi ne pas simplement supprimer autant d'abstractions que possible et le faire dans le code machine? (Astuce: parce que les abstractions d'ordre supérieur facilitent la vie du programmeur). Cela ne répond pas à la question et ressemble plus à une diatribe contre LINQ.
dépensier

8
Qui a dit que les données provenaient d'une base de données?
user247702

1
Bien sûr, c'est une base de données, il y a des mots "jointure externe" en question :) google.cz/search?q=outer+join
Milan Švec

1
Je comprends qu'il s'agit d'une solution «à l'ancienne», mais avant de voter, comparez sa complexité avec d'autres solutions :) Sauf celle acceptée, c'est bien sûr la bonne.
Milan Švec

Bien sûr, cela peut être une base de données ou non. Je recherche une solution avec une jointure externe entre les listes en mémoire
edc65
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.