Jak zrobić pełne złączenie zewnętrzne w Linq?

Odziedziczyłem bazę danych, która nie została zaprojektowana optymalnie i muszę manipulować niektórymi danymi. Pozwól, że podam bardziej powszechną analogię tego rodzaju rzeczy, które muszę zrobić:

Powiedzmy, że mamy Student tabelę, StudentClass tabelę rejestrującą wszystkie zajęcia, w których brał udział, i StudentTeacher tabelę, która przechowuje wszystkich nauczycieli, którzy uczyli tego ucznia. Tak, Wiem, że to głupi projekt i bardziej sensowne byłoby przechowywanie nauczyciela na stole klasowym - ale to właśnie pracujemy z.

Chcę teraz wyczyścić dane i znaleźć wszystkie miejsca, w których uczeń ma nauczyciela, ale nie ma klas, lub klasę, ale nie ma nauczycieli. SQL w ten sposób:
select *
from StudentClass sc
full outer join StudentTeacher st on st.StudentID = sc.StudentID
where st.id is null or sc.id is null
Jak to się robi w Linq?
Author: Shaul Behr, 2010-01-18

5 answers

Myślę, że mam tutaj odpowiedź, która nie jest tak elegancka, jak się spodziewałem, ale powinna zadziałać:

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 };
Prawdopodobnie mógłbyś wycisnąć te dwa stwierdzenia w jedno, ale myślę, że poświęciłbyś przejrzystość kodu.
 28
Author: Shaul Behr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-17 14:51:58

Dla podanych 2 zbiorów a i b , wymagane pełne połączenie zewnętrzne może być następujące:

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

Jeśli a i b nie są tego samego typu, to wymagane są 2 oddzielnelewe zewnętrzne połączenia :

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;

Oto jednolinijkowa opcja używająca 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
Author: Boris Lipschitz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-20 03:56:01

Metoda rozszerzenia:

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

Test:

[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
Author: andrey.tsykunov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-12 06:10:39

Początek...

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

Zobacz też http://www.linqpad.net / więcej próbek Dobre narzędzie do zabawy

 1
Author: salgo60,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-19 07:02:08

Na podstawie odpowiedzi Shaula, ale z małym usprawnieniem:

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

Zauważ, że dla pełnego połączenia zewnętrznego może to również zadziałać. Pomiń klauzulę where i użyj pierwszej select powyżej, a nie drugiej.

 1
Author: sq33G,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-30 11:27:43