Entity Framework many-to-many relationship error - c#

I have two classes:
public class Cluster
{
public int Id { get; set; }
public virtual ICollection<Blob> Blobs { get; set; }
}
public class Blob
{
public int Id { get; set; }
public virtual ICollection<Cluster> Clusters { get; set; }
}
public ClusterConfiguration ()
{
this.HasKey(p => p.Id)
.HasRequired(p => p.Frame)
.WithMany(p => p.Clusters)
.HasForeignKey(p => p.FrameId)
.WillCascadeOnDelete(true)
;
this.HasMany(p => p.Blobs)
.WithMany(p => p.Clusters)
;
}
public BlobConfiguration ()
{
this.HasKey(p => p.Id)
.HasRequired(p => p.Frame)
.WithMany(p => p.Blobs)
.HasForeignKey(p => p.FrameId)
.WillCascadeOnDelete(true)
;
this.HasMany(p => p.Clusters)
.WithMany(p => p.Blobs)
;
}
There are references to other tables in these classes but I do not think that is the problem. The error is:
[{"Introducing FOREIGN KEY constraint 'FK_dbo.ClusterBlobs_dbo.Blob_Blob_Id' on table 'ClusterBlobs' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.\r\nCould not create constraint. See previous errors."}].
I'm not quite sure how to tell EF to cascade delete Blobs if Clusters are deleted but not to delete Clusters if Blobs are deleted. Please advise.
UPDATE: Using EF5 by the way.

The multiple cascading delete path is actually in effect if you delete a Frame, not a Cluster or Blob:
Frame is deleted -> Cascades to Clusters -> Cascades to link table
Frame is deleted -> Cascades to Blobs -> Cascades to link table
So, these are the two paths from Frame to the link table.
I would suggest to disable cascading delete for one of the two (or both) relationships from Frame to Cluster or Blob. (Use WillCascadeOnDelete(false) there.) Disabling cascading delete for the link table is not possible on an individual relationship basis. The only way is to disable the convention globally:
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
But this will affect all many-to-many relationships in your model.
I'm not quite sure how to tell EF to cascade delete Blobs if Clusters
are deleted but not to delete Clusters if Blobs are deleted.
This is by the way not possible. There is no cascading delete between Cluster and Blob because from database viewpoint the many-to-many relationship is actually modeled with two one-to-many relationships with the link table in between. Cascading delete only acts on the link table which is the dependent in the relationships. Cluster and Blob are both principals.

I believe that turning off ManyToManyCascadeDeleteConvention globally is not a wise option. Instead, it's better to turn it off only for the concerned table.
This can be achieved through editing the generated migration file, for property cascadeDelete. For example:
AddForeignKey("dbo.ClusterBlobs", "Blob_Id", "dbo.Blob", "Blob_Id", cascadeDelete: false);

Related

Delete cascade a tree in EF core (SQL Server)

