Recursive or hierarchical queries using EF Code First and Migrations

From Logic Wiki
Jump to: navigation, search

Introduction

This sample demonstrates how you could implement recursive or hierarchical queries using EF Code First for a self-referencing table or entity. You can then query an entity for all its descendents or all its ancestors. LINQ itself does not support recursive or hierarchical queries. To execute recursive or hierarchical queries you need to execute sql in the database directly that uses a recursive common table expression (CTE) in Microsoft SQL Server or uses CONNECT BY PRIOR and CONNECT_BY_ROOT in Oracle. Preferably you would create a table valued function (TVF) in the database, but EF Code First does not yet support calling TVFs. Instead, until EF Code First supports TVFs, we can create a join or junction entity and have EF Code First map that entity to a view instead of a table, where this view has the required recursive or hierarchical sql. This join entity is otherwise similar to the one required by many-to-many relationships. Where the parent-child relationship is one-to-many, the ancestor-descendant relationship is indeed many-to-many. If you let EF Code First create your database it will by default create a table for this join entity. Using an IDatabaseInitializer you can drop the table after its been created and create a view in its place. Or using EF Code First Migrations you can forego the creation of the table completely and create a view directly. The view is not an updateable view, but as long as you only query the join entity and not insert, update or delete the join entity EF Code First is just as happy querying a view as it is querying a table. And just like a TVF a view is composable.

Building the Sample

The sample requires Visual Studio 2010 and SQL Server Express. It uses the current beta 1 of EF Code First Migrations. To build the database run the Update-Database command from the NuGet Package Manager Console in Visual Studio.

Description

We start with a simple self-referencing entity called Employee.

public class Employee 
   { 
       public int EmployeeID { get; set; } 
       [Required] 
       public string LastName { get; set; } 
       [Required] 
       public string FirstName { get; set; } 
       public int? ReportsToEmployeeID { get; set; } 
       public virtual Employee ReportsTo { get; set; } 
       public virtual ICollection<Employee> Manages { get; set; } 
   }

We then create a DbContext as follows:

public class NorthwindContext : DbContext, INorthwindContext  
   {  
       public IDbSet<Employee> Employees { get; set; }  
 
       protected override void OnModelCreating(DbModelBuilder modelBuilder)  
       {  
           modelBuilder.Entity<Employee>().HasOptional(e => e.ReportsTo).WithMany(e => e.Manages).HasForeignKey(e => e.ReportsToEmployeeID).WillCascadeOnDelete(false);  
       }  
   }

Next we define the signature of our two recursive or hierarchical queries in an interface.

 public interface INorthwindContext : IUnitOfWork  
   {  
       IDbSet<Employee> Employees { get; }  
       IQueryable<Employee> GetEmployeesForManagerRecursively(int managerEmployeeID);  
       IQueryable<Employee> GetManagersForEmployeeRecursively(int employeeID);  
   }

Preferably we would like to use a table-valued function to implement these two methods, but because EF Code First does not yet support calling TVFs, we can achieve the same result using a join or junction entity called ManagerEmployee mapped to a view in the database.

 public class ManagerEmployee  
   {  
       public int ManagerEmployeeID { get; set; }  
       public int EmployeeID { get; set; }  
   }

Preferrably we would define this class as internal instead of public, but in order for EF Code First to be able to map this entity to a view in the database we need to define this class as public. We also need to define a public IDbSet of this entity on the NorthwindContext, but we purposely do not expose the IDbSet of this entity on the INorthwindContext interface. We do need to define the primary key of the entity in the NorthwindContext OnModelCreating method. Finally we can also implement the recursive or hierarchical queries using the ManagerEmployee entity.

