65.9K
CodeProject 正在变化。 阅读更多。
Home

Signum Framework 原理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (24投票s)

2009年3月29日

CPOL

18分钟阅读

viewsIcon

104785

downloadIcon

1128

解释Signum Framework背后的理念,这是一个拥有完整LINQ提供程序的ORM,鼓励采用实体优先的方法。

Signum 框架教程 

引言

Signum Framework 是一个新的开源框架,用于开发以实体为中心的N层应用程序。其核心是 Signum.Engine,一个带有完整 LINQ 提供程序的 ORM,它在客户端-服务器(WPF & WCF)和 Web 应用程序(ASP.Net MVC,仍在开发中)上都能很好地工作。

我开发了这个框架,所以很难保持客观。然而,在这第一篇教程中,我将尝试解释指导我们设计 Signum Framework 的原则、该解决方案的优缺点,以及具体来说,如果您开始使用它,您的开发体验将如何得到改善。

请原谅本文缺乏可运行的源代码。我保证接下来的文章会更具实践性,但在第一篇中,我想专注于理念和设计原则。

数据库优先 vs 实体优先

为什么在2009年的今天,当市面上有如此多针对 .NET 的 ORM 时,还要学习一个新的 ORM 呢?难道所有东西不都已经被发明出来了吗?嗯……不完全是。我们可以将 ORM 分为三大类。

数据库优先
无论是使用存储过程(LLBLGen)、拖放代理类(Linq to Sql)还是通过代码生成(Entity Spaces, SubSonic, LLBLGen Pro),其根本在于你的数据库定义了数据的形态,而实体只是遵循这些规则。

灵活
像 NHibernate 这样的技术允许您在实体和数据库之间建立灵活的映射,但这样一来,您就需要维护三样东西:数据库模式、实体以及它们之间的映射。Castle Project ActiveRecord 通过使用“约定优于配置”原则简化了这种映射,但在映射不同的关系模型时仍然保持了灵活性。

实体优先
很少有 ORM 鼓励纯粹的实体优先方法。Django 是一个例子,但它是在 Python 中工作的,而不是在 .NET 中。

Signum Framework 就属于这一类。

Signum Framework 背后的主要思想是,你先编写实体(用 C#),然后自动生成数据库。这一根本性的改变意味着你将无法使用你现有的遗留数据库。但是,如果你有机会从事一个全新的项目,或者你足够勇敢去改变你的数据库,Signum Framework 将会让你的生活变得更轻松,让我们来看看为什么。

Signum Framework 原理

影响 Signum Framework 设计每一部分的最重要原则是简化可组合性。没有理由让应用程序变得如此不同且难以集成,而这种差异根本上源于数据库。

这就是为什么你可以轻松地为任何 UI 技术(WinForms、WPF、ASP.NET...)购买功能强大的网格和图表控件,但你却找不到用于授权、库存或支付模块的表和实体,因为没有人知道如何将它们与你的数据库集成。

将处理 UI 和数据库的垂直模块集成到你的应用程序中,这个想法究竟有什么根本性的错误?答案是缺乏坚实的基础。

Signum.Entities 提供了一小组基类IdentifiablelEntityEmbeddedEntity…)和原语(MList<T>Lazy<T>自定义特性…)来建模你的业务实体,使其可以轻松地与其他模块集成。就像乐高积木一样:小巧、可重用但一点也不灵活。

由于这些实体只是 C# 类,并且我们遵循实体优先的方法,Signum Framework 能够遵循其他好的原则:

Signum Framework 鼓励

Signum Framework 试图避免

干净简洁的代码,通过暴露static类,并使用ScopePattern、特性(Attributes)等,我们试图创建不言自明且符合预期的代码,消除冗余。

复杂的架构,这些架构会使你的代码库增大300%,更难理解,只是为了保护你免受那10%概率会发生的变化。

不惜一切代价消除冗余,因为只说一次,且仅说一次,是为变化做好准备的最佳方式。

生成你的实体代码,这使得为实体添加行为(如验证)变得困难,让你对应用程序最重要的部分失去控制。

编译时检查的 C#,因此常见操作(添加、删除或重命名字段或类) благодаря重构而变得不那么痛苦。这就是我们使用 LINQ 查询而不是 SQL,并使用强类型反射来配置应用程序的原因。

