Reputation: 120498
I'm refactoring someone else's code involving EF6, code-first. I am trying to reuse database columns so that they are involved in more than one foreign key relationship.
I am aware that by doing this, I am denormalizing my schema, but in so doing, it will allow me to make more direct relationships between models that were missing in the original design. I would like this to happen with minimal changes to the underlying database schema, as there are hand-written sprocs and UDFs that will be affected.
The models below represent the current state of affairs:
A Frame
is comprised of a FrameType
and a Moulding
. Frames are unique, in that no pair of FrameType
or Moulding
is repeated, so we have a composite primary key over FrameType_Id
and Moulding_Id
.
public class Frame
{
[Key,Column(Order = 0)]
[ForeignKey("Moulding")]
public int Moulding_Id { get; set; }
public Moulding Moulding { get; set; }
[Key, Column(Order = 1)]
[ForeignKey("FrameType")]
public int FrameType_Id { get; set; }
public FrameType FrameType { get; set; }
}
where we can assume a minimal implementation for FrameType
and Moulding
public class FrameType
{
public int Id{get; set;}
public ICollection<Frame> Frames{get; set;}
}
public class Moulding
{
public int Id{get; set;}
public ICollection<Frame> Frames{get; set;}
}
Elsewhere we have a Product
which references both FrameType
and Moulding
:
public class Product
{
public int Id{get; set;}
[ForeignKey("Moulding")]
public int Moulding_Id { get; set; }
public Moulding Moulding { get; set; }
[ForeignKey("FrameType")]
public int FrameType_Id { get; set; }
public FrameType FrameType { get; set; }
}
but, critically, does not directly reference Frame
.
I would like to add a property to Frame
:
public ICollection<Product> Products{get; set;}
and on Product
:
public Frame Frame{get; set;}
re-using the Moulding_Id
and FrameType_Id
fields not only as foreign keys to Moulding
and FrameType
but also as a composite foreign key directly to Frame
.
Is this kind of "reuse" of foreign keys possible in EF?
Upvotes: 1
Views: 420
Reputation: 205719
Yes, it's possible. The only EF6 requirements are (1) the referenced entity property to be a PK, and (2) in case of reusing the parts of the FK, the FK properties to be explicitly defined, since there is no way to map 2 shadow properties to one and the same name. Fortunately your sample model satisfies both conditions.
I personally prefer the fluent configuration, because it's more explicit and IMO easier to follow. Or in case you prefer data annotations, decorate the navigation property with the ForeignKey
attribute rather than the inverse, because while bot allow mapping a single FK field, the former is the only way to map the composite FK fields through data annitation.
Applying it to your sample model looks like this:
public class Frame
{
[Key, Column(Order = 0)]
public int Moulding_Id { get; set; }
[Key, Column(Order = 1)]
public int FrameType_Id { get; set; }
[ForeignKey("Moulding_Id")]
public Moulding Moulding { get; set; }
[ForeignKey("FrameType_Id")]
public FrameType FrameType { get; set; }
public ICollection<Product> Products { get; set; }
}
public class Product
{
public int Id { get; set; }
public int Moulding_Id { get; set; }
public int FrameType_Id { get; set; }
[ForeignKey("Moulding_Id")]
public Moulding Moulding { get; set; }
[ForeignKey("FrameType_Id")]
public FrameType FrameType { get; set; }
[ForeignKey("Moulding_Id,FrameType_Id")]
public Frame Frame { get; set; }
}
However, there is a small issue - the above introduces a multiple cascade paths and normally would require you to turn cascade delete off. Which in turn will require fluent configuration, and once you use fluent configuration, there is no need of ForeignKey
data annotation, so remove it from Product.Frame
property and use the following:
modelBuilder.Entity<Product>()
.HasRequired(e => e.Frame)
.WithMany(e => e.Products)
.HasForeignKey(e => new { e.Moulding_Id, e.FrameType_Id })
.WillCascadeOnDelete(false);
Upvotes: 2