如何在Linq中完成一个完整的外连接?

我已经inheritance了一个没有完全devise的数据库,我需要操纵一些数据。 让我举一个比较常见的比喻来说明我所要做的事:

比方说,我们有一个Student表,一个StudentClass表,logging他所有的class级logging,还有一个StudentTeacher表,存储所有教这个学生的老师。 是的,我知道这是一个愚蠢的devise,将老师存放在class级表上会更有意义 – 但这就是我们正在处理的事情。

我现在想清理这些数据,我想find一个学生有一个老师,但没有class级,或一个class级,但没有老师的地方。 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怎么做?

我想我在这里得到了答案,这并不像我希望的那样优雅,但它应该做到这一点:

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

你可以将这两个语句合并为一个,但是我认为你会牺牲代码的清晰度。

对于给定的2个集合ab ,所需的完整外连接可能如下:

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

如果a和b不是相同的types,则需要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}); 

扩展方法:

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

testing:

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

开始…

  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/更多示例好玩的工具;

根据Shaul的回答,但稍微精简一点:

 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 ,而不是第二个。