XML 配置,这纯粹是臃肿、易出错、表达能力不强,并且需要保持同步(在 ORM 映射的情况下)。你是在一个框架上构建解决方案,而不是配置一个框架来成为你的解决方案。

函数式编程,因为 lambdas、对象初始化器和 LINQ 查询,一旦理解,就会使你的代码更短、更易读、更同质化。

命令式编程,通常你不得不退回到命令式编程,但只要有机会使用声明式/函数式编程,你的代码就会更清晰,bug 也更少。

POCO 支持

POCO 是 Plain Old C# Object 的缩写。一个支持 POCO 的 ORM 是指可以处理那些对 ORM 没有任何先验知识的类。这被称为持久化无知(Persistence Ignorance)。这里有解释。

我们知道,POCO 是许多对当前 LINQ 提供程序(而非 LINQ 本身)感到“失望”的人所要求的功能,比如DDD(领域驱动设计)的拥护者。不幸的是,POCO 与 ORM 的任何其他方面都配合得不好。例如,为了避免使用 DataContext 并以简单的方式实现 SOAP 序列化,我们在实体内部嵌入了变更跟踪。我们的实体还通过实现 IDataErrorInfo 集成了对验证的支持,并实现了 INotifyPropertyChanged 以简化数据绑定(例如在 WPF 场景中)。

所以,如果你喜欢 POCO 是因为它能清晰地分离实体和逻辑,那么我们的架构显然提倡这一点:典型的架构通常有两个不同的程序集,实体只需要引用 Signum.Entities,而不需要引用存放数据库相关内容的 Signum.Engine

此外,如果你喜欢 POCO 是因为你的实体干净简单,那么你迟早需要实现 IDataErrorInfoINotifyPropertyChanged,这时混乱就会出现。在 Signum Framework 中,我们通过将所有这些混乱移到基类中来消除它,即使有验证,也能保持你的实体简单。

然而,如果你喜欢 POCO 是因为这样你可以编写一个独立于 ORM 的模型,那么 Signum Framework 在这一点上会让你失望。因为更换 ORM 就像做心脏移植手术一样不常见,而启用纯 POCO 意味着需要在运行时添加大量魔法(例如,与序列化配合不佳的延迟加载),并且需要添加大量外部映射,增加了重复性,所以我们选择不支持 POCO。

但更重要的是,Signum Framework 强制每个 IdentifiableEntity(和 Entity)都必须有一个 Id 和一个 ToStr 字段。这些字段也会在数据库中创建列。所以,Signum Framework 不仅强制你的类继承自某些特定的基类,它也强制你的表这样做。这就是为什么你需要一个新的数据库。从这个意义上说,Signum Framework 对 POCO 的支持比大多数框架都要少。

然而,这些要求是值得的,因为它们实现了极高水平的代码重用。

想想 System.Object:由于每个对象都有 ToStringGetHashCodeGetType,你可以将它们添加到 ListDictionaryPropertyGridComboBox 中……所有这些在像 C++ 这样的语言中要复杂得多。

由于我们的实体有坚实的基础,拥有 IdToStr,我们可以使用 Lazy<T> 以一种简洁而强大的方式解决延迟加载问题,或者使用实体控件来简化用户界面,在典型场景下几乎不需要任何后台代码。这些要求也使得应用程序模块的集成成为可能。这是 Signum Framework 最重要的特性。

模式生成

为了消除重复,数据库模式是根据你的实体自动生成的,使用从实体到表的简单 1-1 映射

  • 每个 IdentifiableEntity 都有自己的表,每个字段都是一个列。
  • EmbeddedEntity 没有表,而是向“宿主实体”的表中添加列。
  • 一个 MList<T> 字段没有列;相反,它会生成一个关系表,其中包含一个指向“宿主实体”ID 的反向引用以及表示 T 所需的列。因此,当 TIdentifiableEntity 时,你可以建立多对多关系;当 TEmbeddedEntity 时,可以建立一对多关系;甚至可以创建值类型的 MList
  • 对于数据库模式而言,使用 Lazy<T>(其中 T 是一个 IdentifiableEntity)与直接使用 T 没有区别。

在应用程序开发的早期阶段,当变更更可能发生时,你可以每次都使用Administrator 类创建一个新的数据库。当应用程序投入生产,你不能丢失数据时,你可以生成一个同步脚本,该脚本将根据你实体模型的变更来修改模式。

