How to cascade delete in SQL on a table that has an FK pointed to the PK of that same table?

StackOverflow https://stackoverflow.com/questions/19254120

Pregunta

Here's my table - ParentId is a foreign key back to Id. This, as you can tell, is a hierarchy:

enter image description here

Here are my relationships (notice that under INSERT And Update Specification, the Delete and Update Rules are grayed out - this is what is causing my issue):

enter image description here

Here's my data:

enter image description here

And finally, just for visual purposes, here's my output on the UI:

enter image description here

QUESTION: What I want to do is delete "Australia" and have SQL automatically cascade down and delete all "sub-regions" that are chained together by ParentId/Id. I know that I could custom code something to do this, but I want to avoid doing that for sake of time. Why are the Delete and Update Rules grayed out? How can I Cascade Delete on a table that has a foreign key tied to the primary key within the same table?

¿Fue útil?

Solución 2

I ended up creating a custom module that works specifically with adjacency lists. An adjacency list in SQL is where you have a single table maintaining an "n" depth hierarchy by a column ParentId that is a foreign key back to the Id column (the primary key) in the same table.

I created a helper class called "Children". The purpose of this class is to return a list of all traversed child Id's for a given parent. So if you pass in the Id for a parent that's say 6 levels up, the below code will traverse down all 6 levels and return the complete list of Id's for all children, grandchildren, great-grandchildren, and so on.

The reason why I am getting a list of all Id's of children instead of a list of objects to delete is because if I get a list of objects to delete I would have to get these objects under a different DbContext, so when I try to actually delete them, I'll get an error saying that the object is detached (or something like that) because it was instantiated under a different context. Getting a list of Id's prevents this from happening and we can use the same context to perform the delete.

So, call this method GetChildrenIds(List<int> immediateChildrenIds) from your code and pass in a list of ints that correspond to the immediate children of a selected node. For example, to get the list of immediate children to pass into this method, use something like the following (this is based on an ASP.NET MVC pattern using WebAPI):

// keep in mind that `id` is the `id` of the clicked on node.

// DELETE api/region/5
public HttpResponseMessage Delete(int id)
{
     Region region = db.Regions.Find(id);

     List<int> tempRegionIds = new List<int>();
     List<int> immediateChildrenIds = (from i in db.Regions where i.ParentId == id select i.Id).ToList();
     List<int> regionsToDeleteIds = new List<int>();

     // the below is needed because we need to add the Id of the clicked on node to make sure it gets deleted as well
     regionsToDeleteIds.Add(region.Id);

     // we can't make this a static method because that would require static member
     // variables, and static member variables persist throughout each recursion
     HelperClasses.HandleChildren.Children GetChildren = new HelperClasses.HandleChildren.Children();

     // see below this code block for the GetChildrenIds(...) method
     tempRegionIds = GetChildren.GetChildrenIds(immediateChildrenIds);

     // here, we're just adding to regionsToDeleteIds the children of the traversed parent
     foreach (int tempRegionId in tempRegionIds)
     {
         regionsToDeleteIds.Add(tempRegionId);
     }

     // reverse the order so it goes youngest to oldest (child to grandparent)
     // is it necessary?  I don't know honestly.  I just wanted to make sure that
     // the lowest level child got deleted first (the one that didn't have any children)
     regionsToDeleteIds.Reverse(0, regionsToDeleteIds.Count);

     if (regionsToDeleteIds == null)
     {
         return Request.CreateResponse(HttpStatusCode.NotFound);
     }

     foreach (int regionId in regionsToDeleteIds)
     {
         // here we're finding the object based on the passed in Id and deleting it
         Region deleteRegion = db.Regions.Find(regionId);
         db.Regions.Remove(deleteRegion);
     }

     ...

The below code is the class that returns a complete list of children Id's. I put this code in a separate helper class file. The DbContext _db is what I was talking about when I said you don't want to retrieve a list of objects under this context otherwise the Delete wouldn't work when it was actually called in your controller. So instead, as you can see, I get a list of Id's instead and make the actual DbContext call to get the object inside my controller, not this helper class.

public class Children
    {
        private Entities _db = new Entities(HelperClasses.DBHelper.GetConnectionString());
        private List<int> _childrenIds = new List<int>();
        private List<int> _childRegionIds = new List<int>();

        public List<int> GetChildrenIds(List<int> immediateChildrenIds)
        {
            // traverse the immediate children
            foreach (var i in immediateChildrenIds)
            {
                _childRegionIds.Add(i);
                _childrenIds = (from child in _db.Regions where child.ParentId == i select child.Id).ToList();
                if (_childrenIds.Any())
                    GetChildrenIds(_childrenIds);
                else
                    continue;
            }

            return _childRegionIds;
        }
    }

Otros consejos

I'm afraid this is not possible to do in SQL Server. If you try to create your table like this:

create table Region (
    Id int primary key,
    ParentId int references Region(Id) on delete cascade,
    Name nvarchar(50)
)

you'll receive an error: Introducing FOREIGN KEY constraint 'FK__Region__ParentId' on table 'Region' may cause cycles or multiple cascade paths. And, actually, it is possible to create cycles with your schema, like this:

Id    Name     ParentId
 1    USA             2
 2    Germany         1

So in your case I think you have to create on delete trigger.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top