简体   繁体   中英

Entity Framework Core 2.1.14 DB First Missing Foreign Key on non-primary candidate key of a misconfigured data type

Using Entity Framework Core 2.1.14 and MySQL database first:

The tl;dr

...Can't change the database.

I have two broken tables that have a "foreign key" relationship between them that is implicit, on a false candidate key rather than the primary, and the "foreign key" field is the wrong data type. There is no formal foreign key in the database between these two tables. I'm putting "foreign key" in scare quotes because it is not a foreign key but is expected to behave like one.

The referenced Parent candidate key is of type int. The referencing Child "foreign key" field is of type bigint.

The details:

I am needing to write data into a database and directly match an implicit table relationship between tables which do not maintain a formal foreign key, their relationship exists on a false candidate(it is supposed to be unique but since there are no data integrity controls on this table, it inevitably received duplicates and accepted them) rather than the primary, and the data types got crossed up in the referencing table. I need Entity Framework to write both in the same context.SaveChanges(); operation and manage this relationship properly.

I've already extended the DbContext, I've set up ValueConverters, set up a PrincipalKey , and set up my Navigation Properties. Whenever I attempt a SaveChanges() , I get a mismatch on the data types when I use the Converted CandidateKeyField and the BorkedForeignKey for this relationship.

When I use the PrincipalKey and the BorkedForeignKey I get an error from the context that PrincipalKey is not mapped whenever I go to add a new BorkedParentEntity with BorkedChildEntity entries in its Nav collection.

Here is a representation of what I have done:

public partial class BorkedParentEntity
{
   //PrimaryKeyField is generated in the generated partial to this class; including for context
   //public long PrimaryKeyField{get;set;}
   public ICollection<BorkedChildEntity> ChildrenNavigation {get;set;}

   //CandidateKeyField is generated in the generated partial to this class; including for context
   //public int CandidateKeyField { get; set; }

   //An attempt to bypass the type mismatch since ValueConverter is not being honored
   public long PrincipalKey { get { return (long)PrimaryKeyField; } }
}
public partial class BorkedChildEntity
{
   //BorkedForeignKeyField is generated in the other partial to this class; including for context
   //public long BorkedForeignKeyField {get;set;}

   public BorkedParentEntity ParentNavigation {get;set;}
}
public class myDatabaseContextNavigable : myDatabaseContext
{
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
      base.OnModelCreating(modelBuilder);

      modelBuilder.Entity<BorkedParentEntity>(entity =>
      {
          entity.Property(e=>e.Id).ValueGeneratedOnAdd();

          entity.Ignore(e=>e.PrincipalKey); 

          entity.Property(e => e.CandidateKeyField).HasConversion<long>();

          entity.HasMany(r => r.ChildrenNavigation)
                .WithOne(s => s.ParentNavigation)
                //.HasPrincipalKey(s => s.PrincipalKey) //=>Added this PrincipalKey because my conversion was not being honored at runtime.
                //.HasPrincipalKey(s =>(long)s.CandidateKeyField) //=>Added this PrincipalKey to attempt to resolve the mismatch or at least change the error I was getting
                .HasForeignKey(r => r.BorkedForeignKeyField);
      });

      modelBuilder.Entity<BorkedChildEntity>(entity =>
      {
          entity.Property(e=>e.Id).ValueGeneratedOnAdd();
          
      });
}

Here are the outputs I receive for each of the attempts:

Note that the derived PrincipalKey property was only included in the partial entity class for attempts which made a direct reference to it.

Value Converter on the CandidateKeyField and a ForeignKey

         entity.Property(e => e.CandidateKeyField).HasConversion<long>();

         entity.HasMany(r => r.ChildrenNavigation)
               .WithOne(s => s.ParentNavigation)
               .HasPrincipalKey(s => s.CandidateKeyField)
               .HasForeignKey(r => r.BorkedForeignKeyField);
         ...

         context.BorkedParents.Add(_new_record);//=>throws InvalidOperationException
         context.SaveChanges();//=>never reached

            System.InvalidOperationException: 'The types of the properties specified for the foreign key {'BorkedForeignKeyField'} on entity type 'BorkedChildEntity' do not match the types of the properties in the principal key {'CandidateKeyField'} on entity type 'BorkedParentEntity'.'

Manual Cast of the CandidateKeyField

         entity.HasMany(r => r.ChildrenNavigation)
               .WithOne(s => s.ParentNavigation)
                .HasPrincipalKey(s => (long)s.CandidateKeyField) 
                .HasForeignKey(r => r.BorkedForeignKeyField);

         ...
         context.BorkedParents.Add(_new_record);//=>throws InvalidOperationException
         context.SaveChanges();//=>never reached

            System.InvalidOperationException: 'The types of the properties specified for the foreign key {'BorkedForeignKeyField'} on entity type 'BorkedChildEntity' do not match the types of the properties in the principal key {'CandidateKeyField'} on entity type 'BorkedParentEntity'.'

