web-dev-qa-db-fra.com

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çue de manière optimale et j'ai besoin de manipuler certaines données. Permettez-moi de donner une analogie plus courante du genre de chose que je dois faire:

Supposons que nous ayons une table Student, une table StudentClass contenant toutes les classes auxquelles il a assisté et une table StudentTeacher qui stocke tous les enseignants qui ont enseigné à cet élève. Oui, je sais que c'est une conception stupide et il serait plus logique de stocker l'enseignant sur la table de classe - mais c'est avec cela que nous travaillons.

Je veux maintenant nettoyer les données, et je veux trouver tous les endroits où un étudiant a un professeur mais pas de classe, ou une classe mais pas de professeur. SQL donc:

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 à Linq?

36
Shaul Behr

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.

28
Shaul Behr

pour les 2 collections données a et b , une jointure externe complète requise pourrait être comme suit:

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});
18
Boris Lipschitz

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"} );
}
18
andrey.tsykunov

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.

1
sq33G

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

1
salgo60