Подтвердить что ты не робот

Оператор 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, вызываемые в базу данных, не находятся в том порядке, в котором я ожидал, что они уйдут - сначала он пытается удалить родительскую запись.

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

В классе 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                 
 *               
 */

где Европа и все ее дети удаляются.

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

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

ctx.DeleteMyEntity(parent);

или любой node, который вы хотите в дереве.

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

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

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();
        }
    }
}

Ответ 2

Удаление родительского и дочернего элементов, как показано ниже, работает для меня. Дети удаляются перед родителем и представляют собой одну транзакцию с одной базой данных (один вызов 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 с текущим содержимым в таблице БД перед удалением сущностей:

screen 1

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

screen 2

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

Ответ 3

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