12

I'm using Entity Framework 4.3.1 Code-First and I need to split an entity between two tables. The tables have a primary key shared, and it is 1-to-1, but the columns are not named the same on each table.

I don't control the data layout, nor can I request any changes.

So for example, the SQL tables could be

SQL data tables

And this would be my entity...

public class MyEntity
{
    public int Id {get; set;}
    public string Name {get;set}
    public string FromAnotherTable {get;set;}
}

And here is the mapping I have.

public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
    public MyEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        this.Property(e => e.Name).HasColumnName("MyDatabaseName");
        this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
        this.Map(m =>
            {
                m.Properties(e =>
                     {
                         e.Id,
                         e.Name
                     });
                m.ToTable("MainTable");
            });
        this.Map(m =>
            {
                m.Properties(e =>
                     {
                         e.Id,
                         e.FromAnotherTable
                     });
                m.ToTable("ExtendedTable");
            });
}

Since the key shared between them has a different column name, I'm not sure how to map it. This mapping will compile, but fails at runtime because EF emits SQL looking for the "ThePrimaryKeyId" column on the "ExtendedTable" table, which doesn't exist.

EDIT To clarify, what I have defined above can (and does) work if the PK on the "ExtendedTable" followed naming conventions. But it doesn't and I can't change the schema.

Basically, what I need EF to emit is a SQL statement like

SELECT
    [e1].*,   /*yes, wildcards are bad. doing it here for brevity*/
    [e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2]  /*Could be left join, don't care. */
    ON  [e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]

But the only thing it seems to want to emit is

 SELECT
        [e1].*,
        [e2].*
    FROM [MainTable] AS [e1]
    INNER JOIN [ExtendedTable] AS [e2]
        ON  [e1].[ThePrimaryKeyId] = [e2].[ThePrimaryKeyId] /* this column doesn't exist */

Edit I tried the 1-to-1 approach again at NSGaga's suggestion. It didn't work, but here are the results. Entities

public class MyEntity
{
    public int Id { get; set; }
    public int Name { get; set; }
    public virtual ExtEntity ExtendedProperties { get; set; }
}
public class ExtEntity
{
    public int Id { get; set; }
    public string AnotherTableColumn { get; set; }
    public virtual MyEntity MainEntry { get; set; }
}

Here are the mapping classes

public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
    public MyEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        this.Property(e => e.Name).HasColumnName("MyDatabaseName");
        this.ToTable("MainTable");
        this.HasKey(e => e.Id);
        this.HasRequired(e => e.ExtendedProperties).WithRequiredPrincipal(f => f.MainEntry);
    }
}

public class ExtEntityMapping : EntityTypeConfiguration<ExtEntity>
{
    public ExtEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("NotTheSameName");
        this.Property(e => e.AnotherTableColumn).HasColumnName("AnotherTableColumn");
        this.ToTable("ExtendedTable");
        this.HasKey(e => e.Id);
        this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties);
    }
}

This setup gets the message

"Column or attribute 'MyEntity_ThePrimaryKeyId' is not defined in 'ExtendedTable'"

Changing the final map line to

this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties).Map(m => M.MapKey("NotTheSameName"));

Returns this message

"Each property name in a type must be unique. property name 'NotTheSameName' was already defined."

Changing the mapped key to use the column from the parent table, MapKey("ThePrimaryKeyId"). returns this message

"Column or attribute 'ThePrimaryKeyId' is not defined in 'ExtendedTable'"

Removing the Id property from the ExtEntity class throws an error because then the entity doesn't have a defined key.

