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.
First, eliminate the PrincipalKey
property on the BorkedParentEntity
. It is unnecessary and counterproductive.
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.
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.