【问题标题】:LINQ Method Syntax with INNER and OUTER Join带有 INNER 和 OUTER 连接的 LINQ 方法语法
【发布时间】:2018-07-19 13:11:25
【问题描述】:

我有 3 个课程并尝试使用 LINQ methods 来执行 INNER JOINLEFT JOIN。我可以分别执行每个,但没有运气在一起,因为我什至无法弄清楚语法。

最终,我要编写的 SQL 将是:

SELECT *
FROM [Group] AS [g]
INNER JOIN [Section] AS [s] ON [s].[GroupId] = [g].[Id]
LEFT OUTER JOIN [Course] AS [c] ON [c].[SectionId] = [s].[Id]

public class Group {
    public int Id { get; set; }
    public int Name { get; set; }
    public bool IsActive { get; set; }
    public ICollection<Section> Sections { get; set; }
}

public class Section {
    public int Id { get; set; }
    public int Name { get; set; }
    public int GroupId { get; set; }
    public Group Group { get; set; }
    public bool IsActive { get; set; }
    public ICollection<Course> Courses { get; set; }
}

public class Course {
    public int Id { get; set; }
    public int UserId { get; set; }
    public int Name { get; set; }
    public int SectionId { get; set; }
    public bool IsActive { get; set; }
}

样本

我希望结果为Group 类型。我成功地在SectionCourse 之间执行了LEFT JOIN,但是我有一个IQueryable&lt;a>, which is not what I want, sinceGroup` 类型的对象。

var result = db.Section
               .GroupJoin(db.Course, 
                    s => s.Id,
                    c => c.SectionId,
                    (s, c) => new { s, c = c.DefaultIfEmpty() })
               .SelectMany(s => s.c.Select(c => new { s = s.s, c }));

我也试过这个,但返回NULL,因为这会在所有表上执行INNER JOIN,并且用户没有输入任何Courses

var result = db.Groups
               .Where(g => g.IsActive)
               .Include(g => g.Sections)
               .Include(g => g.Sections.Select(s => s.Courses))
               .Where(g => g.Sections.Any(s => s.IsActive && s.Courses.Any(c => c.UserId == _userId && c.IsActive)))
               .ToList();

问题

如何以最少的数据库调用次数执行INNERLEFT JOIN 并获得Group 类型的结果?

期望的结果

我想要 1 个 Group 类型的对象,但前提是 Group 具有 Section。我还想返回用户对特定SectionCourses 或返回NULL

【问题讨论】:

    标签: c# linq lambda linq-method-syntax


    【解决方案1】:

    我认为如果不返回一个新的(匿名)对象而不是 Group(如 this answer 所示),您所要求的是不可能的。由于关系和实体缓存的工作方式,EF 将不允许您在 Section 中获取过滤后的 Course 集合,这意味着您不能为此任务使用导航属性。

    首先,您希望控制加载哪些相关实体,因此我建议通过在您的实体中将 SectionsCourses 集合属性标记为 virtual 来启用延迟加载(除非您已经为应用程序中的所有实体启用延迟加载),因为我们不希望 EF 加载相关的 SectionsCourses,因为无论如何它都会为每个用户加载所有课程。

    public class Group {
        public int Id { get; set; }
        public int Name { get; set; }
        public bool IsActive { get; set; }
        public virtual ICollection<Section> Sections { get; set; }
    }
    
    public class Section {
        public int Id { get; set; }
        public int Name { get; set; }
        public int GroupId { get; set; }
        public Group Group { get; set; }
        public bool IsActive { get; set; }
        public virtual ICollection<Course> Courses { get; set; }
    }
    

    在方法语法中,查询可能看起来像这样:

    var results = db.Group
        .Where(g => g.IsActive)
        .GroupJoin(
            db.Section.Where(s => s.IsActive),
            g => g.Id,
            s => s.GroupId,
            (g, s) => new
            {
                Group = g,
                UserSections = s
                    .GroupJoin(
                        db.Course.Where(c => c.IsActive && c.UserId == _userId).DefaultIfEmpty(),
                        ss => ss.Id,
                        cc => cc.SectionId,
                        (ss, cc) => new
                        {
                            Section = ss,
                            UserCourses = cc
                        }
                    )
            })
        .ToList();
    

    你会将结果消费为:

    foreach (var result in results)
    {
        var group = result.Group;
    
        foreach (var userSection in result.UserSections)
        {
            var section = userSection.Section;
    
            var userCourses = userSection.UserCourses;
    
        }
    }
    

    现在,如果您不需要在数据库级别对组结果进行额外过滤,您也可以使用此 LINQ 查询采用 INNER JOIN 和 LEFT OUTER JOIN 方法并在内存中进行分组:

    var results = db.Group
        .Where(g => g.IsActive)
        .Join(
            db.Section.Where(s => s.IsActive),
            g => g.Id,
            s => s.GroupId,
            (g, s) => new
            {
                Group = g,
                UserSection = new
                {
                    Section = s,
                    UserCourses = db.Course.Where(c => c.IsActive && c.UserId == _userId && c.SectionId == s.Id).DefaultIfEmpty()
                }
            })
        .ToList() // Data gets fetched from database at this point
        .GroupBy(x => x.Group) // In-memory grouping
        .Select(x => new
        {
            Group = x.Key,
            UserSections = x.Select(us => new
            {
                Section = us.UserSection,
                UserCourses = us.UserSection.UserCourses
            })
        });
    

    请记住,每当您尝试访问 group.Sectionssection.Courses 时,都会触发延迟加载,这将获取所有子部分或课程,而不管 _userId 是什么。

    【讨论】:

    • 谢谢!看起来像一个有希望的答案。我会努力的,让你知道结果。我实际上是延迟加载(在我的所有属性上标记virtual),这就是为什么我的初始示例代码使用.Include(...) 立即获取它们。这会影响你的回答吗?
    • 只是想确保您启用了延迟加载。此外,如果您只需要读取权限,请使用 db.Group.AsNoTracking() 而不是 db.Group
    • o.0 谢谢!不知道.AsNoTracking()。我假设这可以节省一些服务器资源?或者它还有什么用处?
    • 它大大提高了查询速度,因为它告诉 EF 不要跟踪返回的实体的更改。
    • 谢谢。我还没有测试它。我会尽快测试并告诉你结果。
    【解决方案2】:

    使用DefaultIfEmpty 执行外左连接

    from g in db.group
    join s in db.section on g.Id equals s.GroupId 
    join c in db.course on c.SectionId equals s.Id into courseGroup
    from cg in courseGroup.DefaultIfEmpty()
    select new { g, s, c }; 
    

    【讨论】:

    • 有没有办法用LINQ Methods而不是LINQ Query来做到这一点?
    • 这是真的,但我喜欢学习不同的方法并想要一个LINQ Methods 版本。 =) 我想我应该说我已经可以用LINQ Query 做到这一点 =)
    • 据我了解,c 不能用于selectcg 应该使用:select new { g, s, cg };
    【解决方案3】:

    您的 SQL 的类型不是 [Group](类型组将是:select [Group].* from ...),无论如何,如果您想要这样,那么它的简单形式将是:

    var result = db.Groups.Where( g => g.Sections.Any() );
    

    但是,如果你真的想转换你的 SQL,那么:

    var result = from g in db.Groups
                 from s in g.Sections
                 from c in s.Courses.DefaultIfEmpty()
                 select new {...};
    

    即使这样也可以:

    var result = from g in db.Groups
                 select new {...};
    

    提示:在设计良好的关系数据库中,您很少需要使用 join 关键字。而是使用导航属性。

    【讨论】:

    • 谢谢。知道了。这是非常好的设计与适当的关系 IMO,但是我需要这个的原因是因为我想要所有活动的Sections,但是学生可能没有Courses 在所有Sections。也许我对 LINQ 的了解不是 =),我可能做错了什么。要去测试一些东西。
    • @RoLYroLLs,好的。我建议您下载并使用精彩的 LinqPad (linqPad.net) 工具。当你向它添加连接时,它会自动扣除模型并帮助你编写复杂的 Linq。
    • 是的,我想过。在我进一步讨论之前,让我问你一个附带问题:如果我写类似var result = db.Groups.Include(g =&gt; g.Sections) 的东西,它会返回尚未分配SectionsGroups,还是SectionsNULL
    • @RoLYroLLs,是的。 Linqpad 是测试这个想法的最快方法(并获得 SQL、lambda 调用:)重新阅读您的问题不确定我是否理解正确,最好自己测试一下。
    • 啊!那我有一个完全不同的问题要问。我认为.Include(...) 就像INNER JOIN,如果没有Section,它就不会返回任何Groups。哦!我会做一些测试并发布一个新问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-15
    • 1970-01-01
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    • 2017-09-22
    相关资源
    最近更新 更多