Josh
  • 2,740
  • 3
  • 27
  • 41
  • Ah! so the "ExtendedTable" is not in the schema that you are creating; if you are just blowing away and creating a new database via a DropCreateDatabaseAlways then there is no way to map to an non-existent table; the only way would be to include this table as part of the schema or have a preset DB with the "ExtendedTable" already there and use EF 4.3 code migrations to alter the database opposed to create a new one. – Ricky Gummadi Feb 21 '12 at 23:29
  • I'm not sure I follow what you mean, but this is an existing database schema. In fact, this is mapping to a DB2 database on a mainframe. I do not want EF to try to drop or create anything. – Josh Feb 22 '12 at 00:11
  • 1
    Hi Josh I have the same problem and have posted it here on the EF forums [here](http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/be9f5e6a-e5bf-45a6-b86a-b2a74cfda105), haven't had much luck working around it. Have you heard from any of the EF team? – TheDuke Mar 20 '12 at 10:55
  • Nope, haven't heard anything but crickets. :) – Josh Mar 20 '12 at 15:20
  • 1
    Are you sure you must 'split' entity in two? just asking - relating two entities (1-to-1 - required:required) together is fully customizable. The map / properties relies on anonymous types and requires a function/expression pointing to a property (and it's how it gets the name) - and it's pretty 'sensible' requirement for splitting into two tables. This 'avenue' of EF has always been a bit problematic. You could try changing the migration script to rename column name but it smells of hacking in this case, not sure if it'd work. – NSGaga-mostly-inactive Mar 21 '12 at 22:37
  • I know I tried 1-to-1, but it has been so long that I don't remember why I had trouble with it. I'll revisit and see if that works. – Josh Mar 22 '12 at 15:06
  • Tried it, no dice. updated my question to include this info. – Josh Mar 26 '12 at 20:32
  • @Josh do you need a relation back too? (i.e. you're linking them both MyEntity -> ExtEntity and ExtEntity -> MyEntity) - I think that's not the problem either, I had that before, but just asking. I have a code somewhere for a very similar thing, quite complex, that should work, will post – NSGaga-mostly-inactive Mar 30 '12 at 21:32
  • @NSGaga I really want it to be a split entity. The table is 1-to-1 as a way of "extending" the main table which is part of a product we can't change, so it is really all part of the same conceptual entity. If I have to go the 1-to-1, then no, I don't think we would require the relation back. Not thinking we ever have an instance where we'd only have the extension but not the main. – Josh Mar 30 '12 at 21:47
  • we have to separate two things here - do you have control over the code / entities (code first/EF)? extending the Db part is ok, I understand that - so i.e. you can map two tables where one is 'yours' in your part of the Db and another one is somewhere else, same Db part of a standard set which you cannot change. The question is whether you have some limitation on the 'C#' side? It doesn't look so to me (I cannot comprehend what that could be :). E.g. I had the example where I 'linked' the aspnet default tables, which I couldn't change to my table, so linking their ID, names with mine. – NSGaga-mostly-inactive Mar 30 '12 at 21:51
  • I really think that's your only chance - I know that required:required can be worked out pretty well now (wasn't the case before, earlier versions) - but your initial idea is not doable I think. I had something like that for a custom ORM/LINQ provider, so there you can do lot of things, but EF/CF constrain you 'a bit'. – NSGaga-mostly-inactive Mar 30 '12 at 21:53
  • 1
    I have complete control over the C# code, and zero control over the database. – Josh Apr 02 '12 at 14:08

8 Answers8

3

I have been working on this very issue for a few days, what I finally did was to set the column name of the Id field within the context of the mapping fragment. This way you can give the Id (or the foreign key dependent on the Id) a different name from the Id of the main table.

this.Map(m =>
    {
        m.Property(p => p.Id).HasColumnName("NotTheSameName");
        m.Properties(e =>
             {
                 e.Id,
                 e.FromAnotherTable
             });
        m.ToTable("ExtendedTable");
    });

If you run and debug this, you would find that it would give you something like what you want:

[e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]
Luke T O'Brien
  • 2,565
  • 3
  • 26
  • 38
2

Move the HasColumnName to within the mapping:

this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
    {
        m.Properties(e => new
             {
                 e.Id,
                 e.Name
             });
             m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
             m.Property(e => e.Name).HasColumnName("MyDatabaseName");

           m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        m.ToTable("MainTable");
    });
this.Map(m =>
    {
        m.Properties(e => new
             {
                 e.Id,
                 e.FromAnotherTable
             });
        m.ToTable("ExtendedTable");
    });
}
Jerther
  • 5,558
  • 8
  • 40
  • 59
  • Nearly, he uses two names for the Id columns, the first mapping fragment has an Id name but not the second. Giving the Id a name within the second mapping fragment with m.Property(p => p.Id).HasColumnName("NotTheSameName"); would solve it :) – Luke T O'Brien Apr 22 '15 at 10:00
2

I can't find anything that specifically states that the name of the column has to be the same in both tables; but neither can I find anything that says it doesn't, or explains how you would map that scenario. Every example I can find has the key with the same name in both tables. It looks to me like this is a hole in the DbContext design.

David Nelson
  • 3,666
  • 1
  • 21
  • 24
1

Looks like it's been fixed in Entity Framework 6. See this issue http://entityframework.codeplex.com/workitem/388

frogcoder
  • 963
  • 1
  • 7
  • 17
1

No Visual Studio here, but try this with the 1-to-1 approach:

this.HasRequired(e => e.ExtendedProperties).HasConstraint((e, m) => e.Id == m.Id);

Update:
Here are some links that might help (could not find a real reference link)

