Как выполнить полное внешнее соединение в Linq?

Я унаследовал базу данных, которая не была спроектирована точно оптимально, и мне нужно манипулировать некоторыми данными. Позвольте мне привести более общую аналогию того, что я должен делать:

Допустим, у нас есть таблица Student, таблица StudentClass, в которой хранятся записи всех классов, которые он посещал, и таблица StudentTeacher, в которой хранятся все учителя, которые учили этого ученика. Да, я знаю, что это глупый дизайн, и было бы разумнее разместить учителя на столе класса - но это то, с чем мы работаем.

Теперь я хочу очистить данные, и я хочу найти все места, где у ученика есть учитель, но нет классов, или класс, но нет учителей. SQL таким образом:

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

Как это сделать в Linq?


person Shaul Behr    schedule 18.01.2010    source источник
comment
Примечание: на самом деле это не полное внешнее соединение, поскольку вы хотите исключить строки, в которых внутреннее соединение выполнено успешно. Я просто упоминаю об этом, так как это лучший результат поиска для 'full external join linq' - так что, если это то, что кто-то ищет, тогда ответы могут быть неправильными.   -  person Simon_Weaver    schedule 30.10.2011


Ответы (5)


Думаю, у меня есть ответ, который не так элегантен, как я надеялся, но он должен помочь:

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

Возможно, вы могли бы сжать эти два оператора в один, но я думаю, что вы пожертвуете ясностью кода.

person Shaul Behr    schedule 19.01.2010
comment
Set Unions автоматически делает вещи отличными en.wikipedia.org/wiki/Union_(set_theory) - person Visionary Software Solutions; 03.11.2010

Способ расширения:

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]
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"} );
}
person andrey.tsykunov    schedule 12.01.2011
comment
+1 за концепцию метода расширения! У меня есть ощущение, что его можно оптимизировать изнутри, но, тем не менее, это хороший ответ. - person Shaul Behr; 12.01.2011

для данных 2 коллекций a и b необходимое полное внешнее соединение может быть следующим:

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

Если a и b не одного типа, то требуются 2 отдельных левых внешних соединения:

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;

вот однострочный вариант с использованием 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});
person Boris Lipschitz    schedule 19.01.2010
comment
Это хорошее семантическое изложение проблемы, но оно не помогает, потому что для того, чтобы это работало в Linq, a и b должны быть одного типа, что здесь не так. - person Shaul Behr; 19.01.2010
comment
Это не правильно. Правильный оператор внешнего соединения доступен здесь: msdn.microsoft.com/en- us / library / vstudio / bb397895.aspx. Проверка на нуль не требуется, и для выбора из группы необходимо ввести другую переменную. - person grzegorz_p; 05.02.2013
comment
Пример @grzegorz_p msdn показывает ЛЕВОЕ внешнее соединение. Вопрос был о ПОЛНОМ внешнем соединении - person Boris Lipschitz; 06.02.2013
comment
А что насчет a.Except(b).Concat(b.Except(a))? - person tigrou; 13.08.2013

Начало...

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

См. Также дополнительные примеры на http://www.linqpad.net/ Хороший инструмент для игры

person salgo60    schedule 18.01.2010
comment
Креативный, но не такой элегантный, как я надеялся. Я дам вам +1 за ссылку на LinqPad, который выглядит довольно крутой программой. :) - person Shaul Behr; 19.01.2010
comment
;-)) У вас есть более элегантные примеры в LinqPad. У него отличное соединение с базой данных, и у вас есть возможность добавить ссылку в свою dll: s и т.д ... Автор также написал лучшую книгу C # в двух словах youtube.com/watch?v=Z6-iUNfJsJw&feature=channel - person salgo60; 20.01.2010
comment
Два момента на заметку: 1) Это создает LEFT OUTER JOIN, а не полное внешнее соединение, и 2) проверка st == null не требуется в linq-to-sql, вместо этого вы можете просто выполнить st.StudentID ?? (нет StudentTeacher) - person Marty Neal; 21.02.2012
comment
Это не приведет к созданию полного внешнего соединения, как объяснил @Martin - person Shyamal Parikh; 19.12.2015

На основе ответа Шауля, но с небольшой оптимизацией:

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

Обратите внимание, что для полного внешнего соединения это тоже может сработать. Оставьте предложение where и используйте первое select выше, а не второе.

person sq33G    schedule 30.06.2013