I have the following code:
public class A
{
public Guid Id { get; set; }
public List<A> AList { get; set; }
}
Configured with fluent Api:
modelBuilder
.Entity<A>()
.HasMany(x => x.AList)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
Creating a Migration works fine but when doing "Update-Database"
I get the following error:
"Introducing FOREIGN KEY constraint 'FK_A_A_AId' on table 'A' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors."
How do I delete cascade a tree in EF Core then?
You can’t “automatically” cascade delete; you must recursively delete child records in client code or create an INSTEAD OF DELETE trigger in the DB with recursive CTE to query for and delete all child records
SO answer for recursively removing children in client code:
Implementing Cascade Delete in a self referencing table in EF Core 2
SO answer for trigger:
On delete cascade for self-referencing table
Alternatively, implement the trigger logic as a stored procedure and configure EFCore to use that sproc for deletes (not built in functionality like EF6 for MapToStoredProcedures so some effort is required:
EF Core - What is MapToStoredProcedures replacement in EF Core 3.1 or 5
With the way the current model is, the same instance of A can appear multiple times in the hierarchy. This would prevent even a database engine from doing a cascade delete.
An alternative would be (depending on your requirement) to add on a "parent" foreign key property to your entity, like the following:
public class A
{
public Guid Id { get; set; }
public Guid? ParentId { get; set; }
public A Parent { get; set; }
public List<A> Children { get; set; }
}
And in your model builder...
modelBuilder.Entity<A>
.HasKey(x => x.Id);
modelBuilder.Entity<A>
.HasMany(x => x.Children)
.WithOne(x => x.Parent)
.HasForeignKey(x => x.ParentId)
.OnDelete(DeleteBehavior.Cascade);
If you did need the same exact instance of a node in the hierarchy appearing in multiple places in the hierarchy, then some additional modeling would be needed to make it work.

Multiple DataAnnotation ForeignKeys cycles or multiple cascade paths Exception

I'm trying to understand and trust in EF.
I have a database with tables and I'm trying to replicate most of the structure with EF Code-First technology as an exercise to practice their basics.
All my classes have 'Hb' as a prefix in their names.
I want to write the object references on other classes without the 'Hb' on the property name.
I'm follow instructions of this site
The first implementation cause exceptions like:
Introducing FOREIGN KEY constraint
'FK_Common.HbZipcode_Common.HbCountry_CountryId' on table 'HbZipcode'
may cause cycles or multiple cascade paths. Specify ON DELETE NO
ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY
constraints. Could not create constraint or index. See previous
errors.
[Required, ForeignKey("Country")]
public int CountryId { get; set; }
public virtual HbCountry Country { get; set; }
[Required, ForeignKey("State")]
public int StateId { get; set; }
public virtual HbState State { get; set; }
[Required, ForeignKey("City")]
public int CityId { get; set; }
public virtual HbCity City { get; set; }
// This Foreignkey never throw exception
[ForeignKey("Neighborhood")]
public int? NeighborhoodId { get; set; }
public virtual HbNeighborhood Neighborhood { get; set; }
I'm doing something wrong and I cannot see.
If anyone could help me it would be nice.
Edit:
After applying the solution passed by the plushpuffin, everything went as expected.
Here's the code:
var modelConfig = dbModelBuilder.Entity<HbZipcode>();
modelConfig
.HasRequired(zc => zc.Country)
.WithMany(c => c.Zipcodes)
.HasForeignKey(zc => zc.CountryId)
.WillCascadeOnDelete(false);
modelConfig
.HasRequired(zc => zc.State)
.WithMany(s => s.Zipcodes)
.HasForeignKey(zc => zc.StateId)
.WillCascadeOnDelete(false);
modelConfig
.HasOptional(zc => zc.Neighborhood)
.WithMany(n => n.Zipcodes)
.HasForeignKey(zc => zc.NeighborhoodId)
.WillCascadeOnDelete(false);
Delete cascade on HbZipcode now occurs only when HbCity is deleted
What's happening here is that HbZipCode has foreign keys to multiple tables, and you can't create 2+ foreign key constraint with ON CASCADE DELETE that have multiple ways of deleting the same rows from the same table.
It's likely that your entity classes are set up such that when HbZipCode is deleted, it cascades the delete to HbNeighborhood, then HbCity, then HbState, and then HbCountry. If you added another foreign key to HbZipCode with ON CASCADE DELETE pointing to HbCity, deleting an HbZipCode record would result in a direct cascade delete to HbNeighborhood and a direct cascade delete to HbCity, but the HbNeighborhood being deleted would also cause a cascade delete to HbCity.
What you need to do is resolve the multiple cascade delete paths to HbCity and the other entity types by turning off CASCADE DELETE for most of them.
See this MSDN page on fluent configuration.
It's likely that you want something like this:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<HbZipCode>()
.HasRequired(t => t.HbCity)
.WithMany(t => t.HbZipCodes)
.HasForeignKey(t => t.CityId)
.WillCascadeOnDelete(false);
}

How do I model deletion in an EF Core many-to-many relationship?

