Signum Framework 原理






4.74/5 (24投票s)
解释Signum Framework背后的理念,这是一个拥有完整LINQ提供程序的ORM,鼓励采用实体优先的方法。
Signum 框架教程
- Signum Framework 原则 (SF 1.0)
- Signum Framework 教程第1部分 - Southwind 实体 (SF 2.0)
- Signum Framework 教程第2部分 - Southwind 逻辑 (SF 2.0)
引言
Signum Framework 是一个新的开源框架,用于开发以实体为中心的N层应用程序。其核心是 Signum.Engine,一个带有完整 LINQ 提供程序的 ORM,它在客户端-服务器(WPF & WCF)和 Web 应用程序(ASP.Net MVC,仍在开发中)上都能很好地工作。
我开发了这个框架,所以很难保持客观。然而,在这第一篇教程中,我将尝试解释指导我们设计 Signum Framework 的原则、该解决方案的优缺点,以及具体来说,如果您开始使用它,您的开发体验将如何得到改善。
请原谅本文缺乏可运行的源代码。我保证接下来的文章会更具实践性,但在第一篇中,我想专注于理念和设计原则。
数据库优先 vs 实体优先
为什么在2009年的今天,当市面上有如此多针对 .NET 的 ORM 时,还要学习一个新的 ORM 呢?难道所有东西不都已经被发明出来了吗?嗯……不完全是。我们可以将 ORM 分为三大类。
数据库优先 |
灵活 |
实体优先 Signum Framework 就属于这一类。 |
Signum Framework 背后的主要思想是,你先编写实体(用 C#),然后自动生成数据库。这一根本性的改变意味着你将无法使用你现有的遗留数据库。但是,如果你有机会从事一个全新的项目,或者你足够勇敢去改变你的数据库,Signum Framework 将会让你的生活变得更轻松,让我们来看看为什么。
Signum Framework 原理
影响 Signum Framework 设计每一部分的最重要原则是简化可组合性。没有理由让应用程序变得如此不同且难以集成,而这种差异根本上源于数据库。
这就是为什么你可以轻松地为任何 UI 技术(WinForms、WPF、ASP.NET...)购买功能强大的网格和图表控件,但你却找不到用于授权、库存或支付模块的表和实体,因为没有人知道如何将它们与你的数据库集成。
将处理 UI 和数据库的垂直模块集成到你的应用程序中,这个想法究竟有什么根本性的错误?答案是缺乏坚实的基础。
Signum.Entities 提供了一小组基类(IdentifiablelEntity
、EmbeddedEntity
…)和原语(MList<T>、Lazy<T>、自定义特性…)来建模你的业务实体,使其可以轻松地与其他模块集成。就像乐高积木一样:小巧、可重用但一点也不灵活。
由于这些实体只是 C# 类,并且我们遵循实体优先的方法,Signum Framework 能够遵循其他好的原则:
Signum Framework 鼓励 |
Signum Framework 试图避免 |
干净简洁的代码,通过暴露 |
复杂的架构,这些架构会使你的代码库增大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 是因为你的实体干净简单,那么你迟早需要实现 IDataErrorInfo
或 INotifyPropertyChanged
,这时混乱就会出现。在 Signum Framework 中,我们通过将所有这些混乱移到基类中来消除它,即使有验证,也能保持你的实体简单。
然而,如果你喜欢 POCO 是因为这样你可以编写一个独立于 ORM 的模型,那么 Signum Framework 在这一点上会让你失望。因为更换 ORM 就像做心脏移植手术一样不常见,而启用纯 POCO 意味着需要在运行时添加大量魔法(例如,与序列化配合不佳的延迟加载),并且需要添加大量外部映射,增加了重复性,所以我们选择不支持 POCO。
但更重要的是,Signum Framework 强制每个 IdentifiableEntity
(和 Entity
)都必须有一个 Id
和一个 ToStr
字段。这些字段也会在数据库中创建列。所以,Signum Framework 不仅强制你的类继承自某些特定的基类,它也强制你的表这样做。这就是为什么你需要一个新的数据库。从这个意义上说,Signum Framework 对 POCO 的支持比大多数框架都要少。
然而,这些要求是值得的,因为它们实现了极高水平的代码重用。
想想 System.Object:由于每个对象都有 ToString
、GetHashCode
和 GetType
,你可以将它们添加到 List
、Dictionary
、PropertyGrid
或 ComboBox
中……所有这些在像 C++ 这样的语言中要复杂得多。
由于我们的实体有坚实的基础,拥有 Id
和 ToStr
,我们可以使用 Lazy<T>
以一种简洁而强大的方式解决延迟加载问题,或者使用实体控件来简化用户界面,在典型场景下几乎不需要任何后台代码。这些要求也使得应用程序模块的集成成为可能。这是 Signum Framework 最重要的特性。
模式生成
为了消除重复,数据库模式是根据你的实体自动生成的,使用从实体到表的简单 1-1 映射。
- 每个
IdentifiableEntity
都有自己的表,每个字段都是一个列。 EmbeddedEntity
没有表,而是向“宿主实体”的表中添加列。- 一个
MList<T>
字段没有列;相反,它会生成一个关系表,其中包含一个指向“宿主实体”ID 的反向引用以及表示T
所需的列。因此,当T
是IdentifiableEntity
时,你可以建立多对多关系;当T
是EmbeddedEntity
时,可以建立一对多关系;甚至可以创建值类型的MList
。 - 对于数据库模式而言,使用
Lazy<T>
(其中T
是一个IdentifiableEntity
)与直接使用T
没有区别。
在应用程序开发的早期阶段,当变更更可能发生时,你可以每次都使用Administrator 类创建一个新的数据库。当应用程序投入生产,你不能丢失数据时,你可以生成一个同步脚本,该脚本将根据你实体模型的变更来修改模式。
验证
Signum.Entities
基类 ModifiableEntity
(EmbeddedEntity
和 IdentifiableEntity
都继承自它)通过实现 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):每个
Person
和Teacher
都进入新表。鉴别器(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>
。这个对象只知道它所指向实体的 Type
、Id
和 ToStr
。
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 的过程。
由于所有实体都实现了 INotifyPropertyChanfe
和 IDataErrorInfo
,数据绑定和验证在用户界面上是免费提供的。
此外,由于每个实体都有 Id
和 ToStr
,我们可以制作实体控件:这些控件可以处理任何类型的实体,因此,当用于特定类型时,它们知道如何检索该类型的实体列表(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 查询中处理继承类型吗?是的。 当
ImplementedBy
和ImplementedByAll
特性在你的实体模型中生效时,必须使用类型转换。在 Signum Framework 2.0 中,也允许使用GetType()
。
我们发现这个测试非常全面,指出了一个合格的 LINQ 提供程序为了实用而需要解决的许多复杂问题。我们还想补充一些 Linq to Signum 中可用但在其他 ORM 中不适用的功能。
- 它支持“点连接”吗?即通过导航关系来进行连接的能力。我们将其实现为外连接而不是内连接。
- 它支持实体相等性比较吗?通过比较主键来
模拟实体上的 == 操作符
。在 Signum Framework 中,当使用ImplementedBy
和ImplementedByAll
时,它也能工作。 - 原生模拟 SQL 函数?典型的
String
、DateTime
、TimeSpan
和Math
函数都使用 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 不会发生的事情。
谁想成为第一个?