开发者

Filtering by max and grouping by id with joins to other entities in LINQ to Entity Framework (C#)

The following snippet does work for what I need. I believe though that there must be a better practice? A more optimal way of doing this query?

What is needed is to get a list of employee objects that are the direct reports for employee/mgr x. The direct reports are listed in a history table that has multiple records for each employee, and so only one (the most recent) record should be returned from that table per each direct report (employee) and then the Employee table should be used to get the employee object where employee id is equal to employee id from each history record in this filtered resultset. I can get both halves with two separate LINQ to EF queries.

A problem occurs when trying to join on the employeeHistory object from the first result set. According to MSDN: Referencing Non-Scalar Closures is Not Supported [Referencing a non-scalar closure, such as an entity, in a query is not supported. When such a query executes, a NotSupportedException exception is thrown with a message that states "Unable to create a constant value of type 'Closure type'. Only primitive types ('such as Int32, String, and Guid') are supported in this context."]

So I run two queries and make the first a list of type int rather than a complex object. This does work, but seems contrived. Any suggestions as to a better way (I would like to do one query).

private List<BO.Employee> ListDirectReports(int mgrId)
{
    IQueryable<BO.Employee> directRpts;
    using(var ctx = new Entities())
    {
        //to get a list of direct rpts we perform two separate queries. linq to ef with linq to objects
        //first one gets a list of emp ids for a direct mgr emp id from the history table
        //this first qry uses grouping and a filter by empid and a filter by max(date)
        //the second qry joins to the resultset from the first and goes to the employee table 
        //to get whole employee objects for everyone in the int emp id list from qry #1

        //qry #1: just a list of integers (emp ids for those reporting to emp id of mgrId)
        IEnumerable<int> directRptIDList = 开发者_如何学运维
            from employeeHistory in ctx.EmployeeHistory
            .Where(h => h.DirectManagerEmployeeID == mgrId).ToList()
                group employeeHistory by employeeHistory.EmployeeID into grp 
                    let maxDt = grp.Max(g => g.DateLastUpdated) from history in grp
                    where history.DateLastUpdated == maxDt
                    select history.EmployeeID;

        //qry #2: a list of Employee objects from the Employee entity. filtered by results from qry #1:
        directRpts = from emp in ctx.Employee
            join directRptHist in directRptIDList.ToList()
            on emp.EmployeeID equals directRptHist
            select emp;
    }
    return directRpts.ToList();
}

Thank you.


2 things I can think of to improve your queries:

ToList is non-deffered. Calling it on your Queryable collections is causing lots of extra trips to the DB. I also believe this call, along with the explicit declaration of IEnumerable<int>, was causing the closure error.

Use the relation between EmployeeHistory and Employee, in your ObjectContex, to join the queries. This will let the Framework produce more efficient SQL. And when directRpts is evaluated on your ToList call, it should only make 1 trip to the DB.

Let me know if this helps.

private List<BO.Employee> ListDirectReports(int mgrId)
{
    using(var ctx = new Entities())
    {
        var directRptIDList = 
            from employeeHistory in ctx.EmployeeHistory
                                       .Where(h => h.DirectManagerEmployeeID == mgrId)
            group employeeHistory by employeeHistory.EmployeeID into grp 
            let maxDt = grp.Max(g => g.DateLastUpdated) from history in grp
            where history.DateLastUpdated == maxDt
            select history;

        var directRpts = 
            from emp in ctx.Employee
            join directRptHist in directRptIDList
            on emp equals directRptHist.Employee
            select emp;
    }
    return directRpts.ToList();
}


There are a number of issues here, not the least of which is that by doing your Where before you get the most recent history item, you're getting records that are no longer valid. Here's how I'd do it:

private List<BO.Employee> ListDirectReports(int mgrId)
{
    using(var ctx = new Entities())
    {
        // First make sure we're only looking at the current employee information
        var currentEntries = 
            from eh in ctx.EmployeeHistory
            group employeeHistory by employeeHistory.EmployeeID into grp 
            select grp.OrderBy(eh => eh.DateLastUpdated).FirstOrDefault();
        // Now filter by the manager's ID
        var directRpts = currentEntries
            .Where(eh => eh.DirectManagerEmployeeID == mgrId);

        // This would be ideal, assuming your entity associations are set up right
        var employees = directRpts.Select(eh => eh.Employee).Distinct();

        // If the above won't work, this is the next-best thing
        var employees2 = ctx.Employee.Where(
                            emp => directRpts.Any(
                                eh => eh.EmployeeId == emp.EmployeeId));

        return employees.ToList();
    }
}


Thank you Sorax. The code I had posted did not error and did give me the results I needed, but as you pointed out, merging the two queries errored when including the ToList() method. Using your tip I merged both successfully (tested it) and have posted the improved single query method below. StriplingWarrior I tried yours as well, maybe I could massage it even more. Function evaluation times out on the first query, so I will stick with Sorax' suggestion for now. I appreciate the help and will revisit this.

    private static List<BO.Employee> ListDirectReports(int mgrId)
    {
        IQueryable<BO.Employee> directRpts;
        using(var ctx = new Entities())
        {
            directRpts = 
                from emp in ctx.Employee
                join directRptHist in 
                (from employeeHistory in ctx.EmployeeHistory
                    .Where(h => h.DirectManagerEmployeeID == mgrId)
                group employeeHistory by employeeHistory.EmployeeID into grp 
                let maxDt = grp.Max(g => g.DateLastUpdated) from history in grp
                where history.DateLastUpdated == maxDt
                select history)
                on emp equals directRptHist.Employee
                select emp;
        }
        return directRpts.ToList();

        //IQueryable<BO.Employee> employees;
        //using(var ctx = new Entities())
        //{

        //        //function evaluation times out on this qry:

        //    // First make sure we're only looking at the current employee information
        //    IQueryable<BO.EmployeeHistory> currentEntries = 
        //        from eh in ctx.EmployeeHistory
        //        group eh by eh.EmployeeID into grp 
        //        select grp.OrderBy(eh => eh.DateLastUpdated).FirstOrDefault();

        //    // Now filter by the manager's ID
        //    var dirRpts = currentEntries
        //        .Where(eh => eh.DirectManagerEmployeeID == mgrId);

        //    // This would be ideal, assuming your entity associations are set up right
        //    employees = dirRpts.Select(eh => eh.Employee).Distinct();

        //    //// If the above won't work, this is the next-best thing
        //    //var employees2 = ctx.Employee.Where(
        //    //                    emp => directRpts.Any(
        //    //                        eh => eh.EmployeeId == emp.EmployeeId));
        //}
        //return employees.ToList();
    }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