I've followed the docs for setting up my many-to-many relationship, using a join table that is exposed as an entity.
But the docs don't mention what I should do about deletion.
So for example, a Student has many teachers, and a Teacher has many students. The join entity/table is StudentTeacher.
The join table/entity:
public class StudentTeacher {
public int StudentId { get; set; }
public Student Student { get; set; }
public int TeacherId { get; set; }
public Teacher Teacher { get; set; }
}
The config for the join table/entity:
modelBuilder.Entity<StudentTeacher>()
.HasOne(b => b.Teacher)
.WithMany(b => b.StudentTeachers)
.HasForeignKey(b => b.TeacherId)
.IsRequired()
.OnDelete(/* ... what goes here? ...*/);
modelBuilder.Entity<StudentTeacher>()
.HasOne(b => b.Student)
.WithMany(b => b.StudentTeachers)
.HasForeignKey(b => b.StudentId)
.IsRequired()
.OnDelete(/* ... what goes here? ...*/);
What do I use in OnDelete()? And why?
.OnDelete(/* ... what goes here? ...*/);
You should specify here what DB must do with child records (in StudentTeacher) when parent records (in Student or Teacher) is deleted: delete too (Cascade) or prohibit and throw error (Restrict) if corresponding child record exists. With Restrict you must manually delete child records before deleting parent one.
But only you can decide what action must be applied for each relationship - this is your application, we don't know all requirements to it.
Important: with Cascade, deleting, say, Teacher will affect (delete) only records in StudentTeacher (with corresponding TeacherId), but Students will be keep intact.
Important 2: In MS SQL Server (you didn't wrote what DB engine you are using), you can set only one to Cascade (other should be Restrict), or you will receive error when applying migration (Introducing FOREIGN KEY constraint _some_name_ on table _some_table_ may cause cycles or multiple cascade paths.)
It's seems confusing at first to model the join table, because <=EF6 didn't need it. But it's actually simple.
When deleting a Teacher entity, you need to delete its relationships to all Student entities. When deleting a Student entity, you need to delete its relationships to all Teacher entities.
So join entities must always be CASCADE deleted.

EntityFramework CodeFirst: CASCADE DELETE for same table many-to-many relationship

I have an entry removal problem with the EntityFramework and a many-to-many relationship for the same entity. Consider this simple example:
Entity:
public class UserEntity {
// ...
public virtual Collection<UserEntity> Friends { get; set; }
}
Fluent API Configuration:
modelBuilder.Entity<UserEntity>()
.HasMany(u => u.Friends)
.WithMany()
.Map(m =>
{
m.MapLeftKey("UserId");
m.MapRightKey("FriendId");
m.ToTable("FriendshipRelation");
});
Am I correct, that it is not possible to define the Cascade Delete in Fluent API?
What is the best way to delete a UserEntity, for instance Foo?
It looks for me now, I have to Clear the Foo's Friends Collection, then I have to load all other UserEntities, which contain Foo in Friends, and then remove Foo from each list, before I remove Foo from Users. But it sounds too complicateda.
Is it possible to access the relational table directly, so that I can remove entries like this
// Dummy code
var query = dbCtx.Set("FriendshipRelation").Where(x => x.UserId == Foo.Id || x.FriendId == Foo.Id);
dbCtx.Set("FriendshipRelation").RemoveRange(query);
Thank you!
Update01:
My best solution for this problem for know is just to execute the raw sql statement before I call SaveChanges:
dbCtx.Database.ExecuteSqlCommand(
"delete from dbo.FriendshipRelation where UserId = #id or FriendId = #id",
new SqlParameter("id", Foo.Id));
But the disadvantage of this, is that, if SaveChanges failes for some reason, the FriendshipRelation are already removed and could not be rolled back. Or am I wrong?
Problem 1
The answer is quite simple:
Entity Framework cannot define cascade delete when it doesn't know which properties belong to the relationship.
In addition, in a many:many relationship there is a third table, that is responsible for managing the relationship. This table must have at least 2 FKs. You should configure the cascade delete for each FK, not for the "entire table".
The solution is create the FriendshipRelation entity. Like this:
public class UserFriendship
{
public int UserEntityId { get; set; } // the "maker" of the friendship
public int FriendEntityId { get; set; }´ // the "target" of the friendship
public UserEntity User { get; set; } // the "maker" of the friendship
public UserEntity Friend { get; set; } // the "target" of the friendship
}
Now, you have to change the UserEntity. Instead of a collection of UserEntity, it has a collection of UserFriendship. Like this:
public class UserEntity
{
...
public virtual ICollection<UserFriendship> Friends { get; set; }
}
Let's see the mapping:
modelBuilder.Entity<UserFriendship>()
.HasKey(i => new { i.UserEntityId, i.FriendEntityId });
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.Friends)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(true); //the one
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany()
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(true); //the one
Generated Migration:
CreateTable(
"dbo.UserFriendships",
c => new
{
UserEntityId = c.Int(nullable: false),
FriendEntityId = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.UserEntityId, t.FriendEntityId })
.ForeignKey("dbo.UserEntities", t => t.FriendEntityId, true)
.ForeignKey("dbo.UserEntities", t => t.UserEntityId, true)
.Index(t => t.UserEntityId)
.Index(t => t.FriendEntityId);
To retrieve all user's friends:
var someUser = ctx.UserEntity
.Include(i => i.Friends.Select(x=> x.Friend))
.SingleOrDefault(i => i.UserEntityId == 1);
All of this works fine. However, there is a problem in that mapping (which also happens in your current mapping). Suppose that "I" am a UserEntity:
I made a friend request to John -- John accepted
I made a friend request to Ann -- Ann accepeted
Richard made a friend request to me -- I accepted
When I retrieve my Friends property, it returns "John", "Ann", but not "Richard". Why? because Richard is the "maker" of the relationship not me. The Friends property is bound to only one side of the relationship.
Ok. How can I solve this? Easy! Change your UserEntity class:
public class UserEntity
{
//...
//friend request that I made
public virtual ICollection<UserFriendship> FriendRequestsMade { get; set; }
//friend request that I accepted
public virtual ICollection<UserFriendship> FriendRequestsAccepted { get; set; }
}
Update the Mapping:
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.User)
.WithMany(i => i.FriendRequestsMade)
.HasForeignKey(i => i.UserEntityId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<UserFriendship>()
.HasRequired(i => i.Friend)
.WithMany(i => i.FriendRequestsAccepted)
.HasForeignKey(i => i.FriendEntityId)
.WillCascadeOnDelete(false);
There are no migrations necessary.
To retrieve all user's friends:
var someUser = ctx.UserEntity
.Include(i => i.FriendRequestsMade.Select(x=> x.Friend))
.Include(i => i.FriendRequestsAccepted.Select(x => x.User))
.SingleOrDefault(i => i.UserEntityId == 1);
Problem 2
Yes, you have to iterate the collection and remove all children objects. See my answer in this thread Cleanly updating a hierarchy in Entity Framework
Following my answer, just create a UserFriendship dbset:
public DbSet<UserFriendship> UserFriendships { get; set; }
Now you can retrieve all friends of a specific user id, just delete all of them in one shot, and then remove the user.
Problem 3
Yes, it is possible. You have a UserFriendship dbset now.
Hope it helps!
1) I don't see any straightforward way to control the cascade on the many-to-many relationships using FluentApi.
2) The only available way I can think of to control that is by using the ManyToManyCascadeDeleteConvention, which I guess is enabled by default, at least it is for me. I just checked one of my migrations including a many-to-many relationship and indeed the cascadeDelete: true is there for both keys.
EDIT: Sorry, I just found that the ManyToManyCascadeDeleteConvention does not cover the self-referencing case. This related question's answer says that
You receive this error message because in SQL Server, a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement. For example, the tree of cascading referential actions must only have one path to a particular table on the cascading referential actions tree.
So you end up having to have a custom delete code (like the sql command that you already have) and execute it in a transaction scope.
3) You should not be able to access that table from the context. Usually the table created by a many-to-many relationship is a by-product of the implementation in a relational DBMS and is considered a weak table respective to the related tables, which means that its rows should be cascade-deleted if one of the related entities is removed.
My advice is that, first, check if your migration is setting your table foreign keys to cascade delete. Then, if for some reason you need to restrict the deletion of a record which has related records in the many-to-many relationship, then you just check for it in your transactions.
4) In order to do that, if you really want to (FluentApi enables by default ManyToManyCascadeDeleteConvention), is to enclose the sql command and your SaveChanges in a transaction scope.

EF Code First Declaring Entity relationships 2 fields to the same collection

I am using Entity Framework with Code First. I have a table in my database that stores relationships between users. The table structure looks a lot like this:
RequestID
UserFromID
UserToID
Both UserFromID and UserToID are foreign keys to my User table.
In my User entity I have a virtual property called Relationships setup. I want this property to be a collection of of all RelationshipRequests from the table listed above where the current users UserID is either the UserFromID OR the UserToID.
These are my bindings in the context:
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.Relationships).WithRequired(e => e.UserFrom)
.HasForeignKey(e => e.UserFromID).WillCascadeOnDelete(false);
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.Relationships).WithRequired(e => e.UserTo)
.HasForeignKey(e => e.UserToID).WillCascadeOnDelete(true);
But once the relationships are retrieved for a user, the only relationships in the collection are the one's where the users ID is the UserToID. I have tried swapping them around in which case the collection only contains the relationships where the users ID is the UserFromID. It seems as though the second binding is overriding the first instead of appending to it like I expected. I'm obviously doing this wrong. My question, is there another way to do this binding so that both keys are bound the way I want, or is this something I will have to implement another way?
Thanks!
the problem is that you use twice the same property, and of course your second binding overrides the first. You have to create 2 navigation properties on your user model, one for each foreign key
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.FromRelationships).WithRequired(e => e.UserFrom)
.HasForeignKey(e => e.UserFromID).WillCascadeOnDelete(false);
modelBuilder.Entity<UserProfile>()
.HasMany(e => e.ToRelationships).WithRequired(e => e.UserTo)
.HasForeignKey(e => e.UserToID).WillCascadeOnDelete(true);
And on the model you will have something like this
public virtual List<Relationships> FromRelationships { get; set; }
public virtual List<Relationships> ToRelationships { get; set; }

Categories

Resources