我们有一个基础对象,首先包含10个子对象和EF6代码。
在这10个子对象中,5个只有少数(额外)属性,5个具有多个属性(5到20个)。 我们将其实现为每个类型的表,因此我们有一个基表表和每个孩子1个表(总共10个)。
然而,这会在整个地方创建包含select case
和unions
的巨大选择查询,这也需要生成EF 6秒(第一次)。
我读到了这个问题,同样的问题在每个具体的表格场景中都有。
所以我们剩下的是table-per-hierachy,但这会创建一个包含大量属性的表,这听起来也不是很好。
还有其他解决办法吗?
我想过可能会跳过继承并创建一个联合视图,以便我想从所有子对象/记录中获取所有项目。
还有其他想法吗?
提前致谢。
答案 0 :(得分:6)
另一种解决方案是实现某种CQRS pattern,其中有单独的数据库用于编写(命令)和读取(查询)。您甚至可以对读取数据库中的数据进行反规范化,因此速度非常快。
假设您至少需要一个具有参照完整性的规范化模型,我认为您的决定实际上归结为每个层次的表和每个类型的表。 TPH为reported by Alex James from the EF team,最近on Microsoft's Data Development site表现更佳。
TPT的优势及其为什么不如性能重要:
更大的灵活性,这意味着能够在不影响任何现有表的情况下添加类型。没有太多关注,因为EF迁移使生成所需的SQL以更新现有数据库而不影响数据变得微不足道。
由于可用字段较少而进行数据库验证。由于EF根据应用程序模型验证数据,因此不是一个大问题。如果通过其他方式添加数据,则运行后台脚本来验证数据并不困难。此外,TPT和TPC实际上在主键验证方面更糟糕,因为两个子类表可能包含相同的主键。通过其他方式留下验证问题。
由于不需要存储所有空字段,因此减少了存储空间。这只是一个非常微不足道的问题,特别是如果DBMS有一个很好的策略来处理'稀疏'列。
设计和直觉。拥有一个非常大的表确实感觉有点不对劲,但这可能是因为大多数数据库设计人员花了很多时间来规范数据和绘制ERD。拥有一个大表似乎违背了数据库设计的基本原则。这可能是TPH的最大障碍。 See this article for a particularly impassioned argument
该文章总结了针对TPH的核心论点:
即使在一个微不足道的意义上它也没有被标准化,它使得不可能在数据上强制执行完整性,并且最令人敬畏的是:它几乎可以保证在任何非平凡的数据集中大规模地执行。
这些大多是错误的。上面提到了性能和完整性,TPH并不一定意味着非规范化。只有许多(可空)外键列是自引用的。因此,我们可以像使用TPH一样继续设计和标准化数据。在当前数据库中,我在子类型之间有许多关系,并创建了一个ERD,就像它是TPT继承结构一样。这实际上反映了代码优先实体框架中的实现。例如,这是我的Expenditure
类,它继承自Relationship
继承的Content
:
public class Expenditure : Relationship
{
/// <summary>
/// Inherits from Content: Id, Handle, Description, Parent (is context of expenditure and usually
/// a Project)
/// Inherits from Relationship: Source (the Principal), SourceId, Target (the Supplier), TargetId,
///
/// </summary>
[Required, InverseProperty("Expenditures"), ForeignKey("ProductId")]
public Product Product { get; set; }
public Guid ProductId { get; set; }
public string Unit { get; set; }
public double Qty { get; set; }
public string Currency { get; set; }
public double TotalCost { get; set; }
}
InversePropertyAttribute
和ForeignKeyAttribute
为EF提供了在单个数据库中进行所需自连接所需的信息。
产品类型也映射到同一个表(也继承自Content)。每个产品在表中都有自己的行,包含支出的行将包含ProductId
列中的数据,对于包含所有其他类型的行,该数据为空。因此数据被标准化,只是放在一个表中。
首先使用EF代码的好处是我们以完全相同的方式设计数据库,无论使用TPH还是TPT,我们都以(几乎)完全相同的方式实现它。要将实现从TPH更改为TPT,我们只需要为每个子类添加一个注释,将它们映射到新表。所以,对你来说好消息是你选择哪一个并不重要。只需构建它,生成一堆测试数据,测试它,改变策略,再次测试它。我估计你会发现TPH是胜利者。
答案 1 :(得分:4)
我自己也经历过类似的问题,但我提出了一些建议。我也愿意改进这些建议,因为这是一个复杂的话题,而且我还没有完成所有这些建议。
在处理复杂实体(即具有多级子集合的实体)上的非平凡查询时,实体框架可能会非常慢。在一些性能测试中,我已经尝试过,它确实在那里编写了很长时间来编译查询。理论上,EF 5及其后的版本应该缓存已编译的查询(即使上下文被处理和重新实例化),而您无需做任何事情,但我并不相信这种情况总是如此。
我已经阅读了一些建议,您应该创建多个DataContexts,其中只有较小的数据库实体子集用于复杂数据库。如果这是实用的,你试试看!但我想这种方法会有维护问题。
1)我知道这很明显,但值得一提 - 确保在数据库中为相关实体设置了正确的外键,因为实体框架将跟踪这些关系,并且可以更快地生成查询你需要使用外键加入。
2)不要检索超过你需要的东西。一刀切所有获得复杂对象的方法很少是最优的。假设您正在获取基础对象列表(放入列表中),您只需要在基础对象列表中显示这些对象的名称和ID。只检索基础对象 - 不应检索任何特定需要的导航属性。
3)如果子对象不是集合,或者它们是集合,但是你只需要1个项目(或者聚合值,例如计数),我绝对会在数据库中实现一个View并进行查询。它快得多。 EF不需要做任何工作 - 这一切都在数据库中完成,数据库更适合这种类型的操作。
4)注意.Include(),这会回到上面的#2点。如果您获得单个对象+子集合属性,则最好不要使用.Include(),因为当检索子集合时,这将作为单独的查询完成。 (因此不能获取子集合中每一行的所有基础对象列)
修改强>
以下评论还有一些进一步的想法。
当我们处理继承层次结构时,为继承类的附加属性+基类的表存储单独的表是合乎逻辑的。至于如何使实体框架表现良好,但仍有争议。
我已经将EF用于类似的场景(但更少的孩子),(数据库优先),但在这种情况下,我没有使用实际的Entity框架生成的类作为业务对象。 EF对象与数据库表直接相关。
我为基类和继承类创建了单独的业务类,并创建了一组将转换为它们的Mapper。查询看起来像
public static List<BaseClass> GetAllItems()
{
using (var db = new MyDbEntities())
{
var q1 = db.InheritedClass1.Include("BaseClass").ToList()
.ConvertAll(x => (BaseClass)InheritedClass1Mapper.MapFromContext(x));
var q2 = db.InheritedClass2.Include("BaseClass").ToList()
.ConvertAll(x => (BaseClass)InheritedClass2Mapper.MapFromContext(x));
return q1.Union(q2).ToList();
}
}
不是说这是最好的方法,但它可能是一个起点? 在这种情况下,查询肯定可以快速编译!
欢迎评论!
答案 2 :(得分:3)
使用每个层次结构表,您最终只能使用一个表,因此显然您的CRUD操作会更快,而且无论如何您的域图层都会抽象出这个表。缺点是您失去了NOT NULL约束的能力,因此业务层需要正确处理,以避免潜在的数据完整性。此外,添加或删除实体意味着表更改;但这也是可以管理的。
使用每种类型的表,您遇到的问题是,您拥有的层次结构中的类越多,CRUD操作就越慢。
总而言之,由于性能可能是这里最重要的考虑因素而且您有很多课程,我认为每个层次结构表在性能和简单性方面都是赢家,并且考虑到你的班级数量。
另请参阅this article,更具体地说,请参阅第7.1.1章(在模型优先或代码优先应用程序中避免TPT),其中声明: &#34;在使用时创建应用程序模型优先或代码首先,您应该避免TPT继承以解决性能问题。&#34;
答案 3 :(得分:2)
EF6 CodeFirst模型我正在使用泛型和一个名为“BaseEntity”的抽象基类。我还使用泛型和EntityTypeConfiguration类的基类。
如果我需要在某些表上重用几个属性“columns”,并且它们在BaseEntity或BaseEntityWithMetaData上没有意义,我会为它们创建一个接口。
E.g。我还有一个尚未完成的地址。因此,如果实体具有地址信息,它将实现IAddressInfo。将实体强制转换为IAddressInfo将为我提供一个只包含AddressInfo的对象。
最初我将我的元数据列作为自己的表。但正如其他人所提到的那样,查询是可怕的,而且速度慢而不是慢。所以我想,为什么我不只是使用多个继承路径来支持我想要做的事情,所以列在每个需要它们的表上,而不是在那些不需要的表上。另外我使用的是mysql,其列数限制为4096.Sql Server 2008有1024.即使在1024,我也没有看到在一个表上查看它的真实场景。
我的objjets中的非对象以这样的方式继承,即他们拥有他们不需要的列。当需要出现时,我在一个级别创建一个新的基类,以防止额外的列。
以下是我的代码中的足够片段,以了解我如何设置继承。到目前为止,它对我来说真的很好。我还没有真正制作出一个我无法用这种设置建模的场景。
public BaseEntityConfig<T> : EntityTypeConfiguration<T> where T : BaseEntity<T>, new()
{
}
public BaseEntity<T> where T : BaseEntity<T>, new()
{
//shared properties here
}
public BaseEntityMetaDataConfig : BaseEntityConfig<T> where T: BaseEntityWithMetaData<T>, new()
{
public BaseEntityWithMetaDataConfig()
{
this.HasOptional(e => e.RecCreatedBy).WithMany().HasForeignKey(p => p.RecCreatedByUserId);
this.HasOptional(e => e.RecLastModifiedBy).WithMany().HasForeignKey(p => p.RecLastModifiedByUserId);
}
}
public BaseEntityMetaData<T> : BaseEntity<T> where T: BaseEntityWithMetaData<T>, new()
{
#region Entity Properties
public DateTime? DateRecCreated { get; set; }
public DateTime? DateRecModified { get; set; }
public long? RecCreatedByUserId { get; set; }
public virtual User RecCreatedBy { get; set; }
public virtual User RecLastModifiedBy { get; set; }
public long? RecLastModifiedByUserId { get; set; }
public DateTime? RecDateDeleted { get; set; }
#endregion
}
public PersonConfig()
{
this.ToTable("people");
this.HasKey(e => e.PersonId);
this.HasOptional(e => e.User).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.HasOptional(p => p.Employee).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.HasMany(e => e.EmailAddresses).WithRequired(p => p.Person).WillCascadeOnDelete(true);
this.Property(e => e.FirstName).IsRequired().HasMaxLength(128);
this.Property(e => e.MiddleName).IsOptional().HasMaxLength(128);
this.Property(e => e.LastName).IsRequired().HasMaxLength(128);
}
}
//I Have to use this pattern to allow other classes to inherit from person, they have to inherit from BasePeron<T>
public class Person : BasePerson<Person>
{
//Just a dummy class to expose BasePerson as it is.
}
public class BasePerson<T> : BaseEntityWithMetaData<T> where T: BasePerson<T>, new()
{
#region Entity Properties
public long PersonId { get; set; }
public virtual User User { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public virtual Employee Employee { get; set; }
public virtual ICollection<PersonEmail> EmailAddresses { get; set; }
#endregion
#region Entity Helper Properties
[NotMapped]
public PersonEmail PrimaryPersonalEmail
{
get
{
PersonEmail ret = null;
if (this.EmailAddresses != null)
ret = (from e in this.EmailAddresses where e.EmailAddressType == EmailAddressType.Personal_Primary select e).FirstOrDefault();
return ret;
}
}
[NotMapped]
public PersonEmail PrimaryWorkEmail
{
get
{
PersonEmail ret = null;
if (this.EmailAddresses != null)
ret = (from e in this.EmailAddresses where e.EmailAddressType == EmailAddressType.Work_Primary select e).FirstOrDefault();
return ret;
}
}
private string _DefaultEmailAddress = null;
[NotMapped]
public string DefaultEmailAddress
{
get
{
if (string.IsNullOrEmpty(_DefaultEmailAddress))
{
PersonEmail personalEmail = this.PrimaryPersonalEmail;
if (personalEmail != null && !string.IsNullOrEmpty(personalEmail.EmailAddress))
_DefaultEmailAddress = personalEmail.EmailAddress;
else
{
PersonEmail workEmail = this.PrimaryWorkEmail;
if (workEmail != null && !string.IsNullOrEmpty(workEmail.EmailAddress))
_DefaultEmailAddress = workEmail.EmailAddress;
}
}
return _DefaultEmailAddress;
}
}
#endregion
#region Constructor
static BasePerson()
{
}
public BasePerson()
{
this.User = null;
this.EmailAddresses = new HashSet<PersonEmail>();
}
public BasePerson(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
#endregion
}
现在,ModelCreating上下文中的代码看起来像
//Config
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
//initialize configuration, each line is responsible for telling entity framework how to create relation ships between the different tables in the database.
//Such as Table Names, Foreign Key Contraints, Unique Contraints, all relations etc.
modelBuilder.Configurations.Add(new PersonConfig());
modelBuilder.Configurations.Add(new PersonEmailConfig());
modelBuilder.Configurations.Add(new UserConfig());
modelBuilder.Configurations.Add(new LoginSessionConfig());
modelBuilder.Configurations.Add(new AccountConfig());
modelBuilder.Configurations.Add(new EmployeeConfig());
modelBuilder.Configurations.Add(new ContactConfig());
modelBuilder.Configurations.Add(new ConfigEntryCategoryConfig());
modelBuilder.Configurations.Add(new ConfigEntryConfig());
modelBuilder.Configurations.Add(new SecurityQuestionConfig());
modelBuilder.Configurations.Add(new SecurityQuestionAnswerConfig());
我为我的实体配置创建基类的原因是因为当我开始沿着这条路走下去时,我遇到了一个恼人的问题。我不得不一遍又一遍地为每个被淘汰的类配置共享属性。如果我更新了一个流畅的API映射,我不得不更新每个开源类中的代码。
但是,通过在配置类上使用此继承方法,可以在一个位置配置这两个属性,并由已配置类的配置类继承。
因此,当配置PeopleConfig时,它会在BaseEntityWithMetaData类上运行逻辑以配置两个属性,并在UserConfig运行时再次运行等等。
答案 4 :(得分:2)
尽管每层次表(TPH)是快速CRUD操作的更好方法,但在这种情况下,不可能避免单个表创建数据库的属性如此之多。您提到的case和union子句是创建的,因为生成的查询实际上是在请求包含多个类型的多态结果集。
但是,当EF返回包含所有类型数据的展平表时,它会做额外的工作以确保为可能与特定类型无关的列返回空值。从技术上讲,这种使用case和union的额外验证不是必需的 以下问题是Microsoft EF6中的性能故障,他们的目标是在将来的版本中提供此修复程序。
以下查询:
SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
CASE WHEN (( NOT (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))) AND ( NOT(([UnionAll1].[C4] = 1) AND ([UnionAll1].[C4] IS NOT NULL)))) THEN CAST(NULL ASvarchar(1)) WHEN (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL)) THEN[UnionAll1].[State] END AS [C2],
CASE WHEN (( NOT (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))) AND ( NOT(([UnionAll1].[C4] = 1) AND ([UnionAll1].[C4] IS NOT NULL)))) THEN CAST(NULL ASvarchar(1)) WHEN (([UnionAll1].[C3] = 1) AND ([UnionAll1].[C3] IS NOT NULL))THEN[UnionAll1].[Zip] END AS [C3],
FROM [dbo].[Customers] AS [Extent1]
可以安全地替换为:
SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[Address] AS [Address],
[Extent1].[City] AS [City],
[UnionAll1].[State] AS [C2],
[UnionAll1].[Zip] AS [C3],
FROM [dbo].[Customers] AS [Extent1]
因此,您刚刚看到了Entity Framework 6当前版本的问题和缺陷,您可以选择使用模型优先方法或使用TPH方法。
答案 5 :(得分:1)
三种不同的方法在M.福勒的语言中有不同的名称:
Single Table inheritance
- 整个继承层次结构保存在一个表中。没有连接,子类型的可选列。您需要区分它是哪种子类型。
Concrete Table inheritance
- 每个具体类型都有一个表。连接,没有可选列。在这种情况下,仅当基类型需要具有自己的映射(可以创建实例)时才需要基类型表。
Class Table inheritance
- 您有基本类型表和子表 - 每个表只向基本列添加其他列。连接,没有可选列。在这种情况下,基类型表总是包含每个子项的行;但是,只有在不需要特定于子项的列时才可以检索公共列(其余可能是延迟加载?)。
所有方法都可行 - 它只取决于您拥有的数据量和结构,因此您可以先测量性能差异。
选择将基于联接数与数据分布与可选列的比较。