简体   繁体   中英

LINQ Self Referencing Table SELECT Query

I have a self referencing employee table, meaning that there are employees with a primary key "EmployeeID" and a foreign key "SupervisorID" for the parent (supervisor). Managers are on top of the hirarchy, so their SupervisorID is null. There's also a date column (CreatedOn), to filter for specific dates, but only managers have a date, not their employees. I'm trying to get all results from a specific date, managers and their employees, but I just can't get it running. I need to achieve this with LINQ, but I thought I could an SQL query first, to see if I'm somehow able to migrate this to an LINQ query (doesn't matter which syntax). But this was very dellusional, once I realized that it's as hard as in LINQ... I thought that this might be achieved with a CTE, but I can't get the self-reference running at all. Either I just get managers or nothing... Here's a sample table:

------------------------------------------------------
|EmployeeID|Name    |SupervisorID|CreatedOn          |
|1         |Sanders |NULL        |2019-01-01 16:20:33|
|2         |Flanders|1           |NULL               |
|3         |Andrews |1           |NULL               |
|4         |Ranger  |NULL        |2019-03-20 18:04:56|
|5         |Hammer  |4           |NULL               |
|6         |Enderson|4           |NULL               |
|7         |Larsson |NULL        |2019-10-03 04:55:16|
|8         |Simpson |8           |NULL               |
------------------------------------------------------

You can see, that Sanders (2019-01-01 16:20:33) has 2 employees (Flanders, Andrews), Ranger 2019-03-20 18:04:56 has 2 employees (Hammer, Enderson) and Larsson (2019-10-03 04:55:16) has 1 employee (Simpson)

I didn't get anywhere at all with LINQ. If I filter on the date, I don't have any way anymore, to filter on the initial dataset for the employees.

// This filters by date, but then I can't get the employees without date anymore
employees.Where(e => e.CreatedOn >= '2019-01-01' && e.CreatedOn < '2019-01-02')

// This filters all employees with a parent, but then I can't get the managers anymore and can't filter against a date anymore either
employees.Where(e => e.SupervisorID != null)

I tried a join, but that didn't work very well either:

from employees m in employees
join e in employees
on m.EmployeeId equals e.SupervisorID
where m.CreatedOn >= '2019-01-01' && m.CreatedOn < '2019-01-02'
select m

An expected resultset for an appropriate filter would be Sanders and his employees, since Sanders' entry was created on January 1 and since his employees work under him:

------------------------------------------------------
|EmployeeID|Name    |SupervisorID|CreatedOn          |
|1         |Sanders |NULL        |2019-01-01 16:20:33|
|2         |Flanders|1           |NULL               |
|3         |Andrews |1           |NULL               |
------------------------------------------------------

Does anyone have an idea, how I could achieve this? I won't be able to adjust the database design, since I can only read and another application pushes the data, which I can't change.

Let's look at supervisors and employees separately, and then merge the two later on.

Supervisors

For a supervisor, the logic is fairly simple, you filter on the creation date.

var fromDate = new DateTime(2019,1,1);
var untilDate = new DateTime(2019,1,2);

var supervisors = employees
                         .Where(e => e.CreatedOn >= fromDate 
                                  && e.CreatedOn < untilDate )
                         .ToList();

Note: in your dataset, only supervisors have a creation date. If employees can have a creation date as well, you need to explicitly filter them out:

var supervisors = employees
                         .Where(e => e.CreatedOn >= fromDate 
                                  && e.CreatedOn < untilDate 
                                  && e.SupervisorID == null)
                         .ToList();

Employees

The logic for employees is slightly more complicated, but you can rely on EF's navigational properties to handle the joins for you.

In short, you want the employees whose supervisor has a creation date in the selected range.

var fromDate = new DateTime(2019,1,1);
var untilDate = new DateTime(2019,1,2);

var subordinates = employees
                         .Where(e => e.SupervisorID != null
                                  && e.Supervisor.CreatedOn >= fromDate 
                                  && e.Supervisor.CreatedOn < untilDate )
                         .ToList();

Merging the two

Merging these is fairly simple, it's effectively doing supervisorFilter OR subordinateFilter :

var fromDate = new DateTime(2019,1,1);
var untilDate = new DateTime(2019,1,2);

var supervisorsAndSubordinates = employees
                         .Where(e => 
                                  // Supervisor filter
                                  (
                                       e.CreatedOn >= fromDate 
                                    && e.CreatedOn < untilDate
                                  )
                                  ||
                                  // Subordinate filter
                                  (
                                       e.SupervisorID != null
                                    && e.Supervisor.CreatedOn >= fromDate 
                                    && e.Supervisor.CreatedOn < untilDate
                                  ))
                         .ToList();

This will give you the result set you're expecting. Note that the solution becomes significantly harder when this structure is recursive (ie multiple "supervisor of supervisor" levels), but this is currently not the case in your example.


using join, you can have both managers and their subordinates:

var list = employees.Where(m=> m.CreatedOn >= new DateTime(2019,1,1) && m.CreatedOn < new DateTime(2019,1,2)).Join(employees,sc=> sc.EmployeeId, soc=> soc.SupervisorId, (sc,soc)=> new {left= sc, right=soc}  ).ToList();

I've created a fiddle, check below link:

C# compiler fiddle

note: I joined employees with itself, considering the date filter for left list, and joining on supervisorId on right list. It shows manager, and his/her respective employee

You need a left outer join

Ie something like

 var mylist = (from employee in employees
              join r in employees on employee.SupervisorId equals r.EmployeeId  into results                  
              from supervisor in results.DefaultIfEmpty()

               let createdDate = supervisor != null ? supervisor.CreatedOn : employee.CreatedOn                  
               where  createdDate >= new DateTime(2019,1,1) && createdDate < new DateTime(2019,1,2) 
               select employee);

I'm assuming that there are only two levels (ie no-one can be a supervisor and have a supervisor).

All Linq methods are lazy-evaluated, you should use an terminal method to get result immediately.

Terminal Methods like ToList, ToArray, ForEach...

employees.Where(e => e.SupervisorID != null).ToList()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM