Оператор DELETE конфликтует с ограничением SAME TABLE REFERENCE в Entity Framework

У меня есть таблица с собственной ссылкой, где ParentId - это FK для идентификатора (PK).
Используя EF (сначала код), я установил свои отношения следующим образом:

this.HasOptional(t => t.ParentValue)
    .WithMany(t => t.ChildValues)
    .HasForeignKey(t => t.ParentId);

Когда я пытаюсь удалить дочерние элементы и их родительский элемент, команды DELETE, которые EF выдают для базы данных, находятся не в том порядке, в котором я ожидал, что они будут работать - сначала он пытается удалить родительскую запись.

Я понимаю, что у меня есть несколько вариантов (ни один из которых мне не нравится):

  1. Сначала удалите дочерние записи, выполните полное сохранение / фиксацию, а затем удалите родительскую запись. Из-за сложности моей модели и поддерживающей ее логики это не вариант - я не могу выдавать несколько команд фиксации, когда захочу.
  2. Прежде чем что-либо удалять, разорвите отношения. Это кажется более разумным решением, но, опять же, я должен выполнить отдельную фиксацию с оператором UPDATE перед операциями DELETE. Я хочу избежать многократных вызовов сохранения / фиксации.
  3. Перед удалением родительской записи используйте триггер для удаления дочерних элементов. Но хотелось бы по возможности избегать триггеров и их проблемного характера.

Итак, вопрос ... есть ли способ принудительно удалить дочерние элементы перед родительской записью? Возможно, мне не хватает какого-то явного способа сообщить EF, что он должен позаботиться об этих дочерних элементах раньше родителя? Может быть, есть способ указать EF на удаление в порядке убывания идентификаторов? Я не знаю .. мысли?


person Kon    schedule 09.05.2013    source источник
comment
Можете ли вы использовать здесь каскадное удаление? Если вы используете первые миграции кода, вы можете установить cascade = true при создании таблицы, в противном случае вам, возможно, придется обновить ее с помощью другой миграции или через базу данных. Я бы подумал, что EF справится с этим, если бы он мог каскадировать.   -  person Brad Gardner    schedule 09.05.2013
comment
Хорошая идея. Наши администраторы баз данных не хотят включать каскад для всей БД, но мы можем сделать это только по этой ссылке FK. Это то, что я только что обсуждал с администратором баз данных. :) Хотя я все еще ищу другие предложения / идеи, возможно, в рамках кода, связанного с EF.   -  person Kon    schedule 09.05.2013
comment
Обновление: невозможно добавить каскад при удалении к FK внутри ссылки на себя в таблице из-за ошибки цикла.   -  person Kon    schedule 10.05.2013
comment
Можете ли вы использовать хранимую процедуру?   -  person Max    schedule 16.05.2013
comment
Что ж, весь смысл ORM, такого как EF (помимо сопоставлений отношений и зависимостей сущностей и всего такого хорошего), заключается в том, что вы имеете дело со своими моделями сущностей и не должны писать свои собственные операторы SQL.   -  person Kon    schedule 16.05.2013


Ответы (3)


Я понимаю, что ответ год назад, но считаю его неполным. На мой взгляд, таблица со ссылками на себя используется для представления произвольной глубины.

Например, рассмотрим следующую структуру:

/*  
 *  earth
 *      europe
 *          germany
 *          ireland
 *              belfast
 *              dublin
 *      south america
 *          brazil
 *              rio de janeiro
 *          chile
 *          argentina                 
 *               
 */

Ответ не решает, как удалить землю или европу из структуры выше.

Я отправляю следующий код в качестве альтернативы (модификация ответа, предоставленная Слаумой, который, кстати, хорошо поработал).

В классе MyContext добавьте следующие методы:

public void DeleteMyEntity(MyEntity entity)
{
    var target = MyEntities
        .Include(x => x.Children)
        .FirstOrDefault(x => x.Id == entity.Id);

    RecursiveDelete(target);

    SaveChanges();

}

private void RecursiveDelete(MyEntity parent)
{
    if (parent.Children != null)
    {
        var children = MyEntities
            .Include(x => x.Children)
            .Where(x => x.ParentId == parent.Id);

        foreach (var child in children)
        {
            RecursiveDelete(child);
        }
    }

    MyEntities.Remove(parent);
}

