Table Per Hierarchy策略下,Include()和ThenInclude()方法会抛出“Sequence contains more than one matching element”异常。

4
我正在使用Entity Framework 7和Code-First工作,我的模型涉及三级父子关系:
- “公司”拥有“企业” - “企业”隶属于“公司”,并拥有“工厂” - “工厂”隶属于“企业”
由于这三个实体有许多共同点,它们都继承自抽象的“BaseOrganization”实体。
当我尝试列出所有工厂,包括它们的母公司,然后包括它们的母公司所属的集团时,我有以下两种不同的情况:
  • 不包括 BaseOrganization 到上下文中,使用 Code-First 创建了三个表(对应于具体类型的表继承或 TPC 模式)。Include()ThenInclude() 正常工作,我可以按预期列出工厂并遍历关系。
  • 包括 BaseOrganization 到上下文中,使用 Code-First 创建了带有鉴别器字段的一个表(对应于层次结构表继承或 TPH 模式)。Include()ThenInclude() 抛出一个 Sequence contains more than one matching element 异常。

此问题(没有继承和抽象基类模式)已经在 EF7 Github 存储库中得到解决,并已被清除(请参见 https://github.com/aspnet/EntityFramework/issues/1460)。

所以我目前不知道我的方法是否有问题,或者这明显是EF7 RC1的问题?请注意,我真的更喜欢保留继承,以便我的SQL模型更易读。

以下是完整的复制代码:

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

    namespace MultiLevelTest
    {
        // All places share name and Id
        public abstract class BaseOrganization
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }

        // a corporation (eg : Airbus Group)
        public class Corporation : BaseOrganization
        {
            public virtual ICollection<Company> Companies { get; set; } = new List<Company>();
        }

        // a company (eg : Airbus, Airbus Helicopters, Arianespace)
        public class Company : BaseOrganization
        {
            public virtual Corporation Corporation { get; set; }
            public virtual ICollection<Factory> Factories { get; set; } = new List<Factory>();
        }

        // a factory of a company (Airbus Toulouse, Airbus US...)
        public class Factory : BaseOrganization
        {
            public virtual Company Company { get; set; }
        }

        // setup DbContext
        public class MyContext : DbContext
        {
            // if this line is commented, then code first creates 3 tables instead of one, and everything works fine.
            public DbSet<BaseOrganization> BaseOrganizationCollection { get; set; }
            public DbSet<Corporation> Corporations { get; set; }
            public DbSet<Company> Companies { get; set; }
            public DbSet<Factory> Factories { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(
                    @"Server=(localdb)\mssqllocaldb;Database=MultiLevelTest;Trusted_Connection=True;MultipleActiveResultSets=true");
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                base.OnModelCreating(modelBuilder);

                modelBuilder.Entity<Corporation>().HasMany(c => c.Companies).WithOne(c => c.Corporation);
                modelBuilder.Entity<Company>().HasMany(c => c.Factories).WithOne(c => c.Company);
                modelBuilder.Entity<Factory>().HasOne(f => f.Company);
            }
        }

        public class Program
        {
            public static void Main(string[] args)
            {
                using (var ctx = new MyContext())
                {
                    ctx.Database.EnsureDeleted();
                    ctx.Database.EnsureCreated();

                    // Add a corporation with companies then factories (this works fine)
                    if (!ctx.Corporations.Any()) CreateOrganizationGraph(ctx);

                    // Get all the factories without including anything (this is still working fine)
                    var simpleFactories = ctx.Factories.ToList();
                    foreach(var f in simpleFactories) Console.WriteLine(f.Name);

                    // Get all the factories including their mother company, then their mother corporation
                    var fullFactories = ctx.Factories
                        .Include(f => f.Company)
                        .ThenInclude(c => c.Corporation)
                        .ToList();
                    foreach (var f in fullFactories) Console.WriteLine($"{f.Company.Corporation.Name} > {f.Company.Name} > {f.Name}");
                }
            }

            public static void CreateOrganizationGraph(MyContext ctx)
            {
                var airbusCorp = new Corporation()
                {
                    Name = "Airbus Group",
                    Companies = new List<Company>()
                            {
                                new Company
                                {
                                    Name = "Airbus",
                                    Factories = new List<Factory>()
                                    {
                                        new Factory {Name = "Airbus Toulouse (FR)"},
                                        new Factory {Name = "Airbus Hambourg (DE)"}
                                    }
                                },
                                new Company
                                {
                                    Name = "Airbus Helicopters",
                                    Factories = new List<Factory>()
                                    {
                                        new Factory {Name = "Eurocopter Marignane (FR)"},
                                        new Factory {Name = "Eurocopter Deutschland (DE)"}
                                    }
                                }
                            }
                };

                ctx.Corporations.Add(airbusCorp);
                ctx.SaveChanges();

            }
        }
    }

您需要包含以下 NuGet 包:
"EntityFramework.Commands": "7.0.0-rc1-final",
"EntityFramework.Core": "7.0.0-rc1-final",
"EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final"

更新

正如我在自己的评论中所说,我的第一个解决方法是避免在DbContext中包含基本类型,这样代码优先生成TPC模式的架构(仅当使用TPH策略时才会出现错误)。

问题在于上面的示例比我的实际实现要简单,其中涉及到在基本类型级别定义的多对多关系。

由于EF7尚未支持多对多关系,因此我们必须定义一个链接实体,它将两个一对多关系映射到自己身上。

即使在基本类型级别定义和使用了该映射实体,代码优先仍然选择TPH策略,然后错误仍然会发生。

换句话说,我被卡住了,或者我将不得不将某些逻辑复制三次,这几乎就像故意折断自己的腿!


我认为我的解决方法就是在DbContext中不包括抽象基类型。这将通过允许我使用Include().ThenInclude()来加载关系来解决问题。但我不喜欢这种情况下,Code-First会生成3个不同的表,因为它不反映SQL模式中的继承,更不用说共享字段将在每个表中重复出现的事实了。 - kall2sollies
我也在GitHub存储库上基本上发布了相同的内容,因为我非常确信这是EF7的问题。https://github.com/aspnet/EntityFramework/issues/5033 - kall2sollies
1个回答

1
我认为在你的情况下不应该尝试使用基类。
组织、公司、工厂代表不同的对象,从我看到的内容来看,你正在尝试重构代码而不是抽象对象:
如果你创建一个存储作者和书籍的数据库,作者和书籍都有名称和id,但是否有意义使用基类?
当然,这样做可以节省几行代码,但会使你的代码更难读懂。
我认为只有在真正存在继承关系时才应该使用基类:
例如,你可以有一个基类Person,以及从Person类继承的Manager和Employee类,因为员工和经理都是人。
对我来说,你只需要删除你的基类,它应该按预期工作:
public class Corporation
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Company> Companies { get; set; } = new List<Company>();
}

public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Corporation Corporation { get; set; }
    public List<Factory> Factories { get; set; } = new List<Factory>();
}

public class Factory
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Company Company { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Corporation> Corporations { get; set; }
    public DbSet<Company> Companies { get; set; }
    public DbSet<Factory> Factories { get; set; }

    ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Corporation>().HasMany(c => c.Companies).WithOne(c => c.Corporation);
        modelBuilder.Entity<Company>().HasMany(c => c.Factories).WithOne(c => c.Company);
        modelBuilder.Entity<Factory>().HasOne(f => f.Company);
    }
}

1
谢谢您的回复。我提供的示例只是为了简洁明了。在我的实际实现中,我的类将共享更多内容 - 当然不仅仅是Id和Name(我还有一个基类型,带有接口仅用于Id和Name,它也可以正常工作)。正如我更新中所指出的那样,我的类具有Persons列表,作为多对多关系链接。由于EF7尚不支持这些关系,因此我需要一些逻辑来公开对Persons集合的直接访问,并且该逻辑必须重复使用,除非我将其因式分解到Organization基类中。 - kall2sollies
1
我也认为从功能角度来看,让公司、企业和工厂继承自一个组织是有意义的,因为它们本身就是组织!很多领域可以共享,比如地址、人员、客户等等... - kall2sollies
好的,感谢您的解释。从商业角度来看是有道理的,但您的数据模型应该反映您的数据库。您可以拥有反映此逻辑的域对象。 - Thomas
无论如何,EF7支持多对多的关系(虽然不完美):https://ef.readthedocs.org/en/latest/modeling/relationships.html#many-to-many。这个解决方案可行吗? - Thomas
这是我之前所说的:您可以通过交叉实体来建模多对多的关系,就像在SQL模式中所做的那样。这就是我所做的。希望EF6的其余特性,如此一类的特性,很快就会在EF7中推出。虽然我还没有阅读路线图! - kall2sollies

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接