How to declare one to one relationship using Entity Framework 4 Code First (POCO)
Entity Framework 4 CTP 4 Code First: how to work with unconventional primary and foreign key names

Community
  • 1
  • 1
Martin
  • 1,149
  • 7
  • 13
  • I'm not finding any method like .HasConstraint(). Can you point me to the docs or some reference? Thanks! – Josh Mar 29 '12 at 18:26
  • It looks like HasConstraint() was in the CTP but not in the release. Haven't found an equivalent. I'm using the 4.3.1 release. – Josh Mar 29 '12 at 21:04
  • BTW, Entity Splitting would be the preferred solution to this. 1-to-1 is a less desirable alternative. I think EF doesn't like it because it wants to see the PK from each table represented on the other table, whereas this is a FK AND PK to enforce it. – Josh Mar 29 '12 at 21:22
1

And just to provide (as I promised) a 1-to-1 (two entities, two tables) mapping, for what it's worth.
Here is what works for me and should in your case...

public class MainTable
{
    public int ThePrimaryKeyId { get; set; }
    public string Name { get; set; }
}
public class ExtendedTable
{
    public int NotTheSameNameID { get; set; }
    public string AnotherTableColumn { get; set; }
    public MainTable MainEntry { get; set; }
}
public class MainDbContext : DbContext
{
    public DbSet<MainTable> MainEntries { get; set; }
    public DbSet<ExtendedTable> ExtendedEntries { get; set; }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MainTable>()
            .HasKey(x => new { x.ThePrimaryKeyId });

        modelBuilder.Entity<ExtendedTable>()
            .HasKey(x => new { x.NotTheSameNameID });

        // Extended To Main 1 on 1
        modelBuilder.Entity<ExtendedTable>()
            .HasRequired(i => i.MainEntry)
            .WithRequiredDependent();
    }
}

...and a test code something like...

using (var db = new UserDbContext())
{
    foreach (var userid in Enumerable.Range(1, 100))
    {
        var main = new MainTable { Name = "Main" + userid };
        db.MainEntries.Add(main);

        var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, MainEntry = main };
        db.ExtendedEntries.Add(extended);
    }
    int recordsAffected = db.SaveChanges();
    foreach (var main in db.MainEntries)
        Console.WriteLine("{0}, {1}", main.Name, main.ThePrimaryKeyId);
    foreach (var extended in db.ExtendedEntries)
        Console.WriteLine("{0}, {1}, {2}, {3}", extended.AnotherTableColumn, extended.NotTheSameNameID, extended.MainEntry.Name, extended.MainEntry.ThePrimaryKeyId);
}

That creates the following SQL script, tables...

CREATE TABLE [MainTables] (
    [ThePrimaryKeyId] [int] NOT NULL IDENTITY,
    [Name] [nvarchar](4000),
    CONSTRAINT [PK_MainTables] PRIMARY KEY ([ThePrimaryKeyId])
)
CREATE TABLE [ExtendedTables] (
    [NotTheSameNameID] [int] NOT NULL,
    [AnotherTableColumn] [nvarchar](4000),
    CONSTRAINT [PK_ExtendedTables] PRIMARY KEY ([NotTheSameNameID])
)
CREATE INDEX [IX_NotTheSameNameID] ON [ExtendedTables]([NotTheSameNameID])
ALTER TABLE [ExtendedTables] ADD CONSTRAINT [FK_ExtendedTables_MainTables_NotTheSameNameID] FOREIGN KEY ([NotTheSameNameID]) REFERENCES [MainTables] ([ThePrimaryKeyId])

And a note, as per our discussion above...
This ain't the 'splitting' - but
(a) code first IMO doesn't allow anything like that (I tried that first and also modifying the migrations manually but it's 'internally' all based on the expected column names being the same and there seems to be no way around it, for this version of EF at least.
(b) table structure wise - the tables could be made to look exactly what you need (as I said before I used it to relate the existing aspnet membership tables (which I could not change) into my user-table which has an own user-id pointing to outside/aspnet table and id.
True, you cannot make it using one C# model class - but the C# side is much more flexible and if you can control the C# that should give the same effect, to my opinion at least (like in the test, you can access it always through the extended entity, both extended and the main columns and they're always matched 1 to 1 and stay 'in sync'.
Hope this helps some
NOTE: you don't have to worry about the fk id etc. - just always access and add the Main entry via MainEntry, and id-s will be fine.

EDIT:
You could also do the following, to gain the appearance of having to deal with just one class (i.e. sort of a split)

public class ExtendedTable
{
    public int NotTheSameNameID { get; set; }
    public string AnotherTableColumn { get; set; }

    public string Name { get { return MainEntry.Name; } set { MainEntry.Name = value; } }
    // public int MainID { get { return MainEntry.ThePrimaryKeyId; } set { MainEntry.ThePrimaryKeyId = value; } }
    internal MainTable MainEntry { get; set; }

    public ExtendedTable()
    {
        this.MainEntry = new MainTable();
    }
}

...and use it like this...

var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, Name = "Main" + userid };  