Я заполняю данные, используя сначала код, следующим классом:

public class TestObjectGraph
{
    public MyEntity RootEntity()
    {
        var root = new MyEntity
        {
            Name = "Earth",
            Children =
                new List<MyEntity>
                    {
                        new MyEntity
                        {
                            Name = "Europe",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity {Name = "Germany"},
                                    new MyEntity
                                    {
                                        Name = "Ireland",
                                        Children =
                                            new List<MyEntity>
                                            {
                                                new MyEntity {Name = "Dublin"},
                                                new MyEntity {Name = "Belfast"}
                                            }
                                    }
                                }
                        },
                        new MyEntity
                        {
                            Name = "South America",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity
                                    {
                                        Name = "Brazil",
                                        Children = new List<MyEntity>
                                        {
                                            new MyEntity {Name = "Rio de Janeiro"}
                                        }
                                    },
                                    new MyEntity {Name = "Chile"},
                                    new MyEntity {Name = "Argentina"}
                                }
                        }
                    }
        };

        return root;
    }
}

который я сохраняю в своей базе данных с помощью следующего кода:

ctx.MyEntities.Add(new TestObjectGraph().RootEntity());

затем вызовите удаление следующим образом:

using (var ctx = new MyContext())
{
    var parent = ctx.MyEntities
        .Include(e => e.Children)
        .FirstOrDefault();

    var deleteme = parent.Children.First();

    ctx.DeleteMyEntity(deleteme);
}

в результате моя база данных теперь имеет такую ​​структуру:

 /*  
 *  earth
 *      south america
 *          brazil
 *              rio de janeiro
 *          chile
 *          argentina                 
 *               
 */

где Европа и все ее дочерние элементы удалены.

выше я указываю первый дочерний элемент корневого узла, чтобы продемонстрировать, что с помощью моего кода вы можете рекурсивно удалить узел и все его дочерние элементы из любой точки иерархии.

если вы хотите протестировать удаление всего, вы можете просто изменить строку следующим образом:

ctx.DeleteMyEntity(parent);

или в зависимости от того, какой узел вы хотите в дереве.

очевидно, я не получу награду, но, надеюсь, мой пост поможет кому-то, кто ищет решение, которое работает для саморегулирующихся сущностей произвольной глубины.

Вот полный источник, который представляет собой модифицированную версию кода Слаумы из выбранного ответа:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace EFSelfReference
{
    public class MyEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public int? ParentId { get; set; }
        public MyEntity Parent { get; set; }

        public ICollection<MyEntity> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<MyEntity>()
                .HasOptional(e => e.Parent)
                .WithMany(e => e.Children)
                .HasForeignKey(e => e.ParentId);
        }


        public void DeleteMyEntity(MyEntity entity)
        {
            var target = MyEntities
                .Include(x => x.Children)
                .FirstOrDefault(x => x.Id == entity.Id);

            RecursiveDelete(target);

            SaveChanges();

        }

        private void RecursiveDelete(MyEntity parent)
        {
            if (parent.Children != null)
            {
                var children = MyEntities
                    .Include(x => x.Children)
                    .Where(x => x.ParentId == parent.Id);

                foreach (var child in children)
                {
                    RecursiveDelete(child);
                }
            }

            MyEntities.Remove(parent);
        }
    }

    public class TestObjectGraph
    {
        public MyEntity RootEntity()
        {
            var root = new MyEntity
            {
                Name = "Earth",
                Children =
                    new List<MyEntity>
                    {
                        new MyEntity
                        {
                            Name = "Europe",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity {Name = "Germany"},
                                    new MyEntity
                                    {
                                        Name = "Ireland",
                                        Children =
                                            new List<MyEntity>
                                            {
                                                new MyEntity {Name = "Dublin"},
                                                new MyEntity {Name = "Belfast"}
                                            }
                                    }
                                }
                        },
                        new MyEntity
                        {
                            Name = "South America",
                            Children =
                                new List<MyEntity>
                                {
                                    new MyEntity
                                    {
                                        Name = "Brazil",
                                        Children = new List<MyEntity>
                                        {
                                            new MyEntity {Name = "Rio de Janeiro"}
                                        }
                                    },
                                    new MyEntity {Name = "Chile"},
                                    new MyEntity {Name = "Argentina"}
                                }
                        }
                    }
            };

            return root;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer<MyContext>(
               new DropCreateDatabaseAlways<MyContext>());
            using (var ctx = new MyContext())
            {
                ctx.Database.Initialize(false);

                ctx.MyEntities.Add(new TestObjectGraph().RootEntity());
                ctx.SaveChanges();
            }

            using (var ctx = new MyContext())
            {
                var parent = ctx.MyEntities
                    .Include(e => e.Children)
                    .FirstOrDefault();

                var deleteme = parent.Children.First();

                ctx.DeleteMyEntity(deleteme);
            }

            Console.WriteLine("Completed....");
            Console.WriteLine("Press any key to exit");
            Console.ReadKey();
        }
    }
}
person geekzster    schedule 06.08.2014

