36 votes

Comment faire une jointure externe complète dans Linq?

J'ai hérité d'une base de données qui n'a pas été conçu exactement de manière optimale, et j'ai besoin de manipuler des données. Permettez-moi de donner un plus commun analogie avec le genre de chose que j'ai à faire:

Disons que nous avons un Student tableau, un StudentClass tableau de garder trace de toutes les classes, il a assisté, et une StudentTeacher tableau qui stocke tous les professeurs qui ont enseigné cette élève. Oui, je sais que c'est stupide de conception, et il serait plus logique de les stocker à l'enseignant de la Classe de la table, mais c'est ce à quoi nous travaillons avec.

Je veux maintenant nettoyer les données, et je veux trouver tous les endroits où un élève a un professeur, mais pas de classes, ou une classe, mais pas d'enseignants. SQL ainsi:

select *
from StudentClass sc
full outer join StudentTeacher st on st.StudentID = sc.StudentID
where st.id is null or sc.id is null

Comment faites-vous cela dans Linq?

28voto

Shaul Points 8267

Je pense avoir la réponse ici, qui n'est pas aussi élégante que je l'espérais, mais cela devrait faire l'affaire:

 var studentIDs = StudentClasses.Select(sc => sc.StudentID)
  .Union(StudentTeachers.Select(st => st.StudentID);
  //.Distinct(); -- Distinct not necessary after Union
var q =
  from id in studentIDs
  join sc in StudentClasses on id equals sc.StudentID into jsc
  from sc in jsc.DefaultIfEmpty()
  join st in StudentTeachers on id equals st.StudentID into jst
  from st in jst.DefaultIfEmpty()
  where st == null ^ sc == null
  select new { sc, st };
 

Vous pourriez probablement comprimer ces deux déclarations en une seule, mais je pense que vous sacrifieriez la clarté du code.

18voto

Boris Lipschitz Points 1434

pour les 2 collections a et b données , une jointure externe complète requise peut être la suivante:

 a.Union(b).Except(a.Intersect(b));
 

Si a et b ne sont pas du même type, alors 2 jointures externes gauches distinctes sont requises:

 var studentsWithoutTeachers =
    from sc in studentClasses
    join st in studentTeachers on sc.StudentId equals st.StudentId into g
    from st in g.DefaultIfEmpty()
    where st == null
    select sc;
var teachersWithoutStudents =
    from st in studentTeachers
    join sc in studentClasses on st.StudentId equals sc.StudentId into g
    from sc in g.DefaultIfEmpty()
    where sc == null
    select st;
 

voici une option d'une ligne utilisant Concat ():

 (from l in left
 join r in right on l.Id equals r.Id into g
 from r in g.DefaultIfEmpty()
 where r == null
 select new {l, r})
     .Concat(
     from r in right
     join sc in left on r.Id equals sc.Id into g
     from l in g.DefaultIfEmpty()
     where l == null
     select new {l, r});
 

18voto

andrey.tsykunov Points 1266

Méthode d'extension:

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

Tester:

 [Test]
public void CanDoFullOuterJoin()
{
    var list1 = new[] {"A", "B"};
    var list2 = new[] { "B", "C" };

    list1.FullOuterJoin(list2, x => x, x => x, (x1, x2) => (x1 ?? "") + (x2 ?? ""))
         .ShouldCollectionEqual(new [] { "A", "BB", "C"} );
}
 

1voto

salgo60 Points 879

Un début...

  var q = from sc in StudentClass
            join st in StudentTeachers on sc.StudentID equals st.StudentID into g
            from st in g.DefaultIfEmpty()
            select new {StudentID = sc.StudentID, StudentIDParent = st == null ? "(no StudentTeacher)" : st.StudentID...........};
 

Voir aussi http://www.linqpad.net/ pour plus d'exemples Bon outil pour jouer avec

1voto

sq33G Points 2247

Basé sur la réponse de Shaul, mais avec un peu de rationalisation:

 var q =
  from id in studentIDs
  join sc in StudentClasses on id equals sc.StudentID into jsc
  join st in StudentTeachers on id equals st.StudentID into jst
  where jst.Any() ^ jsc.Any() //exclusive OR, so one must be empty

  //this will return the group with the student's teachers, and an empty group
  //   for the student's classes - 
  //   or group of classes, and empty group of teachers
  select new { classes = jsc, teachers = jst };

  //or, if you know that the non-empty group will always have only one element:
  select new { class = jsc.DefaultIfEmpty(), teacher = jst.DefaultIfEmpty() };
 

Notez que pour une jointure externe complète, cela peut également fonctionner. Supprimez la clause where et utilisez le premier select ci-dessus, plutôt que le second.

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