Проблемы с использованием TPT (Table Per Type) в EF 4.2 и удалением родительских объектов - PullRequest
10 голосов
/ 22 декабря 2011

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

дополнительная справочная информация ...

У меня есть класс Section, в котором есть номер, заголовок и список страниц. Страница разработана с использованием суперкласса, который содержит основные свойства страницы. У меня есть около 10+ подклассов класса страницы. Класс Section содержит ICollection этих страниц. БД создана правильно, за исключением того, что в подклассовых таблицах нет ON DELETE CASCADE.

Мой код создаст сущности и добавит в БД штрафа. Однако, если я пытаюсь удалить раздел (или все разделы), он не удаляется из-за ограничения FK в моей таблице страниц подкласса ...

public abstract BaseContent 
{
... common properties which are Ignored in the DB ...
}

public class Course : BaseContent
{
    public int Id {get;set;}
    public string Name {get;set;}
    public string Descripiton {get;set;}
    public virtual ICollection<Chapter> Chapters{get;set;}
    ...
}

public class Chapter : BaseContent
{
    public int Id {get;set;}
    public int Number {get;set;}
    public string Title {get;set;}
    public virtual Course MyCourse{get;set;}
    public virtual ICollection<Section> Sections{get;set;}
    ...
}

public class Section : BaseContent
{
    public int Id {get;set;}
    public int Number {get;set;}
    public string Title {get;set;}
    public virtual Chapter MyChapter {get;set;}
    public virtual ICollection<BasePage> Pages {get;set;}
    ...
}

public abstract class BasePage : BaseContent, IComparable
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string PageImageRef { get; set; }
    public ePageImageLocation ImageLocationOnPage { get; set; }
    public int PageNumber { get; set; }
    public virtual Section MySection { get; set; }
    ...
}

public class ChapterPage : BasePage
{
    public virtual int ChapterNumber { get; set; }
    public virtual string ChapterTitle  { get; set; }
    public virtual string AudioRef { get; set; }
}

public class SectionPage : BasePage
{
    public virtual int SectionNumber { get; set; }
    public virtual string SectionTitle  { get; set; }
    public virtual string SectionIntroduction { get; set; }
}

... плюс около 8 других подклассов BasePage ...

public class MyContext: DbContext
{
...
    public DbSet<Course> Courses { get; set; }
    public DbSet<Chapter> Chapters { get; set; }
    public DbSet<Section> Sections { get; set; }
    public DbSet<BasePage> Pages { get; set; }
...
}

.. Свободный API ... (обратите внимание, что для SqlServer схема определяется как "", для Oracle - имя схемы)

private EntityTypeConfiguration<T> configureTablePerType<T>(string tableName) where T : BaseContent
{
    var config = new EntityTypeConfiguration<T>();

    config.ToTable(tableName, Schema);

    // This adds the appropriate Ignore calls on config for the base class BaseContent
    DataAccessUtilityClass.IgnoreAllBaseContentProperties<T>(config);

    return config;
}

public virtual EntityTypeConfiguration<BasePage> ConfigurePageContent()
{
    var config = configureTablePerType<BasePage>("PageContent");

    config.HasKey(pg => pg.Id);
    config.HasRequired(pg => pg.Title);
    config.HasOptional(pg => pg.PageImageRef);

    config.Ignore(pg => pg.ImageLocationOnPage);

    return config;
}

public virtual EntityTypeConfiguration<ChapterPage> ConfigureChapterPage()
{
    var config = configureTablePerType<ChapterPage>("ChapterPage");

    config.HasOptional(pg => pg.AudioRef);
    config.Ignore(pg => pg.ChapterNumber);
    config.Ignore(pg => pg.ChapterTitle);

    return config;
}

public virtual EntityTypeConfiguration<SectionPage> ConfigureSectionPage()
{
    var config = configureTablePerType<SectionPage>("SectionPage");

    config.HasOptional(pg => pg.AudioRef);
    config.Ignore(pg => pg.SectionNumber);
    config.Ignore(pg => pg.SectionTitle);

    return config;
}

... другой код для моделирования других таблиц ...

Таким образом, приложение может заполнять контент и правильно устанавливать отношения. Однако, когда я пытаюсь удалить курс, я получаю сообщение об ошибке, что удаление не удалось из-за ограничения таблицы ChapterPage to PageContent ..

Вот код, который удаляет Курс (фактически я удаляю все курсы) ...

using (MyContext ctx = new MyContext())
{
    ctx.Courses.ToList().ForEach(crs => ctx.Courses.Remove(crs));
    AttachLookupEntities(ctx);
    ctx.SaveChanges();
}

Если я добавлю 'ON DELETE CASCADE' в таблицу ChapterPage и SectionPage для общего ресурса с PageContent, удаление будет завершено.

В итоге

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

Я искренне надеюсь, что что-то неправильно кодировал или забыл некоторые настройки в логике построителя моделей. Потому что, если нет, разработчики EF определили архитектуру (подход к проектированию TPT), которая не может быть использована в любой реальной ситуации без обходного пути. Это наполовину законченное решение. Не поймите меня неправильно, мне нравится проделанная работа, и, как и большинство решений MSFT, она работает для 70% большинства основных приложений. Он просто не готов к более сложным ситуациям.

Я пытался сохранить дизайн БД полностью в рамках API EF, свободный и автономный. Для меня это примерно 98%, просто было бы неплохо, если бы они закончили работу, возможно, в следующем выпуске. По крайней мере, это спасает меня от всех операций CRUD.

Ciao! Джим Шоу

1 Ответ

5 голосов
/ 22 декабря 2011

Я воспроизвел проблему с немного более простым примером:

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

namespace EFTPT
{
    public class Parent
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public ICollection<BasePage> Pages { get; set; }
    }

    public abstract class BasePage
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Parent Parent { get; set; }
    }

    public class DerivedPage : BasePage
    {
        public string DerivedName { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Parent> Parents { get; set; }
        public DbSet<BasePage> BasePages { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Parent>()
                .HasMany(p => p.Pages)
                .WithRequired(p => p.Parent);  // creates casc. delete in DB

            modelBuilder.Entity<BasePage>()
                .ToTable("BasePages");

            modelBuilder.Entity<DerivedPage>()
                .ToTable("DerivedPages");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var ctx = new MyContext())
            {
                var parent = new Parent { Pages = new List<BasePage>() };
                var derivedPage = new DerivedPage();

                parent.Pages.Add(derivedPage);

                ctx.Parents.Add(parent);
                ctx.SaveChanges();
            }

            using (var ctx = new MyContext())
            {
                var parent = ctx.Parents.FirstOrDefault();
                ctx.Parents.Remove(parent);
                ctx.SaveChanges();  // exception here
            }
        }
    }
}

Это дает то же исключение, что и вы.Похоже, только решения:

  • Либо настройте каскадное удаление для ограничения TPT в БД вручную, как вы уже тестировали (либо поместите соответствующую команду SQL в метод Seed).
  • Или загрузите в память объекты, участвующие в наследовании TPT.В моем примере кода:

    var parent = ctx.Parents.Include(p => p.Pages).FirstOrDefault();
    

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

Еще более проблематично, если Parent имеетICollection<DerivedPage> (тогда обратное свойство Parent находится в DerivedPage):

public class Parent
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<DerivedPage> Pages { get; set; }
}

public abstract class BasePage
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class DerivedPage : BasePage
{
    public string DerivedName { get; set; }
    public Parent Parent { get; set; }
}

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

Подобный вопрос и анализ были здесь:http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/3c27d761-4d0a-4704-85f3-8566fa37d14e/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...