Удаление родителя и ребенка, как показано ниже, действительно работает для меня. Дочерние элементы удаляются перед родительским, и это один обход базы данных (один вызов SaveChanges) с, конечно, тремя операторами DELETE в одной транзакции:

using (var ctx = new MyContext())
{
    var parent = ctx.MyEntities.Include(e => e.Children).FirstOrDefault();

    foreach (var child in parent.Children.ToList())
        ctx.MyEntities.Remove(child);

    ctx.MyEntities.Remove(parent);

    ctx.SaveChanges();
}

(Использование ToList() здесь необходимо, потому что вызов Remove для дочерних элементов также удаляет из родительской коллекции Children. Без использования ToList будет выдано исключение времени выполнения, что коллекция, по которой проходит цикл foreach, была изменена.)

Порядок, в котором Remove вызывается для дочерних и родительских элементов, не имеет значения. Это тоже работает:

using (var ctx = new MyContext())
{
    var parent = ctx.MyEntities.Include(e => e.Children).FirstOrDefault();

    var children = parent.Children.ToList();

    ctx.MyEntities.Remove(parent);

    foreach (var child in children)
        ctx.MyEntities.Remove(child);

    ctx.SaveChanges();
}

EF сортирует операторы DELETE в правильном порядке в обоих случаях.

Полная тестовая программа (EF 5 / .NET 4.5 / SQL Server):

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace EFSelfReference
{
    public class MyEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public int? ParentId { get; set; }
        public MyEntity Parent { get; set; }

        public ICollection<MyEntity> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<MyEntity>()
                .HasOptional(e => e.Parent)
                .WithMany(e => e.Children)
                .HasForeignKey(e => e.ParentId);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer<MyContext>(
                new DropCreateDatabaseAlways<MyContext>());
            using (var ctx = new MyContext())
            {
                ctx.Database.Initialize(false);

                var parent = new MyEntity { Name = "Parent",
                    Children = new List<MyEntity>() };

                parent.Children.Add(new MyEntity { Name = "Child 1" });
                parent.Children.Add(new MyEntity { Name = "Child 2" });

                ctx.MyEntities.Add(parent);

                ctx.SaveChanges();
            }

            using (var ctx = new MyContext())
            {
                var parent = ctx.MyEntities.Include(e => e.Children)
                    .FirstOrDefault();

                foreach (var child in parent.Children.ToList())
                    ctx.MyEntities.Remove(child);

                ctx.MyEntities.Remove(parent);

                ctx.SaveChanges();
            }
        }
    }
}

Снимок экрана после первого using блока с текущим содержимым в таблице БД перед удалением сущностей:

экран 1

Снимок экрана профилировщика SQL после последнего SaveChanges:

экран 2

Т.е. Child 1 (Id = 2) и Child 2 (Id = 3) удаляются до Parent (Id = 1).

person Slauma    schedule 16.07.2013
comment
что, если я не хочу удалять родителя? Я просто хочу удалить ребенка? - person eugenekgn; 11.06.2015

Есть другой способ (подумайте о недостатках, прежде чем делать это ...), вы можете установить связь как ON DELETE CASCADE и попытаться удалить только родительскую строку.

person Aram    schedule 17.07.2013
comment
Вы не можете удалить каскад для ограничения SAME TABLE REFERENCE, как описано в заголовке вопроса. - person Avi Cherry; 23.07.2015