...also you can revert the direction of the fk by doing the WithRequiredPrincipal instead of dependent.
(also all references have to be w/o 'virtual' if you have required one-to-one)
(and MainTable can be made 'internal' as it's here, so it's not visible from outside - it cannot be nested as that EF doesn't allow - is treated like NotMapped)
...well, that's the best I could do:)

NSGaga-mostly-inactive
  • 14,052
  • 3
  • 41
  • 51
  • Tried this and it blew up with the error {"Invalid column name 'ExtendedTable_ThePrimaryKeyId'."} – Josh Apr 02 '12 at 19:35
  • I've gone through this some more and I think part of the problem is the entity provider we're using. The default SQL acts as you describe, but this is a DB2 provider from IBM. – Josh Apr 02 '12 at 19:56
  • have you tried 'cleaning' the Db, migrations? I'm using EF 4.3, latest - and I had to enable migrations - each time I usually remove db and migrations from project that redo (look at this post of min for details on that http://stackoverflow.com/questions/9364750/entity-framework-4-3-doesnt-create-database/9745125#9745125). I can send you the working project if needed - just not sure how to do that in SO – NSGaga-mostly-inactive Apr 02 '12 at 19:59
  • that's a new thing (to me, I see you mentioned) - still regardless of the provider - your migration file should look the same, I think - you can check there if you have the columns you should like mine SQL/tables. Migrations are a bit tricky - I'm using SQL CE for this, still MS. – NSGaga-mostly-inactive Apr 02 '12 at 20:02
1

I would like to suggest using some data annotations like this:

MainTable
---------
MainTableId
DatabaseName

ExtendedTable
----------
NotTheSameName
AnotherColumn

public class MainTable
{
 [Key]
 public int MainTableId { get; set; }
 public string DatabaseName { get; set; }

 [InverseProperty("MainTable")]
 public virtual ExtendedTable ExtendedTable { get; set; }
}

public class ExtendedTable
{
 [Key]
 public int NotTheSameName { get; set; }

 public string AnotherColumn { get; set; }

 [ForeignKey("NotTheSameName")]
 public virtual MainTable MainTable { get; set; }
}
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • Thank you for the suggestion, but Data Annotations aren't an option for us in this case. Moreover, Microsoft says that whatever is possible with Data Annotations is doable with the Fluent API, and there are things you can do with the Fluent API that you cannot do with annotations. – Josh Apr 03 '12 at 21:15
  • @Josh - The Fluent API can become tedious and does not read as directly as data annotations in my opinion. Here is the API, but the inverse property definition is rather vague: http://msdn.microsoft.com/en-us/library/hh295843(v=vs.103).aspx. It is too bad you cannot use data annotations, they make using EF a lot easier to read and code. The accepted answer "It looks to me like this is a hole in the DbContext design." is just not true. Using either fluent api or data annotations will accomplish the inverse property mapping. – Travis J Apr 03 '12 at 21:26
  • 1
    I disagree with you that the Fluent API is tedious or less direct. I also disagree that annotations are easier to read. But my opinion doesn't matter in this. Our problem is using the Data Annotations leaks the Entity Framework dependency into the entity objects, which means dependent projects must also have the EF reference. By separating out the entity mapping classes, we have isolated the EF dependency to the project that implements DbContext and consumes the mappings. Moreover, we are mapping the same entities to two different data stores, which require different mappings. – Josh Apr 04 '12 at 03:40
  • If you supply an answer to the asked question, then I will revise the answer. To simply state it is "not true" without demonstrating an answer is useless. – Josh Apr 04 '12 at 03:43
  • 1
    @Josh - You should explicitly state the requirement for a fluent api answer if you wish to only get answers in that form. I demonstrated a technique which shows that EF does support this type of shared data, so to say "It looks to me like this is a hole in the DbContext design." with no supporting link or even an example is the answer here *is* useless to others trying to solve this problem. See this link for a fluent api approach and an argument against data annotations: http://stackoverflow.com/a/5693427/1026459 – Travis J Apr 04 '12 at 07:08
0

I faced this issue, and solved by add Column attribute to match the both column names. [Key] [Column("Id")] public int GroupId { get; set; }