Search code examples
inheritanceentity-framework-4identity-columntable-per-type

How can I add inheritance to an object defined in an EntityDataModel (EF 4)?


I have a simple 2 object inheritance defined in an EF model, Person <- User.

Person is the base entity/class, it is not abstract, and it contains fields like firstname, lastname, email.

User is the derived entity/class, and contains fields like username, lastlogin, islockedout.

The database uses a table-per-type schema, so there is 1 table for Person, and another table for User. Both use the same primary key field, PersonID. PersonID is an IDENTITY column, but in the EDM, StoreGeneratedPattern for PersonID is set to None.

Because this is a table-per-type inheritance schama, there can be a Person row/object that doesn't have a corresponding User row/object. This is so that the system can contain data about people who are not users. However, a Person may eventually become a User, and this is where I'm having problems.

I have a test case for this scenario, where the system finds a Person that it wants to turn into a User. I've tried setting the PersonID field on a new User object, adding it to the ObjectContext, and saving changes. What happens is the database creates a new Person row along with the new User row, ignoring the value I set for PersonID.

How can I get around this? Do I have to create sprocs to handle the EDM crud operations?


Solution

  • I was able to solve this problem, though I'm not entirely happy with the solution. I ended up writing stored procedures to map the insert, update, and delete operations for both Person and User (since they're in the same Entity set, both have to be mapped with all 3 sprocs). The insert sproc for User takes a PersonID parameter, and uses it to decide whether to create a new Person row, or attach to an existing Person row.

    The catch is that the PersonID has to be mapped as both sproc input parameter and as a Result Column binding. When the db must generate a new PersonID, we have to get it back out using the result binding in order for other tables to get the correct value for foreign key columns referencing PersonID. Except.... you can't map User.PersonID as both an input parameter and a result column binding at the same time. This causes a runtime exception "Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements or store generated values."

    I ended up adding a new column to the User table (and to the corresponding conceptual model) which is a nullable int (I called it BasePK). In the EDM, I made the getters and setters for this property protected, to hide them from the public interface. Then, I mapped BasePK as the sproc input parameter, and mapped PersonID as a result column binding to receive the identity value if one was generated. One overload for the User constructor goes like this:

    public User(int personID) : base() { this.BasePK = personID; }

    So now, whenever I want to turn an existing Person into a User, I just have to construct the user using this overload.

    int personID = [some method to search for person and return his/her PersonID]; var User = new User(personID);

    When the ObjectContext goes to insert the entity, it passes the PersonID to the sproc via the BasePK property. When the sproc is done, it returns the PersonID to the actual primary key property.

    I didn't like the idea of having to add a column & property to accomplish this, but it does jive with my comment to Craig's answer: I want to be able to plug into Person with new specializations without having to change / recompose the Person entity each time. This way, at least I can keep all of the changes in the derived entity types, even if it means those changes involve adding meaningless columns which will always be null in the physical store.