Remove cascade on delete from a single path

asp.net asp.net-mvc c# entity-framework entity-framework-6

Question

Follwing my data models. I stripped all the unimportant annotations to keep it short and clean.

public class Bubble
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Level> Levels { get; set; }
}

public class Level
{
    public int Id { get; set; }
    public int BubbleId { get; set; }
    [ForeignKey("ParentLevel")]
    public int? LevelId { get; set; }

    public string Name { get; set; }

    public Level ParentLevel { get; set; }
    public Bubble Bubble { get; set; }

    public ICollection<Level> Levels { get; set; }
    public ICollection<Item> Items { get; set; }
}

public class Item
{
    public int Id { get; set; }
    public int LevelId { get; set; }

    public string Name { get; set; }

    public Level Level { get; set; }
}

If i create it like that i will get the error:

'Introducing FOREIGN KEY constraint 'FK_dbo.Item_dbo.Level_LevelId' on table 'Item' 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.'

If i add this code the database creation works:

modelBuilder.Entity<Item>()
    .HasRequired(i => i.Level)
    .WithMany(l => l.Items)
    .HasForeignKey(i => i.LevelId)
    .WillCascadeOnDelete(false);

But then i get this error when i delete a bubble:

SqlException: The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.Item_dbo.Level_LevelId". The conflict occurred in database "MvBubbles1", table "dbo.Item", column 'LevelId'. The statement has been terminated.

So i belive the problem is that Level is self referencing. Because i have many one to many relationships and cascade on delete works everywhere except between Level and Item, the only difference is that the parent is not self referencing in the cases that work. I belive i just have to remove one cascade path but at the moment i can't figure out how to do this and where exactly the problem is without disabling cascade on delete out right but i don't want to disable it.

Delete code:

db.Bubbles.Remove(bubble);
db.SaveChanges();
1
1
2/20/2019 1:11:29 PM

Popular Answer

The reason that you can't remove the Bubble while cascading is off, is because you still have Levels with a foreign key to the Bubble you want to remove.

Besides, suppose you want to Remove Bubble 2. Bubble 2 has a top Level 20.
Bubble 3 has a Level 21, which is a sub-level of Level 20.
Bubble 4 has a Level 22, which is a sub-level of Level 21.

If you remove Bubble 2, should it remove the Levels of Bubbles 3 and 4?

Let's assume that your software hasn't got these peculiarities: no circular Level references, and all Levels are from the same Bubble.

You could nullify all LevelIds of all Levels of the Bubble you want to remove before removing the Levels and the Bubbles:

// we want to remove Bubble 2
var levelsToRemove = dbContext.Levels.Where(level => level.BubbleId == 2).ToList();
// nullify all levelIds:
foreach (var levelToRemove in levelsToRemove)
{
    levelToRemove.LevelId = null;
}
// TODO: maybe we need an extra SaveChanges

// Remove the Levels and the Bubble:
dbContext.Levels.RemoveRange(levelsToRemove);

var bubbleToRemove = dbContext.Find(2); // TODO: exception if not found
dbContext.Bubbles.Remove(bubbleToRemove);
dbContext.SaveChanges();

A proper solution

The fact that your code has to prevent circular references and Bubbles with Levels that are sub-levels of Levels from other Bubbles, should give you an inkling that your database isn't normalized enough.

Consider to give your LevelCollection. This would be a one-to-zero-or-one relation, or if all Levels of Bubble 2 have several things in common, consider putting these things into a LevelCollection and let your Bubble have zero or more LevelCollections (one-to-many).

Every LevelCollection belongs to exactly one Bubble. The LevelCollection has zero or more Levels.

Now it is guaranteed that if LevelCollection 42 belongs to Bubble 2, that all Levels of LevelCollection 42 belong to Bubble 2. You can remove the foreign key BubbleId from the Level.

This won't prevent circular Level references, but it will prevent that Levels from one Tree belong to different bubbles

Why cascading had to be switched off

Normally if you have a one-to-many relation like a School with his many Students, when you delete a School you also want to automatically delete all its Students. When cascading is on, then entity framework will first remove all items with a foreign key to the item you request to move before it removes your item.

This can't be done automatically with Bubbles and Levels

Let's Add some Bubbles and levels

                            Id | Name
Add Bubble with name A  =>  1  |  A

                                                  Id | BubbleId | LevelId
Add Level without Parent for Bubble 1         =>  10 |    1     |  null
Add sub Level of Level 10 Parent for Bubble 2 =>  11 |    1     |   10

Now give Level 10 a new LevelId               =>  10 |    1     |   11

Ok, cascading is on, let's remove Bubble 1.

Before entity framework can do that it has to remove everything that has a foreign key to Bubble 1. So we need to remove Level 10 first, which we can't because we have to remove level 11 first. However, Level 11 can't be removed, because Level 10 can't be removed, etc

We have made a small circle, but you can imaging what would happen if you have a circle with 1000 Levels.

Your code will probably prevent that you create a circle, but entity framework can't prevent you to do this.

Another problem: Suppose Bubble 2 has a level 20. Level 20 is a sub level of level 21, and level 21 has a foreign key to Bubble 1. If you remove Bubble 1, what should happen with the Level of Bubble 2? Some people might say: it becomes a top level of Bubble 2, others might say: no, Bubble 2 loses his level. Entity Framework can't detect what you want, so you'll have to do this yourself and switch cascading off.

0
2/20/2019 3:43:13 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow