在EF 4.2中使用TPT (Table Per Type)和删除父对象的问题



从我对几个帖子的理解来看,使用EF的TPT架构在使用共享主键....时不会创建必要的on DELETE级联也有人说EF上下文将处理子类表的删除的适当顺序(但是我确实得到一个错误,它打破了约束,我可以通过在子类表上添加ON DELETE级联来修复它)…

更多背景信息…

我有一个Section类,它有一个数字、标题和一个页面列表。这个页面是用一个拥有基本页面属性的超类来设计的。我有大约10+ page类的子类。Section类保存了这些页面的一个集合。除子类表上没有ON DELETE CASCADE外,DB创建正常。

我的代码将创建实体并添加到DB fine。然而,如果我试图删除一个节(或所有节),由于子类页表上的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();
}

如果我在ChapterPage和SectionPage表中为其与PageContent共享的主表添加'ON DELETE CASCADE',则删除将通过。

总的来说,

我看到的唯一解决方案是手动更改约束,为所有子类页表添加ON DELETE级联。我可以实现的变化,因为我有代码生成的DB脚本的EF表,我需要(我们的整个DB的一个小子集),因为我们不会使用EF创建或实例化的DB(因为它不正确支持迁移到目前为止…)。

我真诚地希望我写错了一些东西,或者忘记了模型构建器逻辑中的一些设置。因为如果没有,EF设计师已经定义了一个架构(TPT设计方法),它不能在任何现实世界的情况下使用,除非有一个hack的解决方案。这是一个半成品。不要误解我的意思,我喜欢已经完成的工作,并且像大多数microsoft解决方案一样,它适用于大多数基本应用程序使用的70%。它只是没有为更复杂的情况做好准备。

我试图保持DB设计全部在EF流畅的API和自包含。对我来说已经有98%了,如果他们能完成这项工作就太好了,也许在下一个版本中。至少它节省了我所有的CRUD操作。

再见!吉姆·肖

我用一个稍微简单一点的例子再现了这个问题:

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/

最新更新