验证

Signum.Entities 基类 ModifiableEntityEmbeddedEntityIdentifiableEntity 都继承自它)通过实现 IDataErrorInfo 添加了验证支持

在你的实体中实现验证逻辑有两种方式:

  • 声明式:当只需要一个值时(70%的情况),你只需在属性上放置自定义特性即可。这在其他框架中很难做到,因为属性是自动生成的。
  • 命令式:当自定义逻辑需要多个属性的值来进行验证时,你可以通过重写 IDataErrorInfo 索引器来采用更命令式的方法。

这些验证在保存对象之前进行检查,避免数据库损坏,并且在 WPF 用户界面(ASP.NET MVC 即将推出)中以非常响应的方式进行检查。

在下一版本中,您还可以通过订阅 Signum.Engine 事件(类似于触发器)来使用数据库范围的验证。

变更跟踪与序列化

变更跟踪嵌入在实体内部,因此不依赖于 ORM 的全局上下文,简化了序列化,并使得将实体发送到应用程序的客户端变得容易。

继承支持

由于关系数据库没有继承的概念,ORM 最重要的特性之一就是它处理继承的方式。

对于这样的一个层级结构:

public abstract class PersonDN : Entity
{
   string name;
   //(..)
}

public class SoldierDN : PersonDN
{
   WeaponDN weapon;
   //(..)
}

public class TeacherDN : PersonDN
{
   BookDN book; 
   //(..)
}

NHibernate 中,有三种在数据库中实现继承的方式:

  • 每层次结构一张表(Table-Per-Hierarchy):每个 PersonTeacher 都进入新表。鉴别器(discriminator)用于区分它们。
    PersonDN (Id, ToStr, Discriminator, Name, idWeapon, idBook)
  • 每子类一张表(Table-Per-Subclass):这里我们有三张表,一张包含共同项,两张包含差异项。
    PersonDN(Id, ToStr, Name)
    SoldierDN(idPerson, idWeapon)
    TeacherDN(idPerson, idBook)
  • 每个具体类一张表(Table-Per-Concrete-class):创建两张表,每个非抽象具体类型一张。
    SoldierDN(id, ToStr, Name, idWeapon)
    TeacherDN(id, ToStr, Name, idBook)

当我们在我们的框架中设计继承时,我们只选择了第三种方式,因为它是最简单的。

  • 对于第一和第二种选项,你需要在框架中添加一个“层级概念”,这个概念将三个类包含在一起,并将它们放在同一个表中(TPH)或同一个表层级中(TPS)。
  • 由于接口允许多重继承的某种形式,同一个实体可能潜在地属于不同的层级结构,这对于可组合性来说是一个非常糟糕的情况。
  • 有了层级结构,知道一个实体实际驻留在哪里的算法变得更加复杂。在 TPH(每层次结构一张表)中,很难只对 Soldiers 创建一个外键。在 TPS(每子类一张表)中,同一个类型最终会有两个 ID,一个作为 Person,另一个作为 Teacher/Soldier
  • 你避免了 TPH 的类型不匹配问题(在模型不需要时允许 null)。

为了支持继承场景,我们将责任转移给了外键(FKs)。我们有两种多态外键:

  • ImplementedBy:在这种外键中,你使用一个特性来指定可能出现在那里的具体类,并为每种类型创建一个列。
  • ImplementedByAll:创建两列,ID(没有外键)和 Type(指向全局的 TypeDN 表)。

然而,选择多态外键(Polymorphic FK)方案的主要原因是,所有这些复杂的解决方案只解决了保存实体层次结构的问题,而 PFK 还允许引用“潜在的每一个实体”(使用 ImplementedByAll),这对于像可以附加到任何其他实体的 Notes 和 Documents 这样的通用实体非常有用。PFK 还简化了在运行时覆盖此信息的过程,使得修改不受你控制的实体变得容易,并最终增强了可组合性。

延迟性

每个 ORM 都必须以某种方式处理延迟加载。许多 ORM(例如 Linq to SQL 或 NHibernate)以“透明”的方式实现这一点。但问题是,这样一来,你的模型就与 ORM 之间产生了一个隐藏的依赖关系。

