Polymorphic Associations in Entity Framework
Asked Answered
C

1

1

I have a legacy database that has a few tables which were designed using Polymorphic Associations. By polymorphic associations, I mean that those tables can be child of different tables according to a column ObjectType.

Example:

  • Documents table has DocumentID (identity primary key), some other columns, and 2 special columns called ObjectType and ObjectID.
  • If ObjectType='STUDENT', ObjectID points to Students table.
  • If ObjectType='TEACHER', ObjectID points to Teachers table, etc.

This is similar to this design (as described in the "no foreign key approach") or this one (described as an anti-pattern). And obviously, there are no foreign key constraints on those columns (AFAIK no database would allow that kind of relationship).

I'm writing a new Data Access Layer using Entity Framework 6 (code-first with Fluent API) which should work side-by-side with existing code. Since this database structure was deployed to hundreds of different customers (each one with a different codebase and different database customizations), modifying the structure of existing tables is not an option.

My question is: How do I map those Polymorphic Associations into my EF code-first model?


EDIT: It seems that I was trying to design the class hierarchy on the wrong entities. My understanding was that I had a lot of "GenericChildTables" (like Documents) that should point to a (non-existing) entity that would have a composite key ObjectType+ObjectID. And then I was trying to create that new entity (let's call it "BusinessObject") and map my core entities (Students, Teachers, etc) to be subtypes of this BusinessObject.

THAT design was probably just plain wrong, and maybe totally impossible because this new table that I was creating (BusinessObject) depended on StudentID/TeacherID, etc, so it couldn't be a parent of those tables. Using some ugly workarounds I could create that BusinessObject as a single-child for each core entity, and map those BusinessObjects to the polymorphic tables, and it was working indeed but in a completely wrong design.

Then I saw Gert Ardold's question and realized that what should be designed as a class hierarchy was NOT Students/Teachers/etc (grouped into a generic entity), but each one of those ChildTables, which were holding different subtypes according to the ObjectType discriminator - those were the types that should be splitted into subtypes. See my solution on my own answer below.

Cheka answered 8/7, 2016 at 21:0 Comment(4)
Here's how I did it using database-first, but also described how I couldn't port it to code-first.Carman
@GertArnold Have you tried something like this gist.github.com/mauriciolobo/40a4fc6017539c79135bf62f57ee80f1 ?Cheka
No, that's not the right model (two foreign keys).Carman
@GertArnold That was a friend's reply to my question, but I'm not sure if it was working. Your design pointed me into the right direction, and I've just posted an answer and it seems to be working now. If I understood correctly your code, I guess the problem was that OwnerId should be removed from the base class (Comment) and be part only of the derived classes.Cheka
C
3

It seems that I was trying to design the class hierarchy on the wrong entities. My understanding was that I had a lot of "GenericChildTables" (like Documents) that should point to a (non-existing) entity that would have a composite key ObjectType+ObjectID. And then I was trying to create that new entity (let's call it "BusinessObject") and map my core entities (Students, Teachers, etc) to be subtypes of this BusinessObject.

Then I saw Gert Ardold's question and realized that the correct inheritance design was NOT about grouping Students/Teachers/etc into a supertype, but about splitting those GenericChildTables into multiple subtypes.

I'll use the Documents table as an example to show how I converted those GenericChildTables into a TPH, and how I mapped my core entities (Students, Teachers, etc) to collections of those subtypes.

First, I created the derived classes (subtypes), added navigation properties, and mapped those subtypes to the base type using ObjectType as type discriminator:

public class StudentDocument : Document
{
    public Student Student { get; set; }
    public int StudentID { get; set; } 
}
public class TeacherDocument : Document
{
    public Teacher Teacher { get; set; }
    public int TeacherID { get; set; } 
}
modelBuilder.Entity<Document>()
.Map<StudentDocument>(m => {
    m.Requires("ObjectType").HasValue("STUDENT");
})
.Map<TeacherDocument>(m => {
    m.Requires("ObjectType").HasValue("TEACHER");
});

Then I added the navigation properties to my core classes (Student and Teachers), pointing to the subtypes created:

partial class Student
{
   public virtual ICollection<StudentDocument> Documents { get; set; }
}
partial class Teacher
{
   public virtual ICollection<TeacherDocument> Documents { get; set; }
}

I created the mappings for relationships Student.Documents and Teacher.Documents. Please note that I use properties StudentID and TeacherID, but they are phisically mapped to ObjectID column:

var sl = modelBuilder.Entity<StudentDocument>();
sl.Property(t => t.StudentID).HasColumnName("ObjectID");
sl.HasRequired(t => t.Student).WithMany(t => t.Documents).HasForeignKey(d => d.StudentID);

var al = modelBuilder.Entity<TeacherDocument>();
al.Property(t => t.TeacherID).HasColumnName("ObjectID");
al.HasRequired(t => t.Teacher).WithMany(t => t.Documents).HasForeignKey(d => d.TeacherID);

Finally, I removed from the base type (Document) the property ObjectType, because it is a type discriminator, and should be used only internally (cannot be exposed on class).
I also removed from the base type ObjectID because this should be mapped only on the subtypes (mapped respectively as StudentID and TeacherID).

And everything worked like a charm!

PS: Please note that if you are using T4 templates (code first from database) they will always regenerate those properties, because templates know nothing about hiearchies, so they map Documents into a single entity with properties for every column, so you should manually exclude those properties.

Cheka answered 9/7, 2016 at 23:16 Comment(2)
I had a play with it and this seemed to create TPC or TPH but without FK assignments. What was the resultant schema it produced?Carinacarinate
Mardoxx, actually I was using a legacy database, so I was using code first from database (in other words, I was not automatically generating the database). I really can't comment on schema generation because I don't know much about code first.Cheka

© 2022 - 2024 — McMap. All rights reserved.