Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I mix TPH and TPT in Entity Framework 6?

Initial situation

I have an application that uses an existing database, currently using NHibernate as O/R-Mapper.
Now I need to migrate to Entity Framework 6.1.1 using Code First and Fluent API Configuration.

But now I have a problem with a part of the data model because it uses different types of inheritance strategies (TPT and TPH)

Structure

Note: Posting the complete data model here seemed a bit too enormous to me so I reproduced the problem I face in a small POC program.

CLASS                  | TABLE              | TYPE
-----------------------+--------------------+------
BaseEntity (abstract)  | BaseTable          |
Inherited_TPH          | BaseTable          |  1
Inherited_TPT          | Inherited_TPT      |  2

The column used as discrimator in the table is called Type

Based on this answer I added an abstract class Intermediate_TPH as intermediate layer:

Diagram of classes and mapping

Some sample data: Entry with ID=3 is of type Inherited_TPT

Data

Code

These are my entity classes and my context class:

class MyContext : DbContext
{
    public MyContext ( string connectionString )
        : base ( connectionString )
    {
    }

    public DbSet<Inherited_TPH> TPH_Set { get; set; }
    public DbSet<Inherited_TPT> TPT_Set { get; set; }
    public DbSet<SomethingElse> Another_Set { get; set; }

    protected override void OnModelCreating ( DbModelBuilder modelBuilder )
    {
        modelBuilder
        .Entity<BaseEntity> ()
        .ToTable ( "BaseTable" );

        modelBuilder
        .Entity<Inherited_TPH> ()
        .Map ( t => t.Requires ( "Type" ).HasValue ( 1 ) );

        modelBuilder
        .Entity<Intermediate_TPT> ()
        .Map ( t => t.Requires ( "Type" ).HasValue ( 2 ) );

        modelBuilder
        .Entity<Intermediate_TPT> ()
        .Map<Inherited_TPT> ( t => t.ToTable ( "Inherited_TPT" ) ); 

        modelBuilder
        .Entity<SomethingElse> ()
        .ToTable ( "SomethingElse" )
        .HasKey ( t => t.Id );
    }
}

public abstract class BaseEntity
{
    public virtual int Id { get; set; }
    public virtual string Title { get; set; }
}
public class Inherited_TPH : BaseEntity
{
}
public abstract class Intermediate_TPT : BaseEntity
{
}
public class Inherited_TPT : Intermediate_TPT
{
    public virtual string Comment { get; set; }
}
public class SomethingElse
{
    public virtual string Description { get; set; }
    public virtual int Id { get; set; }
}

Running the following code will give me an error.

    static void Main ( string[] args )
    {
        Database.SetInitializer<MyContext> ( null );
        var ctx = new MyContext ( @"Data Source=(local);Initial Catalog=nh_ef;Integrated Security=true" );
        try
        {
            // Accessing Inherited_TPH works just fine
            foreach ( var item in ctx.TPH_Set ) Console.WriteLine ( "{0}: {1}", item.Id, item.Title );
            // Accessing Inherited_TPT works just fine
            foreach ( var item in ctx.TPT_Set ) Console.WriteLine ( "{0}: {1} ({2})", item.Id, item.Title, item.Comment );
            // The rror occurs when accessing ANOTHER entity:
            foreach ( var item in ctx.Another_Set ) Console.WriteLine ( "{0}: {1}", item.Id, item.Description );
        }
        catch ( Exception ex )
        {
            Console.WriteLine ( ex.Message );
            if( ex.InnerException != null ) { Console.WriteLine ( ex.InnerException.Message ); }
        }
    }

Output

The program produces the following output:

1: Simpson
2: Johnson
3: Smith (More details about SMITH)
4: Miller (More details about MILLER)
An error occurred while preparing the command definition. See the inner exception for details.

(26,10) : error 3032: Problem in mapping fragments starting at lines 14, 26:EntityTypes PoC.Inherited_TPH, PoC.Inherited_TPT are being mapped to the same rows in table BaseEntity. Mapping conditions can be used to distinguish the rows that these types are mapped to.

Question

As you can see, the mapping seems to work because I can load all data from Inherited_TPT and Inherited_TPH. But when accessing another entity, I get an exception.

How do I need to configure the mapping to get rid of this error and be able to access the existing database structure?

like image 919
Stephan Bauer Avatar asked Nov 26 '14 10:11

Stephan Bauer


1 Answers

I finally found the solution myself. I'll post it here in case someone else will need the same behavior...

  • The intermediate layer is not needed in this case
  • The discriminator is only needed for TPH

So all we need to do is to change the definition of the mapping:

protected override void OnModelCreating ( DbModelBuilder modelBuilder )
{
    // Not changed
    modelBuilder
        .Entity<BaseEntity> ()
        .ToTable ( "BaseTable" );


    // --- CHANGED ---
    modelBuilder.Entity<BaseEntity> ()
        // TPH => Discriminator
        .Map<Inherited_TPH> ( m => m.Requires ( "Type" ).HasValue ( 1 ).IsOptional () ) 
        // TPT => Mapping to table
        .Map<Inherited_TPT> ( m => m.ToTable ( "Inherited_TPT" ) ); 

    // Not changed
    modelBuilder
        .Entity<SomethingElse> ()
        .ToTable ( "SomethingElse" )
        .HasKey ( t => t.Id );
}
like image 165
Stephan Bauer Avatar answered Oct 13 '22 01:10

Stephan Bauer