【问题标题】:How to do a full outer join in Linq?如何在 Linq 中进行完全外部联接?
【发布时间】:2011-01-06 07:47:40
【问题描述】:

我继承了一个设计不完全优化的数据库,我需要处理一些数据。让我对我必须做的事情做一个更常见的类比:

假设我们有一个 Student 表,一个 StudentClass 表记录了他参加的所有课程,还有一个 StudentTeacher 表存储了教过这个学生的所有老师。是的,我知道这是一个愚蠢的设计,将教师存储在 Class table 上会更有意义 - 但这就是我们正在使用的。

我现在想清理数据,我想找到一个学生有老师但没有班级,或有班级但没有老师的所有地方。 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 中做到这一点?

【问题讨论】:

  • 注意:这并不是真正的完全外连接——因为您想排除内连接成功的行。我只是提到这一点,因为这是“完全外部连接 linq”的热门搜索结果 - 所以如果这是某人正在寻找的内容,那么答案可能不正确

标签: c# linq linq-to-sql outer-join full-outer-join


【解决方案1】:

开始...

 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/ 很好玩的工具

【讨论】:

  • 创意,但没有我希望的那么优雅。我会给你 +1 链接到 LinqPad,它看起来是一个非常酷的软件。 :)
  • ;-)) 你在 LinqPad 中有更多优雅的例子 它有一个很酷的数据库连接,你有可能链接到你的 dll:s 等...作者还写了最好的书 C#简而言之youtube.com/watch?v=Z6-iUNfJsJw&feature=channel
  • 需要注意的两点:1)这会生成一个 LEFT OUTER JOIN 而不是一个完整的外连接 2)在 linq-to-sql 中不需要 st == null 检查,而是您可以这样做st.StudentID ?? “(没有 StudentTeacher)”
  • 这不会像@Martin 解释的那样创建完整的外部联接
【解决方案2】:

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

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

【讨论】:

  • 这是一个很好的问题语义陈述,但它没有帮助,因为要在 Linq 中工作,a 和 b 必须是相同的类型,而这里不是这种情况。
  • 这是错误的。此处提供了正确的外部连接语句:msdn.microsoft.com/en-us/library/vstudio/bb397895.aspx。不需要空检查,必须引入另一个变量才能从组中选择
  • @grzegorz_p msdn 示例显示左外连接。问题是关于 FULL 外连接
  • a.Except(b).Concat(b.Except(a)) 呢?
【解决方案3】:

我想我在这里有了答案,虽然没有我希望的那么优雅,但它应该可以解决问题:

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

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

【讨论】:

【解决方案4】:

扩展方法:

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

【讨论】:

  • +1 为扩展方法的概念!我觉得它可以在内部进行优化,但仍然是一个很好的答案。
【解决方案5】:

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

【讨论】:

    猜你喜欢
    • 2011-07-26
    • 1970-01-01
    • 2014-03-10
    • 2021-11-17
    • 1970-01-01
    • 2017-08-04
    相关资源
    最近更新 更多