Entity Framework 5 与 Oracle 11g 下的 NHibernate 3.3 对比






4.69/5 (20投票s)
在真实的 .NET 应用程序中使用 Oracle 11g,对比 Entity Framework 5 和 NHibernate 3.3。
引言
在这篇短文中,我想分享我最近关于为 .NET 应用程序选择 ORM 的调查结果,该应用程序使用 Oracle 作为数据库管理系统 (DBMS)。更具体地说,我只考虑了其中两个:Microsoft Entity Framework 5 和 NHibernate 3.3(从现在开始,我将省略版本号)。文章描述了我遇到的具体问题和考虑因素,希望能对读者有所帮助。
背景
我有一个机会开始一个新的中型 .NET 应用程序,该应用程序处理发票领域。客户选择了 Oracle 11g 作为数据库管理系统。发票处理的性能是关键约束之一。当然,ORM 是我必须首先做出的决定之一。在尝试了 Entity Framework 和 NHibernate 几周后,我收集了一份相当有趣的如下所述的问题列表。我想指出的是,我的经验并未涉及诸如基本映射或类层次结构策略之类的基础知识。相反,我专注于我实际遇到的问题,并且我认为这些问题值得与其他开发人员和架构师分享。如果有人发现任何错误或误解,我将不胜感激。
模型
从现在开始,在整篇文章中,我将使用以下简化模型。每个问题都将引用其中的某个点。
public enum CustomerType
{
Private = 0,
Corporate = 1
}
public abstract class Entity
{
public virtual Guid Id { get; set; }
// other common fields
}
public class Customer : Entity
{
public Customer()
{
CustomerComments = new Collection<CustomerComment>();
Invoices = new Collection<Invoice>();
}
public virtual string CustomerNumber { get; set; }
public virtual bool IsActive { get; set; }
public CustomerType CustomerType { get; set; }
public DateTime? StartDate { get; set; }
public virtual ICollection<CustomerComment> CustomerComments { get; set; }
public virtual ICollection<Invoice> Invoices { get; set; }
}
public class Invoice : Entity
{
public virtual Customer Customer { get; set; }
public virtual string Number { get; set; }
}
从 DDD(领域驱动设计)的角度来看,有两个根聚合:Customer
(客户)和 Invoice
(发票),并且 Customer
聚合包含 CostomerComments
(客户评论)作为简单的子集合。
从 DDD 聚合的角度来看,将发票集合包含在 Customer
类中是否合适是可以讨论的,但这并不是关键点,就这样吧。
问题 1. 加载期间的父子关系
在 Entity Framework 和 NHibernate 中处理子集合加载时存在细微差别。假设我们加载一组客户记录(例如,根据最终用户填写的某个过滤器)和一组发票(同样,根据最终用户填写的某个过滤器,例如,上个月的)。这两组都通过单独的调用加载。关键在于,我们确切地知道加载的发票与加载的客户相关联。在 Entity Framework 中,调用如下所示:
List<Customer> customers = Context.Customers.Where(some condition);
Context.Invoices.Where(some condition).Load();
其中 Context
是两次调用的同一个实例,即使这些调用可能(也应该)在不同的存储库中。在 NHibernate 中,相同的调用如下所示:
List<Customer> customers = Session.Query<Customer>().Where(some condition).ToList();
Session.Query<Customer>().Where(some condition).ToList();
同样,Session
实例也是两次调用的同一个实例。
所以,区别在于:
- Entity Framework 将客户与发票进行**双向**连接:客户实体中的发票集合将被填充适当的发票,并且每个发票的客户引用将被填充适当的客户实体。换句话说,对于第二个调用中拥有发票的某个客户,这意味着
customer.Invoices.Count > 0
。 - NHibernate 只进行**单向**连接:客户实体中的发票集合**将不会**被填充适当的发票,而每个发票的客户引用将被填充适当的客户实体。换句话说,这意味着对于所有已加载的客户,
customer.Invoices.Count == 0
,除非我们在代码中修改了集合,导致通过代理加载它。
我应该说,在此示例中,延迟加载模式 (Lazy Load) 是开启的。否则,NHibernate 将在加载客户时加载所有已加载客户下的**所有**发票。
NHibernate 这样做的根本语义思想是聚合体的思想。但在这里我只指出事实本身。此外,如果我们需要 NHibernate 的行为与 Entity Framework 相同,以下是一种可能的解决方案:
- 删除
Customer.Invoices
集合的映射(但保留反向引用映射Invoice.Customer
) - 添加
PostLoadEventListener
,它将自动连接已加载的发票到客户。public class MyPostLoadEventListener : IPostLoadEventListener { public void OnPostLoad(PostLoadEvent @event) { Invoice invoice = @event.Entity as Invoice; if (invoice != null) { invoice.Customer.Invoices.Add(invoice); } } }
问题 2. GUID 主键
Oracle 没有专门的 GUID 字段类型。它使用 RAW(16)
类型。不幸的是,在 Code-First 方法中,无法告诉 Entity Framework 为映射中的主键属性(例如,模型中的 Entity.Id
属性)使用确切的数据库字段类型。Entity Framework 用于 GUID 的默认数据库字段是 RAW(2000)
(!)。避免此问题的唯一方法是切换到 Model-First 方法,在该方法中,可以在 SSDL 文件中将数据库字段类型设置为所需的 RAW(16)
。
<Property Name="ID" Type="raw" Nullable="false" MaxLength="16" />
NHibernate 默认情况下可以很好地处理 GUID 属性。
问题 3. 布尔字段
最令人惊讶的问题之一是,我没有找到任何方法来映射 Entity Framework 中的布尔属性(例如,模型中的 Customer.IsActive
属性)。Oracle 没有存储布尔值的专用类型(如 SQL Server 的 bit 类型),通常使用 NUMBER(1,0)
。但似乎在任何方法中都无法映射布尔值。
NHibernate 默认情况下可以很好地处理布尔属性。
更新:感谢对我的文章的回复,我重新检查了我的调查,并发现我可能犯了一个错误,实际上可以通过配置映射到布尔值。
<oracle.dataaccess.client>
<settings>
<add name="bool" value="edmmapping number(1,0)" />
</settings>
</oracle.dataaccess.client>
问题 4. 枚举属性映射
乍一看,两个 ORM 中的枚举映射看起来都不错(例如,模型中的 Customer.CustomerType
属性)。起初我没有发现任何可疑之处。但随着工作的深入,在分析 Entity Framework 生成的查询后,我注意到了一些奇怪的事情。如果我使用枚举字段进行过滤,即使使用了 CustomerType
字段的索引,我也会遇到严重的性能问题。
List<Customer> customers = Context.Customers
.Where(c => c.CustomerType == CustomerType.Private)
.ToList();
通过分析 Entity Framework 生成的 SQL 查询,我发现它使用 CAST
表达式来处理枚举字段,如下所示:
... WHERE CAST(some_alias.CustomerType as NUMBER(38, 0)) = :plinq1
此外,在嵌套查询的情况下(例如,使用了 .Include()
),SQL 表达式将包含嵌套的 CAST
表达式。... WHERE CAST(CAST(some_alias.CustomerType as NUMBER(38, 0))) = :plinq1
这里的问题是,如果字段被任何函数覆盖,Oracle **不会**使用为 CustomerType
字段创建的索引。为了提高性能,您应该使用基于函数的索引而不是 CAST(CustomerType as NUMBER(38, 0))
。那么嵌套查询呢?嵌套查询会生成嵌套的 CAST
s。CAST(...(CAST(CustomerType as NUMBER(38, 0)))...)
需要为嵌套的 CAST
s 添加额外的索引!
不幸的是,我没有找到任何方法可以移除这个 CAST
,也没有找到方法来覆盖 Entity Framework 中的 SQL 生成,以生成不带 CAST
s 的 SQL 查询。
NHibernate 默认情况下可以很好地处理枚举属性。
问题 5. 数据库级别的 Null 对象模式
由于性能改进调查,我发现另一个细微的问题是数据库中的 NULL
列。事实证明,Oracle 在过滤、连接和排序列是 NOT NULLABLE
时性能最佳。我不会深入解释为什么会这样,因为这不是本文的重点。因此,避免在数据库中将某些字段标记为 NULLABLE
至关重要。
这里的想法是在数据库级别应用 Null 对象模式,通过具有被解释为 NULL
s 的专用值在业务层中。在 ORM 级别执行适当的转换(往返)以在模型中仍然拥有可空字段。在模型类中有两种类型的字段可以设为可空:
- 简单的字段(例如,
Customer.StartDate
) - 引用(例如,
Invoice.Customer
)
因此,我的目标是在数据访问层从数据库加载和保存时将这些转换为预定义值。例如,将 DateTime
值转换为 DateTime.MaxDate
值,并将引用 Invoice.Customer
转换为选定为 Null 客户的特殊客户。
不幸的是,我没有找到任何方法可以使用 Entity Framework 来实现这一点。
NHibernate 可以通过实现 IUserType
接口并将其应用于映射来实现这一点。乍一看,这种实现看起来有点杂乱,但只需要在默认实现中更改几行代码。
问题 5. LINQ 支持
Entity Framework 拥有最强大的 LINQ 支持。尽管 NHibernate 可以通过 Session.Query<>()
使用 LINQ,但它仍然不如 Entity Framework 丰富。另一方面,NHibernate 拥有强大的 QueryOver()
机制,可以解决任何复杂的查询问题。而且,由于 ORM 及其查询通常隐藏在 Repository 和/或 Query 模式之后,因此如果需要,使用混合查询机制不是问题。
问题 6. 可扩展性和灵活性
在可扩展性和灵活性方面,NHibernate 仍然更加成熟。它有很多点可以扩展/覆盖/拦截其功能。令我惊讶的是,Entity Framework 可供扩展/覆盖/拦截其功能的点非常少。
结论
不幸的是,Entity Framework 仅在 SQL Server 上表现最佳。如果您的项目底层使用的是 Oracle DBMS,请毫不犹豫:NHibernate 仍然是赢家。