LINQ查询以包含子表

时间:2016-06-15 12:36:25

标签: c# .net entity-framework linq

我正在尝试研究如何使用LINQ查询返回主实体的详细信息,以及来自1对多(或0..1对多)子项的一行的详细信息实体。

这里的细节只是一个简化的例子。这更像是我正在寻找的概念。实体框架类可以定义为:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Department { get; set; }

    public ICollection<Login> Logins { get; set; }
}

public class Login
{
    public int Id { get; set; }
    public DateTime LoginTimestamp { get; set; }
    public string IpAddress { get; set; }
    public string UserAgent { get; set; }

    public virtual User User { get; set; }
}

所以开始查询,我想要像:

var results = from u in Users
    select new {u.Id, u.Name, u.Email, u.Department}

到目前为止一切顺利。假设我想添加上次登录日期和时间:

var results = from u in Users
    select new {u.Id, u.Name, u.Email, u.Department, u.Logins.Max(LoginTimestamp)}

仍然很好。但现在我想从最新的登录记录中添加更多细节。假设我也想包括上次登录时的IP地址。我知道查询将不得不大幅改变并使用除“Max”之外的其他内容,但我不确定如何。加入?子查询?

我看过这样做的提法:

var results = from u in Users
    select new {u.Id, u.Name, u.Email, u.Department, 
        u.Logins.Max(LoginTimestamp), 
        u.Logins.OrderByDescending(LoginTimestamp).FirstOrDefault().IPAddress}

...但我怀疑效率非常低,并且对于从子实体添加的每个其他列越来越多。

最好/最有效的方法是什么?

为了清楚起见,我只希望每个用户在结果集中有一行,只包含上次登录的详细信息。

由于

2016年6月16日编辑:

我已经构建了这个示例,并使用LINQPad来测试查询。

对于此LINQ查询:

from u in Users
    select new {u.Id, u.Name, u.Email, u.Department, 
        LastLogin = u.Logins.OrderByDescending(l => l.LoginTimestamp).FirstOrDefault().LoginTimestamp, 
        LastIP = u.Logins.OrderByDescending(l => l.LoginTimestamp).FirstOrDefault().IpAddress}

...它产生的SQL是......

SELECT 
    [Project2].[Id] AS [Id], 
    [Project2].[Name] AS [Name], 
    [Project2].[Email] AS [Email], 
    [Project2].[Department] AS [Department], 
    [Project2].[C1] AS [C1], 
    [Limit2].[IpAddress] AS [IpAddress]
    FROM   (SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[Email] AS [Email], 
        [Extent1].[Department] AS [Department], 
        (SELECT TOP (1) [Project1].[LoginTimestamp] AS [LoginTimestamp]
            FROM ( SELECT 
                [Extent2].[LoginTimestamp] AS [LoginTimestamp]
                FROM [dbo].[Logins] AS [Extent2]
                WHERE [Extent1].[Id] = [Extent2].[User_Id]
            )  AS [Project1]
            ORDER BY [Project1].[LoginTimestamp] DESC) AS [C1]
        FROM [dbo].[Users] AS [Extent1] ) AS [Project2]
    OUTER APPLY  (SELECT TOP (1) [Project3].[IpAddress] AS [IpAddress]
        FROM ( SELECT 
            [Extent3].[LoginTimestamp] AS [LoginTimestamp], 
            [Extent3].[IpAddress] AS [IpAddress]
            FROM [dbo].[Logins] AS [Extent3]
            WHERE [Project2].[Id] = [Extent3].[User_Id]
        )  AS [Project3]
        ORDER BY [Project3].[LoginTimestamp] DESC ) AS [Limit2]

...这不是特别有效,它只会变得更糟,因为它似乎为(相同的)子实体的每个字段输出添加一个子查询。

如果我在SQL中这样做,我会使用:

SELECT Users.Id, Users.Name, Users.Email, Users.Department, Logins.LoginTimestamp, Logins.IpAddress, Logins.UserAgent
FROM (SELECT MAX(LoginTimestamp) AS LastLoginTimestamp, User_Id
          FROM Logins AS Logins_1
          GROUP BY User_Id) AS LastLogins INNER JOIN
      Logins ON LastLogins.LastLoginTimestamp = Logins.LoginTimestamp AND LastLogins.User_Id = Logins.User_Id INNER JOIN
      Users ON Logins.User_Id = Users.Id

一个子查询,您可以根据需要从子表中引入尽可能多的字段。

所以我想我的问题是如何在LINQ中复制这个SQL查询?

或者,如果通过LINQ有更有效的方法,我很乐意看到它。

任何帮助表示赞赏! 感谢

4 个答案:

答案 0 :(得分:2)

您可以使用let

来避免使用双子查询
var result = from u in Users
             let lastLogin = u.Logins.OrderByDescending(l => l.LoginTimestamp).FirstOrDefault()
             select new {u.Id, u.Name, u.Email, u.Department, 
                    LastLogin = lastLogin.LoginTimestamp,
                    LastIP = lastLogin.IpAddress};

答案 1 :(得分:1)

试试这个

var results = from u in Users
    select new {u.Id, u.Name, u.Email, u.Department,     
    Logins.Where(l=>l.User.Id==u.Id).LastOrDefault()}

我猜登录ID是自动递增。如果是这样,LastOrDefault()将返回用户的最后一次登录

答案 2 :(得分:0)

为我自己的问题提供另外两个解决方案......

两种反向视角都是因为查询从登录的上下文中解决了问题,而不是之前的答案所做的用户而不是用户。 它们非常相似,但彼此之间略有不同。

方法1:

from l in Logins
// where clause here
group l by l.User.Id into grp
let LastLogin = grp.Max(ul => ul.LoginTimestamp)
from l in grp
where l.LoginTimestamp == LastLogin
select new { l.User.Id, l.User.Name, l.User.Email, l.User.Department, LastLogin }

方法2:

from l in Logins
// where clause here
group l by l.User.Id into grp
let ul = grp.OrderByDescending(ul => ul.LoginTimestamp).FirstOrDefault()
select new { ul.User.Id, ul.User.Name, ul.User.Email, ul.User.Department, LastLogin = ul.LoginTimestamp }

方法1生成的SQL看起来比方法2更清晰,更优化,但仍然不能完全匹配前两个答案。出于这个原因,我仍然更喜欢Jamiec的答案。

无论如何,这两种选择可能对某人有用。

答案 3 :(得分:-1)

通过为每个Entity Framework查询调用ToString(),您可以在每种情况下获取生成的SQL。你可以检查它的效率。

如果您错误地使用Enumerable扩展而不是EF和Queryable扩展(转换为单个SQL查询),您会看到这一点。