Menus

Thursday, November 22, 2012

Implementing Inheritance with the Entity Framework in an ASP.NET MVC Application Chapter8

In the previous tutorial you handled concurrency exceptions. This tutorial will show you how to implement inheritance in the data model.
In object-oriented programming, you can use inheritance to eliminate redundant code. In this tutorial, you'll change the Instructor and Student classes so that they derive from a Person base class which contains properties such as LastName that are common to both instructors and students. You won't add or change any web pages, but you'll change some of the code and those changes will be automatically reflected in the database.

Table-per-Hierarchy versus Table-per-Type Inheritance

In object-oriented programming, you can use inheritance to make it easier to work with related classes. For example, the Instructor and Student classes in the School data model share several properties, which results in redundant code:
Student_and_Instructor_classes
Suppose you want to eliminate the redundant code for the properties that are shared by the Instructor and Student entities. You could create a Person base class which contains only those shared properties, then make the Instructor and Student entities inherit from that base class, as shown in the following illustration:
Student_and_Instructor_classes_deriving_from_Person_class
There are several ways this inheritance structure could be represented in the database. You could have a Person table that includes information about both students and instructors in a single table. Some of the columns could apply only to instructors (HireDate), some only to students (EnrollmentDate), some to both (LastName, FirstName). Typically you'd have a discriminator column to indicate which type each row represents. (In this case , the discriminator column might have "Instructor" for instructors and "Student" for students.)
Table-per-hierarchy_example
This pattern of generating an entity inheritance structure from a single database table is called table-per-hierarchy (TPH) inheritance.
An alternative is to make the database look more like the inheritance structure. For example, you could have only the name fields in the Person table and have separate Instructor and Student tables with the date fields.
Table-per-type_inheritance
This pattern of making a database table for each entity class is called table per type (TPT) inheritance.
TPH inheritance patterns generally deliver better performance in the Entity Framework than TPT inheritance patterns, because TPT patterns can result in complex join queries. This tutorial demonstrates how to implement TPH inheritance. You'll do that by performing the following steps:
  • Create a Person class and change the Instructor and Student classes to derive from Person.
  • Add model-to-database mapping code to the database context class.
  • Change InstructorID and StudentID references throughout the project to PersonID.

Creating the Person Class

In the Models folder, create Person.cs and replace the existing code with the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
    public abstract class Person
    {
        [Key]
        public int PersonID { get; set; }

        [Required(ErrorMessage = "Last name is required.")]
        [Display(Name="Last Name")]
        [MaxLength(50)]
        public string LastName { get; set; }

        [Required(ErrorMessage = "First name is required.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [MaxLength(50)]
        public string FirstMidName { get; set; }

        public string FullName 
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}
In Instructor.cs, derive the Instructor class from the Person class and remove the key and name fields. The code will look like the following example:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
        [Required(ErrorMessage = "Hire date is required.")]
        [Display(Name = "Hire Date")]
        public DateTime? HireDate { get; set; }

        public virtual ICollection<Course> Courses { get; set; }
      
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}
Make similar changes to Student.cs. The Student class will look like the following example:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [Required(ErrorMessage = "Enrollment date is required.")]
        [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime? EnrollmentDate { get; set; }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

Adding the Person Entity Type to the Model

In SchoolContext.cs, add a DbSet property for the Person entity type:
        public DbSet<Person> People { get; set; }
This is all that the Entity Framework needs in order to configure table-per-hierarchy inheritance. As you'll see, when the database is re-created, it will have a Person table in place of the Student and Instructor tables.

Changing InstructorID and StudentID to PersonID

In SchoolContext.cs, in the Instructor-Course mapping statement, change MapRightKey("InstructorID") to MapRightKey("PersonID"):
modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));
This change isn't required; it just changes the name of the InstructorID column in the many-to-many join table. If you left the name as InstructorID, the application would still work correctly.
Next, perform a global change (all files in the project) to change InstructorID to PersonID and StudentID to PersonID. Make sure that this change is case-sensitive. (Note that this demonstrates a disadvantage of the classnameID pattern for naming primary keys. If you had named primary keys ID without prefixing the class name, no renaming would be necessary now.)

Adjusting Primary Key Values in the Initializer

In SchoolInitializer.cs, the code currently assumes that primary key values for Student and Instructor entities are numbered separately. This is still correct for Student entities (they'll still be 1 through 8), but Instructor entities will now be 9 through 13 instead of 1 through 5, because the block of code that adds instructors comes after the one that adds students in the initializer class. Replace the code that seeds the Department and OfficeAssignment entity sets with the following code that uses the new ID values for instructors:
var departments = new List<Department>
{
    new Department { Name = "English",     Budget = 350000,  
StartDate = DateTime.Parse("2007-09-01"), PersonID = 9 },
    new Department { Name = "Mathematics", Budget = 100000,  
StartDate = DateTime.Parse("2007-09-01"), PersonID = 10 },
    new Department { Name = "Engineering", Budget = 350000,  
StartDate = DateTime.Parse("2007-09-01"), PersonID = 11 },
    new Department { Name = "Economics",   Budget = 100000,  
StartDate = DateTime.Parse("2007-09-01"), PersonID = 12 }
};
var officeAssignments = new List<OfficeAssignment>
{
    new OfficeAssignment { PersonID = 9, Location = "Smith 17" },
    new OfficeAssignment { PersonID = 10, Location = "Gowan 27" },
    new OfficeAssignment { PersonID = 11, Location = "Thompson 304" },
};

Changing OfficeAssignment to Lazy Loading

The current version of the Entity Framework doesn't support eager loading for one-to-zero-or-one relationships when the navigation property is on the derived class of a TPH inheritance structure. This is the case with the OfficeAssignment property on the Instructor entity. To work around this, you'll remove the code you added earlier to perform eager loading on this property.
In InstructorController.cs, delete the three occurrences of the following line of code:
             .Include(i => i.OfficeAssignment)

Testing

Run the site and try various pages. Everything works the same as it did before.
In Solution Explorer, double-click School.sdf to open the database in Server Explorer. Expand School.sdf and then Tables, and you see that the Student and Instructor tables have been replaced by a Person table. Expand the Person table and you see that it has all of the columns that used to be in the Student and Instructor tables, plus the discriminator column.
Server_Explorer_showing_Person_table
The following diagram illustrates the structure of the new School database:
School_database_diagram
Table-per-hierarchy inheritance has now been implemented for the Person, Student, and Instructor classes.

No comments:

Post a Comment