Как сделать полное внешнее объединение в Linq? - PullRequest
36 голосов
/ 18 января 2010

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

Допустим, у нас есть таблица 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?

Ответы [ 5 ]

28 голосов
/ 19 января 2010

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

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

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

18 голосов
/ 12 января 2011

Метод расширения:

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"} );
}
18 голосов
/ 19 января 2010

для данных 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});
1 голос
/ 30 июня 2013

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

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 выше, а не второе.

1 голос
/ 18 января 2010

Начало ...

 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/ для большего количества образцов Хороший инструмент для игры с

...