Signum Framework 通过使用 Lazy<T> 来坦诚地处理延迟加载。每当你需要指向一个 Person,但又不想每次都加载它时,你就使用 Lazy<Person>。这个对象只知道它所指向实体的 TypeIdToStr

Lazy<T> 集成在 Signum Framework 的每一层:在 UI 层(EntityControls)、引擎本身(用于检索 Lazy 和将 IdentifiableEntity 转换为 Lazy 的扩展方法)以及 Linq 提供程序中。

Graphs

Signum Framework 在保存前会遍历实体图以查找循环。如果找到循环,它会在第二阶段创建连接,这样你就可以一次性保存复杂的对象图,而无需担心循环问题。

事务

我们的事务模型基于 System.Transaction.TransactionScope,但默认情况下使每个嵌套事务静默执行。这对于使业务逻辑易于组合非常方便。

用户界面(WPF 和 ASP.Net MVC)

Signum Engine 是 Signum Framework 代码中强大的 ORM。但 Signum Framework 不仅仅是一个 ORM,它是一个完整的框架,帮助你在应用程序的每一层构建可组合的应用程序。Signum.Windows 这个程序集从根本上简化了为你的实体构建 WPF UI 的过程。

由于所有实体都实现了 INotifyPropertyChanfeIDataErrorInfo,数据绑定和验证在用户界面上是免费提供的。

此外,由于每个实体都有 IdToStr,我们可以制作实体控件:这些控件可以处理任何类型的实体,因此,当用于特定类型时,它们知道如何检索该类型的实体列表(EntityCombo)、启用自动完成EntityLine)等,而当默认实现足够时,无需编写任何后台代码。如果默认实现不够,则会暴露事件以修改行为。

此外,Signum.Windows 鼓励以声明式的方式编写 UI,使用 UserControls 而不是 Windows。它还提供了一种集中的方式来进行导航安全等……这简化了用户界面的可组合性。

WCF 服务

Signum.Services 是一个非常简单的程序集,它定义了 WPF 客户端应用程序和服务器之间的 WCF 合约。我们在这里再次打破常规,因为我们使用 NetDataContract 而不是 DataContract,这使得双方可以共享类型(而不是共享合约),以便共享实体。

我们希望只在实体中放置一次验证,并且我们的目标场景是您同时控制服务器和客户端应用程序,因此我们拒绝了通用的 SOA 架构,即在每个实体上使用 DataContracts 并在客户端和服务器之间有一个庞大的 WSDL,因为我们不需要双方(客户端和服务器)独立发展。

LINQ 支持

为像 SQL 这样复杂的语言编写一个完整的 LINQ 提供程序并非易事。许多人尝试过,但很少有项目能实现一个合格的提供程序。

市面上的许多 LINQ 提供程序只实现了 LINQ 操作的一小部分子集,有时这没问题,因为翻译的目标语言不支持某些给定的操作。但当目标是 SQL 时,它们通常只是“假装”有一个完整的 Linq 提供程序,而实际上并没有。请注意以下几点:

  • 使用 IEnumerable (Func<…>) 而非 IQueryable (Expression<Func<…>>) 的 ORM LINQ 提供程序:因为如果你试图将其用作 SQL 查询,将会产生巨大的性能损失(你将不得不先检索大量对象)。记住,一个 IEnumerable LINQ 提供程序只不过是一个对 LINQ 友好的 API。
  • 使用 LINQ to SQL 而非编写自己的 LINQ 提供程序的 ORM:因为这样你所有的实体都会被复制(ORM 实体和 LINQ to SQL 实体),你将花费大量时间在两者之间进行转换。
  • 带有不完整 LINQ 提供程序的 ORM:一旦你采用 IQueryable 方法,启用每个 LINQ 操作都是一项艰巨的工作。如果你的 LINQ 提供程序不支持某些操作,你就不得不在这些情况下“回退”到 SQL,从而损失大量生产力。目前,这对于“灵活”的 ORM 来说是一个普遍问题,比如 NHibernate 或 Entity Framework,因为模型和数据库之间的距离越大,制作 LINQ 提供程序就越难。

Frans Bouma 在为 LLBLGen PRO 编写 LINQ 提供程序时也遇到了同样的问题,为了“去芜存菁”,他发布了一项 LINQ 提供程序的测试

