In code I have an object graph that looks like this:
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Publisher
{
public int PublisherId { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public string Postcode { get; set; }
}
How do I model this in a relational database schema?
This needs to be a 0-1..1 relation, i.e. Authors and Publishers may either have or not have an Address.
Ideally Addresses would have to be referenced by either an Author or a Publisher but not both.
Massive bonus respect if you can model it in Entity Framework Core using navigation properties and have cascade delete remove the Address when an Author or Publisher is removed. (But I'm betting no one will be able to).
I'd list all the things I've tried, but the post would be so long no one would read it. It'll be quicker just to say I've tried everything I can think of.
There are many ways you can achieve the goal in EF Core. The key point is that Address will be the dependent end of the relationships and it will contain optional FKs to the principal entities Author and Publisher.
Here are the possible Address models and configurations:
(1) Address with explicit FK and navigation properties
Model:
public class Address
{
public int AddressId { get; set; }
public string Postcode { get; set; }
public int? AuthorId { get; set; }
public Author Author { get; set; }
public int? PublisherId { get; set; }
public Publisher Publisher { get; set; }
}
Configuration:
modelBuilder.Entity<Author>()
.HasOne(e => e.Address)
.WithOne(e => e.Author)
.HasForeignKey<Address>(e => e.AuthorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Publisher>()
.HasOne(e => e.Address)
.WithOne(e => e.Publisher)
.HasForeignKey<Address>(e => e.PublisherId)
.OnDelete(DeleteBehavior.Cascade);
(2) Address with navigation properties only
Model:
public class Address
{
public int AddressId { get; set; }
public string Postcode { get; set; }
public Author Author { get; set; }
public Publisher Publisher { get; set; }
}
Configuration:
modelBuilder.Entity<Author>()
.HasOne(e => e.Address)
.WithOne(e => e.Author)
.HasForeignKey<Address>("AuthorId")
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Publisher>()
.HasOne(e => e.Address)
.WithOne(e => e.Publisher)
.HasForeignKey<Address>("PublisherId")
.OnDelete(DeleteBehavior.Cascade);
(3) Address with explicit FK properties only
Model:
public class Address
{
public int AddressId { get; set; }
public string Postcode { get; set; }
public int? AuthorId { get; set; }
public int? PublisherId { get; set; }
}
Configuration:
modelBuilder.Entity<Author>()
.HasOne(e => e.Address)
.WithOne()
.HasForeignKey<Address>(e => e.AuthorId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Publisher>()
.HasOne(e => e.Address)
.WithOne()
.HasForeignKey<Address>(e => e.PublisherId)
.OnDelete(DeleteBehavior.Cascade);
(4) Address without explicit FK and navigation properties
Model:
public class Address
{
public int AddressId { get; set; }
public string Postcode { get; set; }
}
Configuration:
modelBuilder.Entity<Author>()
.HasOne(e => e.Address)
.WithOne()
.HasForeignKey<Address>("AuthorId")
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<Publisher>()
.HasOne(e => e.Address)
.WithOne()
.HasForeignKey<Address>("PublisherId")
.OnDelete(DeleteBehavior.Cascade);
Reference: Relationships
Related
I'm creating a project for hospital automation in user authentication and using code-first in Entity Framework.
Here my Hospital entity:
public int Id { get; set; }
public string Name { get; set; }
public string PhoneNumber { get; set; }
Here my Clinic entity:
public int Id { get; set; }
public string Name { get; set; }
And my HospitalAndClinic entity:
public int Id { get; set; }
public int HospitalId { get; set; }
public int ClinicId { get; set; }
[ForeignKey("HospitalId")]
public Hospital Hospital { get; set; }
[ForeignKey("ClinicId")]
public Clinic Clinic { get; set; }
This is the Doctor entity:
public string Branch { get; set; }
public int? HospitalAndClinicId { get; set; }
[ForeignKey("HospitalAndClinicId")]
public HospitalAndClinic HospitalAndClinic { get; set; }
This is my employee entity
public string Position { get; set; }
public int HospitalAndClinicId { get; set; }
[ForeignKey("HospitalAndClinicId")]
public HospitalAndClinic HospitalAndClinic { get; set; }
My Doctorand 'Employee' tables extend from Person class that has fields like id, name etc.
When I do migration I get this problem
Introducing FOREIGN KEY constraint 'FK_Doctor_HospitalAndClinic_HospitalAndClinicId' on table 'Doctor' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
How can I solve this?
In the migration, under constrains you can add onDelete property to say what happens when deleted.
migrationBuilder.CreateTable(
name: "Doctor",
columns: table => new
{
Id = table.Column<int>(nullable: false),
.
.
},
constraints: table =>
{
table.PrimaryKey("PK_Doctor", x => x.Id);
table.ForeignKey(
name: "FK_Doctor_HospitalAndClinic_HospitalAndClinicId",
column: x => x.HospitalAndClinicId,
principalTable: "HospitalAndClinic",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction); // <---- Add this.
});
Or what you can also do is, as Hopeless pointed out, use fluent API to configure your model by overriding the OnModelCreating method in your derived context.
modelBuilder.Entity<Doctor>()
.HasOne(e => e.HospitalAndClinic)
.WithMany()
.OnDelete(DeleteBehavior.NoAction); // <-- Add this
Visit here to see all DeleteBehaviors and visit here to see all ReferentialActions
I have the following entities when I generate migration it creates two columns with name RestrictedCategoryId and RestrictedCategoryId1(FK). How to solve this issue to generate only one column with FK?
Note: I need OrderId in each entity.
`C#
public class Order
{
public Guid Id { get; set; }
public DateTime OrderDate { get; set; }
private List<Category> _categories;
public List<Category> Categories => _categories;
}
public class Category
{
public Guid Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public Guid OrderId { get; set; }
public Order Order { get; set; }
private List<RestrictionCategory> _restrictedCategories;
public List<RestrictionCategory> RestrictedCategories => _restrictedCategories;
}
public class RestrictionCategory
{
public Guid Id { get; set; }
public string Name { get; set; }
public Guid OrderId { get; set; }
public Order Order { get; set; }
public Guid CategoryId { get; set; }
public Category Category { get; set; }
public Guid RestrictedCategoryId { get; set; }
public Category RestrictedCategory { get; set; }
}
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.Id).IsRequired();
}
}
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.HasKey(c => new { c.Id, c.OrderId });
builder.Property(o => o.Id).IsRequired();
builder.Property(o => o.OrderId).IsRequired();
builder.HasMany(c => c.RestrictedCategories).WithOne(cr => cr.Category)
.HasForeignKey(cr => new { cr.CategoryId, cr.OrderId
}).OnDelete(DeleteBehavior.NoAction);
}
}
public class RestrictionCategoryConfiguration : IEntityTypeConfiguration<RestrictionCategory>
{
public void Configure(EntityTypeBuilder<RestrictionCategory> builder)
{
builder.HasKey(c => new { c.Id, c.OrderId });
builder.Property(o => o.Id).IsRequired();
builder.Property(o => o.OrderId).IsRequired();
builder.HasIndex(cr => new { cr.RestrictedCategoryId, cr.OrderId });
}
}
`
The entities resembles to actual ones.
Actually you get two additional columns:
RestrictedCategoryId = table.Column<Guid>(nullable: false),
RestrictedCategoryId1 = table.Column<Guid>(nullable: true), // <--
RestrictedCategoryOrderId = table.Column<Guid>(nullable: true) // <--
Apparently EF Core Foreign Key Conventions doesn't play well with composite keys, so you have to explicitly configure the relationship - similar to what you did for the other relationship, just since your model has no corresponding collection navigation property you have to use HasMany with generic type argument and no parameters, e.g. inside CategoryConfiguration:
builder.HasMany<RestrictionCategory>()
.WithOne(cr => cr.RestrictedCategory)
.HasForeignKey(cr => new { cr.RestrictedCategoryId, cr.OrderId})
.OnDelete(DeleteBehavior.NoAction);
I'm using EF6 over a DB that's over 15 years old. I did not make this architecture decision. All of my experience with EF has been code-first, with models I've created myself.
One of our tables has a reference table that has some info needed in selects only. These mappings will never be used for update/insert/delete.
I have two entities. My primary table:
public class QualParam
{
[Key]
public string MillId { get; set; }
[Key]
public string Qparam { get; set; }
public string ValueNum { get; set; }
public string ActiveFlag { get; set; }
public int ModifiedTimestamp { get; set; }
public int CreatedTimestamp { get; set; }
public decimal SbIncrement { get; set; }
public string QualityDesc { get; set; }
public string TypeCode { get; set; }
public QualParamHeader QualParamHeader { get; set; }
public virtual UnitMeasure UnitMeasure { get; set; }
}
and a reference table:
public class UnitMeasure
{
[Key]
public string UnitOfMeasure { get; set; }
public int ModifiedTimestamp { get; set; }
public int CreatedTimestamp { get; set; }
public string BaseUnits { get; set; }
public string UnitDesc { get; set; }
[Key]
public string TableName { get; set; }
public string RollWeightFlag { get; set; }
public string MetricFlag { get; set; }
public string MxActionCode { get; set; }
[Key]
public string TypeCode { get; set; }
public byte[] RecordVersion { get; set; }
public List<QualParam> QualParams { get; set; }
}
QualParam may have a UnitMeasure, and UnitMeasure can have many QualParams, easy, right?
In SQL the join is done thusly
SELECT *
FROM qual_params AS q
LEFT JOIN unit_measure AS u
ON u.unit_meas = q.unit_meas
AND u.table_name = 'qual_params'
AND u.type_code = q.type_code
So yes, there's a constant, the table name, and yes the keys from the source to the reference tables don't match, and aren't even enumerated in the db to begin with. Again, legacy.
Our Db context-
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new QualParamsConfiguration());
modelBuilder.Configurations.Add(new UnitMeasureConfiguration());
base.OnModelCreating(modelBuilder);
}
I'm using auto generated configs-
public QualParamConfiguration()
: this("dbo")
{
}
public QualParamConfiguration(string schema)
{
ToTable(schema + ".qual_params");
HasKey(x => new { x.MillId, x.Qparam });
Property(x => x.Qparam).HasColumnName(#"qparam").IsRequired().IsFixedLength().IsUnicode(false).HasColumnType("char").HasMaxLength(10).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.None);
Property(x => x.ValueNum).HasColumnName(#"value_num").IsRequired().IsFixedLength().IsUnicode(false).HasColumnType("char").HasMaxLength(9);
Property(x => x.ActiveFlag).HasColumnName(#"active_flag").IsRequired().IsFixedLength().IsUnicode(false).HasColumnType("char").HasMaxLength(1);
Property(x => x.ModifiedTimestamp).HasColumnName(#"ts_modified").IsRequired().HasColumnType("int");
Property(x => x.CreatedTimestamp).HasColumnName(#"ts_create").IsRequired().HasColumnType("int");
Property(x => x.SbIncrement).HasColumnName(#"sb_increment").IsRequired().HasColumnType("decimal").HasPrecision(7, 3);
Property(x => x.QualityDesc).HasColumnName(#"quality_desc").IsRequired().IsFixedLength().IsUnicode(false).HasColumnType("char").HasMaxLength(50);
Property(x => x.MillId).HasColumnName(#"mill_id").IsRequired().IsFixedLength().IsUnicode(false).HasColumnType("char").HasMaxLength(10).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.None);
this.HasRequired(a => a.QparamHeader).WithMany(b => b.QualParams).HasForeignKey(c => c.Qparam).WillCascadeOnDelete(false);
}
The last navigation wrote itself, for obvious reasons. Simple relationship.
This one, not so much. I'm open to everything- I'm fairly aware at this point that I'll have to create a separate model to achieve this.
The examples I've seen using .Map must be from older versions of EF, since many of the methods are not available to me. Is it even possible to achieve this relationship?
I am coding an MVC5 internet application with EF6, and have a question in regards to a foreign key name.
I have a model called MapLocationList that has these two fields:
public int mapLocationListGalleryId { get; set; }
public virtual MapLocationListGallery mapLocationListGallery { get; set; }
When EF creates the table, there is both the following columns:
mapLocationListGalleryId
MapLocationListGallery_Id
Can someone please explain why there are two columns for the MapLocationListGallery foreign key?
Thanks in advance
EDIT
I have changed the name to use an uppercase M, yet the additional column is still there.
Here is my model:
public class MapLocationList : IMapLocationItemWithAssets
{
[Key]
public int Id { get; set; }
[Required]
public string name { get; set; }
public bool enabled { get; set; }
[ScaffoldColumn(false)]
public string mapLocationItemType { get; set; }
[ScaffoldColumn(false)]
public string userName { get; set; }
[ScaffoldColumn(false)]
public DateTime creationDate { get; set; }
[ScaffoldColumn(false)]
public DateTime lastUpdate { get; set; }
public string thumbnailDisplayText { get; set; }
public bool parentIsMapLocation { get; set; }
public int thumbnailAssetId { get; set; }
public virtual Asset thumbnailAsset { get; set; }
public int mapLocationId { get; set; }
public virtual MapLocation mapLocation { get; set; }
public int mapLocationListGalleryId { get; set; }
public virtual MapLocationListGallery mapLocationListGallery { get; set; }
public virtual ICollection<MapLocationListItem> listItems { get; set; }
public MapLocationList()
{
creationDate = DateTime.Now;
lastUpdate = DateTime.Now;
listItems = new List<MapLocationListItem>();
}
}
I also have the following in the OnModelCreating function:
modelBuilder.Entity<MapLocationListGallery>()
.HasRequired(c => c.thumbnailAsset)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<MapLocationList>()
.HasRequired(c => c.thumbnailAsset)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<MapLocationList>()
.HasRequired(c => c.mapLocationListGallery)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<MapLocationListItem>()
.HasRequired(c => c.thumbnailAsset)
.WithMany()
.WillCascadeOnDelete(false);
I use this approach as well and I do not experience this behavior. Probably you need to rename your properties to CamelCase (note the capital M):
public int MapLocationListGalleryId { get; set; }
public virtual MapLocationListGallery MapLocationListGallery { get; set; }
If that doesn't help take a look at the ForeignKeyAttribute here and here.
Edit
I'm not familiar with the fluent api, but I think you could try to set the foreign key explicitly using something like:
modelBuilder.Entity<MapLocationList>()
.HasRequired(c => c.mapLocationListGallery)
.WithMany()
.HasForeignKey(x => x.mapLocationListGalleryId)
.WillCascadeOnDelete(false);
For more info see this article, topic: "Configuring Unconventional Foreign Key Names". Although it's strange this is necessary because your code seems to comply with the Code First convention (with capital M, i.e. the class name).
Look the mapping below.
When I do :
session.Get<Customer>(theId); The result return is the right customer but the Address list is empty. In the database, I see the customer record and the address. The address record in the field Customer_id (generated by NHibernate) is not null and has the right value (id of the customer).
Class and Mapping
public class Customer
{
public virtual int Id { get; set; }
public virtual string LastName { get; set; }
public virtual Iesi.Collections.Generic.ISet<CustomerAddress> Address { get; set; }
public Customer()
{
Address = new Iesi.Collections.Generic.HashedSet<CustomerAddress>();
}
}
public class CustomerAddress
{
public virtual int Id { get; set; }
public virtual string Street { get; set; }
public virtual Customer Customer { get; set; }
}
public class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Id(x => x.Id).GeneratedBy.Native();
Map(x => x.LastName)
.Length(50)
.Not.Nullable();
HasMany(x => x.Address)
.AsSet()
.Inverse()
.Cascade.AllDeleteOrphan();
}
}
public class CustomerAddressMap : ClassMap<CustomerAddress>
{
public CustomerAddressMap()
{
Id(x => x.Id).GeneratedBy.Native();
Map(x => x.Street).Length(50);
References(x => x.Customer);
}
}
Lazy loading is enabled by default, which means you won't retrieve the addresses until you touch the Address property. You can disable lazy loading in your mapping with:
HasMany(x => x.Address)
.AsSet()
.Inverse()
.Cascade.AllDeleteOrphan()
.Not.LazyLoad();