public class NorthwindContext : DbContext, INorthwindContext  
   {  
       public IDbSet<Employee> Employees { get; set; }  
       public IDbSet<ManagerEmployee> ManagerEmployees { get; set; }  
 
       protected override void OnModelCreating(DbModelBuilder modelBuilder)  
       {  
           modelBuilder.Entity<Employee>().HasOptional(e => e.ReportsTo).WithMany(e => e.Manages).HasForeignKey(e => e.ReportsToEmployeeID).WillCascadeOnDelete(false);  
           modelBuilder.Entity<ManagerEmployee>().HasKey(me => new { me.ManagerEmployeeID, me.EmployeeID });  
       }  
 
       public IQueryable<Employee> GetEmployeesForManagerRecursively(int managerEmployeeID)  
       {  
           return from managerEmployee in this.ManagerEmployees  
                  join employee in this.Employees on managerEmployee.EmployeeID equals employee.EmployeeID  
                  where managerEmployee.ManagerEmployeeID == managerEmployeeID  
                  select employee;  
       }  
 
       public IQueryable<Employee> GetManagersForEmployeeRecursively(int employeeID)  
       {  
           return from managerEmployee in this.ManagerEmployees  
                  join manager in this.Employees on managerEmployee.ManagerEmployeeID equals manager.EmployeeID  
                  where managerEmployee.EmployeeID == employeeID  
                  select manager;  
       }  
   }

To create the database and the view we could create a DatabaseInitializer as follows

  public class NorthwindDatabaseInitializer : IDatabaseInitializer<NorthwindContext> 
   { 
       public void InitializeDatabase(NorthwindContext context) 
       { 
           if (context.Database.Exists() && !context.Database.CompatibleWithModel(true)) 
           { 
               context.Database.Delete(); 
           } 

           if (!context.Database.Exists()) 
           { 
               context.Database.Create(); 

               context.Database.ExecuteSqlCommand("DROP TABLE [dbo].[ManagerEmployees]"); 

               context.Database.ExecuteSqlCommand(@"CREATE VIEW [dbo].[ManagerEmployees] 

AS

   WITH    cte ( ManagerEmployeeID, EmployeeID ) 
             AS ( SELECT   EmployeeID , 
                           EmployeeID 
                  FROM     dbo.Employees 
                  UNION ALL 
                  SELECT   e.EmployeeID , 
                           cte.EmployeeID 
                  FROM     cte 
                           INNER JOIN dbo.Employees AS e ON e.ReportsToEmployeeID = cte.ManagerEmployeeID 
                ) 
   SELECT  ISNULL(EmployeeID, 0) AS ManagerEmployeeID , 
           ISNULL(ManagerEmployeeID, 0) AS EmployeeID 
   FROM    cte"); 

               Seed(context); 

               context.SaveChanges(); 
           } 
       } 

       private void Seed(NorthwindContext context) 
       { 
        } 
   }

Which we can set as follows:

Database.SetInitializer<NorthwindContext>(new NorthwindDatabaseInitializer());

Or, using EF Code First Migrations instead of a DatabaseInitializer you can completely forego the creation of the table and create the view directly.

 public partial class ManagerEmployeesMigration : DbMigration 
   { 
       public override void Up() 
       { 
           //CreateTable( 
           //    "ManagerEmployees", 
           //    c => new 
           //        { 
           //            ManagerEmployeeID = c.Int(nullable: false), 
           //            EmployeeID = c.Int(nullable: false), 
           //        }) 
           //    .PrimaryKey(t => new { t.ManagerEmployeeID, t.EmployeeID }); 

           Sql(@"CREATE VIEW [dbo].[ManagerEmployees] 

AS

   WITH    cte ( ManagerEmployeeID, EmployeeID ) 
             AS ( SELECT   EmployeeID , 
                           EmployeeID 
                  FROM     dbo.Employees 
                  UNION ALL 
                  SELECT   e.EmployeeID , 
                           cte.EmployeeID 
                  FROM     cte 
                           INNER JOIN dbo.Employees AS e ON e.ReportsToEmployeeID = cte.ManagerEmployeeID 
                ) 
   SELECT  ISNULL(EmployeeID, 0) AS ManagerEmployeeID , 
           ISNULL(ManagerEmployeeID, 0) AS EmployeeID 
   FROM    cte"); 
       } 

       public override void Down() 
       { 
           //DropTable("ManagerEmployees"); 
           Sql("DROP VIEW [dbo].[ManagerEmployees]"); 
       } 
   }

You then create the database using the Update-Database command in the NuGet Package Manager Console in Visual Studio.

Sample File

RecursiveSample