Signum Framework 在 Database.Query<T> 方法中公开了 LINQ 提供程序。以下是我们通过考试的情况:

  • 它能做连接(joins)吗?能。
  • 它能处理 GroupJoin 和 DefaultIfEmpty 吗?能。DefaultIfEmpty 的工作方式有些差异。
  • 它是否在所有支持的数据库上都支持 LINQ?SF 只支持 SQL 2005/8,所以,是的。
  • 它能否在 Linq 查询的任何地方处理布尔值,在其他数据库上也行吗?
  • 它能在 C# VB.NET 中进行 Group By 吗?带多个聚合函数?能。
  • 它能处理 let 吗?能。我不觉得 let 真的有问题。它是一个查询理解功能,最终会被翻译成标准的 Select 语句。
  • 它能生成服务器端分页查询吗?在所有支持的数据库上?我们实现了 Take 和 Skip,所以可以。(再次强调,仅限 SQL 2005/8)。
  • 它是否处理查询内的内存中对象构建?以及内存中方法调用?是的。像往常一样,只在“查询的末尾”(而不是在 SQL 的 where 语句中)。
  • 它是否提供了一种灵活的方式将 .NET 方法/属性映射到数据库函数/构造上?是的。我们不鼓励这样做,因为我们遵循实体优先的方法,但你可以使用SQL 兼容性扩展在你的查询中使用这个功能。
  • 它能进行分层获取吗?获取实体?高效地?你可以进行分层获取,但效率不高(每次都会执行一个子查询)。计划在后续版本中实现。 现在已在 Signum Framework 2.0 中支持
  • 它能处理可空类型和隐式转换吗?能。不确定我们所有(Signum Framework、LLBLGen、Linq to SQL 和 Linq to Entities)是否都以完全相同的方式处理,但能。
  • 它是否处理继承场景的类型转换?它能在 LINQ 查询中处理继承类型吗?是的。 ImplementedByImplementedByAll 特性在你的实体模型中生效时,必须使用类型转换。在 Signum Framework 2.0 中,也允许使用 GetType()。   

我们发现这个测试非常全面,指出了一个合格的 LINQ 提供程序为了实用而需要解决的许多复杂问题。我们还想补充一些 Linq to Signum 中可用但在其他 ORM 中不适用的功能。

  • 它支持“点连接”吗?即通过导航关系来进行连接的能力。我们将其实现为外连接而不是内连接。
  • 它支持实体相等性比较吗?通过比较主键来模拟实体上的 == 操作符。在 Signum Framework 中,当使用 ImplementedByImplementedByAll 时,它也能工作。
  • 原生模拟 SQL 函数?典型的 StringDateTimeTimeSpanMath 函数都使用 SQL 函数进行模拟。
  • 它支持 SelectMany 吗?总是支持吗?在所有支持的数据库上都支持吗?对我们来说很容易,因为我们只支持 SQL 2005/8,但当 CROSS APPLY / OUTER APPLY 不可用时就不容易了。

最后,有一些 Linq to Signum 的便捷功能需要提及,但这些功能不适用于其他 ORM,例如 Retriever 集成(当完整实体是结果的一部分时能够检索它们,这很方便,但当然会慢一些),或者通过引用 Lazy<T> 类型的列或在完整实体上调用 ToLazy() 来检索 Lazy 对象。

结论

Signum Framework 是一个与众不同的框架。它使用了微软最新的技术(LINQ、WCF、WPF),但它也足够勇敢地对一些当前“主流”原则说不,比如 POCO、SOA 和复杂的 XML 配置。

和许多框架一样,整个框架试图将开发者从技术细节中解放出来,以便他们可以专注于业务实体和逻辑。但它也为未来的应用程序可组合性(从数据库到用户界面)提供了坚实的基础,其形式是使用极少数的原语来建模业务实体。

在不久的将来,Signum Software 的路线图是发布 Signum.Web(将 Signum.Windows 背后的理念应用于 ASP.NET MVC),并提供一些通用模块来简化常见任务,如授权、操作等。

随着时间的推移,我们期望第三方开发者销售像支付、库存管理、CRM、人力资源、薪资、任务、待办事项列表等业务模块,这些模块可以轻松地集成到他们的应用程序中,而这是使用更灵活的 ORM 不会发生的事情。

谁想成为第一个?

© . All rights reserved.