Addition of PrincipalKey field (without ignore of the property)

         entity.HasMany(r => r.ChildrenNavigation)
                .WithOne(s => s.ParentNavigation)
                .HasPrincipalKey(s => s.PrincipalKey)
                .HasForeignKey(r => r.BorkedForeignKeyField);
         ...
         context.BorkedParents.Add(_new_record); //=>succeeds
         context.SaveChanges();//=>throws DbUpdateException


            Message: Unknown column 'PrincipalKey' in 'field list'

PrincipalKey field (with ignore of the property)

         entity.Ignore(e => e.PrincipalKey);         

         entity.HasMany(r => r.ChildrenNavigation)
                .WithOne(s => s.ParentNavigation)
                .HasPrincipalKey(s => s.PrincipalKey) 
                .HasForeignKey(r => r.BorkedForeignKeyField);

         ...
         context.BorkedParents.Add(_new_record); //=>succeeds
         context.SaveChanges();//=>throws DbUpdateException


            Message: Unknown column 'PrincipalKey' in 'field list'

No PrincipalKey, no ValueConverter

         entity.HasMany(r => r.ChildrenNavigation)
               .WithOne(s => s.ParentNavigation)
               .HasForeignKey(r => r.BorkedForeignKeyField);

         ...
         context.BorkedParents.Add(_new_record); //=>succeeds
         context.SaveChanges();//=>succeeds with bad data

            The BorkedForeignKeyField on the BorkedChildEntity table row has the value of the BorkedParent's Primary Key rather than the CandidateKey it is supposed to have.

I'm fresh out of ideas. Can anyone help match the BorkedChildEntity s bigint BorkedForeignKeyField to the BorkedParentEntity s int CandidateKeyField in a way that puts the CandidateKeyField of BorkedParentEntity into BorkedChildEntity 's BorkedForeignKeyField in the database?

The solution to this compounded problem is an "all of the above approach"++.

The 3 problems have to be treated interdependently.

  1. First, eliminate the PrincipalKey property on the BorkedParentEntity . It is unnecessary and counterproductive.

  2. Implicit foreign key on the Candidate Key is itself resolved with 2 steps:

a) Manually add the appropriate NavigationProperties to partials of the Entity classes. In my case, that sends ChildrenNavigation to the BorkedParentEntity and ParentNavigation to the BorkedChildEntity

   public partial class BorkedParentEntity
   {
      public ICollection<BorkedChildEntity> ChildrenNavigation { get;set; }
   }

   public partial class BorkedChildEntity
   {
      public BorkedParentEntity ParentNavigation {get;set;}
   }

b) manually define the relationship between these tables through these navigation properties.

   public class myDatabaseContextNavigable: myDatabaseContext
   {
      public override void OnModelCreating(ModelBuilder modelBuilder)
      {
         base.OnModelCreating(modelBuilder);
         
         modelBuilder.Entity<BorkedParentEntity>(entity =>
         {
            entity.HasMany(s => s.ChildrenNavigation)
                  .WithOne(r => r.ParentNavigation)
                  .HasPrincipalKey(r => r.CandidateKeyField)
                  .HasForeignKey(s => s.BorkedForeignKeyField);
         });
      }
   }

This addresses the missing relationship and the accommodation to key off of the Candidate.

  1. Fix the column type mismatch

No matter what I did(I think I attempted every available permutation of conversion) I could not get Entity Framework to honor my converted type with the mapped property types being misaligned. My solution to the problem is to add a duplicate implementation of the CandidateKeyField in that Entity's partial class with instructions to anyone encountering a build error on this field to go delete the corresponding generated property in the scaffolded Entity class. The manually entered property is entered with the relationship's expected data type.

   //so `BorkedParentEntity` becomes:
   public partial class BorkedParentEntity
   {
      public ICollection<BorkedChildEntity> ChildrenNavigation { get;set; }
      
      //This is my note. There are many like it, but this one is mine
      //If you have a build error on a duplicated property, delete the other one or you will break stuff
      public long CandidateKeyField { get; set; }
   }

and delete the property of the exact same name from the generated entity type.

The last remaining step is to instruct the database that this runtime long is actually mapped to a database int

Inside OnModelCreating(ModelBuilder modelBuilder) when the BorkedParentEntity is being configured, now add

   entity.Property(e => e.CandidateKeyField).HasConversion<int>();

The entities will now persist as I expected.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM