转载

专访实体建模工具创造者Frans Bouma

本文要点:

  • .NET的第三方ORM已经进入利基市场,其中大多数已经被淘汰。如果你比较2008年和现在的ORM列表,你会发现剩下的并没有多少。
  • 在实体内部对变更进行跟踪有很多优点,其中一个是你可以有一个独立的工作对象单元。
  • 每一个ORM开发人员都知道,如果ORM里包含了LINQ Provider,它会带来无尽的麻烦。因为总是会出现各种问题,比如树结构中出现的非预期表达式。
  • 当你必须在一段代码中处理大量数据时,在应用程序代码里使用存储过程会非常有用:你不需要事先把数据从数据库中传输出来。
  • 对于基本的CRUD来说,使用存储过程会带来维护问题,存储过程数量会因此激增。

我们今年的第一次关于.NET的采访对象是 LLBLGen Pro 的Frans Bouma。这个工具的历史几乎与.NET一样长,但是由于其商业产品的特殊性,它并没有其他一些免费工具那样知名。

InfoQ:LLBLGen Pro不仅仅是一个ORM,它是一个“实体建模解决方案”。这应该怎么理解?

Frans Bouma:一开始LLBLGen Pro由两部分组成:LLBLGen Pro Designer和LLBLGen Pro运行时框架。你在Designer中对(抽象)实体模型进行建模,将模型转换两部分,一部分是源代码,另一部分是关系数据库schema。就像其它ORM框架一样,LLBLGen Pro运行时框架在实体模型源代码和关系数据库schema之间进行转化工作:将实体模型实例转化成数据库实例。

这两个部分一起组成一个方便的系统,借助它可以将抽象实体模型转化成可运行的源代码,用于访问关系数据库。

InfoQ:为什么你当初在设计LLBLGen Pro的时候不仅使用它自带的ORM,而且支持其他的ORM,例如EF和NHibernate?

Frans Bouma:这里面确实有许多原因。我们的ORM,也就是LLBLGen Pro运行时框架,它并不是一个POCO框架,它的实体类继承了在运行时定义的基类。虽然这有很多优点,不过有些人想要使用POCO类,他们不希望他们的实体类继承不属于他们的基类。因此,他们不会使用我们的ORM。通过为其他ORM开放我们的设计器,这些框架的用户现在可以使用我们的设计器来对他们的实体模型建模,而不是手工完成。如果我们将设计器限制在我们自己的ORM,我们会失去这些客户。

另一个原因在于,对于使用NHibernate和Entity Framework的用户,没有真正好用的设计器工具,而这些ORM的许多用户都想使用设计器。通过在设计器中为这些框架提供支持,这些用户可以使用全功能的建模系统,并仍然使用他们想要(或必须)使用的ORM。

最主要的原因当然是微软推动Entity Framework作为“官方”ORM框架,并使它看起来没有其他竞争对手。这使得EF成为.NET上使用最广泛的ORM。但是这样产生的副作用就是,.NET上的第三方ORM被驱赶到一个利基市场,其中的大多数已经被淘汰。如果你比较2008年和现在的ORM列表,你会发现剩下的并没有多少。通过支持EF、NHibernate和Linq to SQL,我们依然可以作为ISV存在,同时与免费ORM竞争,尽管它们被大量开发人员使用。

InfoQ:下一个版本的Entity Framework,也就是EF Core,它将完全放弃设计器,只支持代码优先的实体建模。这样做的一个重要原因是它使用了大量的XML文件,导致在源代码管理方面效果不佳,特别是在合并分支时。那么LLBLGen设计器是如何处理这些问题的?

Frans Bouma:映射文件的合并冲突实际上与典型的代码冲突是一样的,但是由于合并冲突是在XML文件中,而不是在C#文件中,所以它们往往看起来更加可怕。将冲突的编辑合并到同一个C#文件往往更加容易,因为我们能够理解这两个代码变更之间的区别,并可以合理地判断选择哪一个。但是在EDMX文件的XML中,这是很困难的,因为它们是通过复杂的元素彼此联系在一起的,我们并不知道它们做了什么。对于我们的项目格式,我们尽可能保持简单和直接:如果你查看项目文件的XML,你就可以理解每个元素做了什么以及为什么。此外,模型元素本身的排序和命名也采用了可以最小化冲突的方式。

InfoQ:LLBLGen Pro的运行时框架与其他ORM(如Entity Framework和NHibernate)有何不同?

Frans Bouma:每个ORM都有其特点和共同点。LLBLGen Pro运行时框架和其他ORM相比最显着的区别之一是它在实体类实例内进行变更跟踪,因此不需要上下文或会话对象(Scott Ambler在ORM中所做的旧有设计)。在实体本身内部进行变更跟踪有许多优点,一个是你可以有一个独立的工作对象单元。这使得你可以使用独立的工作对象单元来跟踪内存实体图的变更,然后可以将其存储到持久层。这将提供你想要的一切:不管在新建、更新或删除这些实体的时候,都不会产生冲突,因为这些信息就在工作单元和实体内部。

这种设计为实体类带来了无冲突解耦和持久性,而ORM里的Ambler模型无法提供这些特性:我们需要告诉集中式会话类或上下文类该如何操作实体类对象图。一个很好的例子是Entity Framework中的添加/附加API。

除了上面这些特性外,在跟实体相关的使用场景方面,它比EF和NHibernate要快10倍以上,这也是很好的一个特点。

InfoQ:EF的添加/附加API绝对是一个问题,特别是我们从富客户端(可以保持上下文开放)切换到REST style服务(对象从传入的JSON重新创建)的时候。你可以用一个简单的例子展示一下LLBLGen Pro是如何在服务环境中执行插入/更新操作的吗?

Frans Bouma:由于变更跟踪是在实体内部完成的,所以如果在.NET客户端中使用它们,那么这些数据就会被序列化/反序列化。这就意味着,当你得到返回的实体结果,你可以简单地保存图:新创建的实体将被插入,需要被更新的实体将得到更新。这个过程并不需要微管理。以下是一个接受客户实体作为参数的方法示例,客户实体可能在它的“订单”集合里引用到订单实体,而订单实体可能还包含对其他实体的引用:

public void StoreChanges(CustomerEntity myCustomer)
{
   using(var adapter = new DataAccessAdapter())
   {
      adapter.SaveEntity(myCustomer);
   }
}

就是这样。当然你也可以使用客户端中的工作单元,通过如下代码提交保存:

public void StoreChanges(UnitOfWork2 uow)
{
   using(var adapter = new DataAccessAdapter())
   {
      uow.Commit(adapter);
   }
}

第二个例子将持久性与应用程序本身所需的工作管理单元分开。有人可能会注意到UnitOfWork2的后缀“2”:其实我们的框架支持两种范式:自助服务(实体自身的持久性,比如myCustomer.Save(),并且支持延迟加载)和上面例子中的适配器。适配器是在自助服务之后引入的,当时我们不想把适配器的不同接口和类绑定到名称“Adapter”上,所以我们使用了一个数字后缀。这是我们比较后悔的一个决定,但是一旦决定了就必须坚持下去,因为要避免大量重大更改,以免影响客户的使用。所以我们保留了后缀。

InfoQ:审计是安全性问题中越来越重要的一个部分。过去,我们只需要记录最后一次接触记录的人就足够了,但是现在我们经常需要一个完整的变化历史记录,甚至是读取数据的时间列表。你可以谈一下LLBLGen Pro是怎样处理授权和审计的吗?

Frans Bouma:LLBLGen Pro运行时框架具有内置的审计和授权功能。这些功能是通过auditor和authorizer类完成的,这些类的实例可以在运行时被添加到实体实例中。而且,你也可以选择通过覆盖一些方法来实现。实体中的auditor对象会在各种操作下被框架调用,比如读取或写入字段时,实体被保存或删除时。开发者在这个阶段可以自由选择,包括创建新的实体,比如映射到审计数据表的实体。在事务的最后阶段,参与事务的实体会被询问包含审计信息实体需要持久化,这些新创建的审计信息实体就是在这个时候被保存的。

授权以相同的方式实现:当一个给定的操作需要授权,比如读取字段或者实例化一个实体对象,框架就会调用实体中的authorizer对象进行授权。同样,开发者可以自由选择处理授权请求的方式,只需要在最后返回一个布尔值来说明授权结果。如果授权失败,则读取的数据将会被视为“null”或“void”。

这一切对开发者来说都是透明的:authorizer和auditor类(以及validators)是在实体类之外编写的,可以通过内置的注入功能或者其他DI框架注入,或者通过覆盖来实现。使用实体类,你完全不必担心数据级别的审核或授权。

InfoQ:LLBLGen设计器或ORM是否支持生成数据库表?

Frans Bouma:LLBLGen Pro的设计器当然能为你创建数据库表的DDL SQL:这是模型优先机制的关键部分。你可以对实体模型进行建模,然后单击按钮将其与项目中的相关模型数据同步。接下来,设计器将基于实体模型中的更改来更新相关的模型数据。之后,你就可以生成DDL SQL更新脚本,用所做的更改来更新真实数据库schema。

数据库的创建是通过DDL SQL脚本实现的,因为在实际的数据库服务器环境中,DBA的规则是首先使用脚本进行迁移测试。这对于新开始的项目来说不是大问题,但对于已经进入生产环境的系统来说(占应用程序生命周期的大部分!),在发布之前需要仔细进行迁移测试。在这些情况下,通过DDL SQL脚本来交付变更是最理想的方式。这也是为什么运行时不会创建表的原因:因为没有必要。如果需要从头开始,你可以从设计器生成DDL SQL脚本。但是一般来说,还有其他元素也是数据库schema的一部分,它们不能从模型中创建:视图,表值函数,存储过程。因此,新安装的应用程序通常从包含视图和其他元素的完整的数据库schema创建DDL SQL脚本开始,例如SSMS。

InfoQ:LLBLGen Pro运行时架构独有的一个特性是QuerySpec。那是什么促使你开发这种LINQ表达式树的替代品?

Frans Bouma:在2008年的大部分时间里,我为我们的运行时框架编写了一个完整的LINQ Provider,而且最后的结果我认为还是可以的。但是,每一个ORM开发人员都知道,如果ORM里包含了LINQ Provider,它会带来无尽的麻烦。因为总是会出现各种问题,比如树结构中出现的非预期表达式。主要原因是表达式树就像一个AST,但没有正式的语法。所以,在把具有正式语法的输入转换成AST时并不存在预定义的规则,它只是某种表达式树,你需要使用访问者来把它重写成你所期望的结果。

LINQ的另一个问题是,它到SQL的映射不是1对1的。在某些情况下,它是这样的。但有时候你必须解释表达式树,并将其转换为符合开发人员实现意图的SQL。解释表达式树的整体复杂性是难以想象的,这使得LINQ Provider变成一个非常复杂的系统。再加上与大量输入树的耦合,你将很难得到一个没有问题的程序。

从用户的角度来看,LINQ也是非常复杂的,如果你使用过“from x in name”这样的查询,你就会知道:用户很难确定SQL会是什么样的(比如,它将执行正确的操作吗?)或反过来:他们知道想要执行的SQL,但很难将其转换回LINQ。

这些原因使我意识到我们需要一个替代的查询系统。我们的运行时框架已经有一个,但是我们的底层API使用的还是2003年的1.0版本的查询API,它很冗长。我使用扩展方法构建了一个流畅的查询系统,它非常接近SQL的流程,同时在运行时更容易处理。它解决了用户如何编写查询语句的问题,如果他们知道SQL(它1:1映射到SQL,也可以选择一些更高级的方法,比如Any() call),他们就知道该如何操作。对我们来说,它解决了非预期的树的问题,因为系统是简单易用的。整个查询系统写了不到2个月就完成了。

InfoQ:你能给出一些QuerySpec可以做但EF的LINQ Provider却做不了的例子吗?

Frans Bouma:一个例子就是LINQ不能为连接提供一个特定的ON子句:在LINQ中,连接总是使用“a = b”谓词的形式。它不能给你提供一个类似'A JOIN B ON A.F> B.F'这样的连接语法。但是在QuerySpec中,你就可以实现:On子句只是一个谓词表达式:它可以是任何你想要的子句。此外,你可以进行全连接、右连接和左连接,而无需了解复杂的结构。

QuerySpec仍然是一个高级查询API,所以它不像jOOQ一样在.NET中为你提供每个SQL关键字:它抽象出SQL语句细节,而且没有作用域和范式转移问题。

InfoQ:你对存储过程和在应用程序代码中单独生成查询有什么看法?

Frans Bouma:当你开始解决一个问题时,你会发现一连串的其他问题。十多年前,人们对存储过程的利弊进行了热烈的讨论,作为ORM的开发者,我当然参与了这些辩论,我认为存储过程被高估了,我们不应该使用存储过程。这个观点在当时算是应了景,但如果将它作为真实的建议却有点太过极端。在应用程序代码中使用存储过程可能会非常有用,换句话说,当你需要在代码里处理大量的数据时,让代码的执行尽量地接近数据会带来很大的好处,因为你不需要把数据从数据库传输到其它域,而是直接在数据上执行代码。而且,SQL作为面向集合的语言更适合于表达面向集合的逻辑,而不是在C#中使用一系列命令式语句。

也就是说,对于基本的CRUD,比如插入X、更新X、删除X,使用存储过程会带来维护问题,因为应用程序代码所需的任何操作都必须在存储过程API中进行:所以对该API的修改变得很困难,因为存储过程也可能被其他代码调用。这往往导致存储过程数量激增:为了避免影响到其它代码,我们简单粗暴地对需要变更的存储过程进行复制和修改。

存储过程依靠动态性质的更新,这也是其效果往往不如那些运行时生成的SQL的另一个原因:哪些字段更新只有在运行时才知道,所以使用SQL语句生成更有效率,而不是用一个通用的存储过程简单地更新每个字段。图表获取/预先加载也很难用存储过程操作,因为这些本质上也是动态的。

LLBLGen Pro设计器允许你在模型中使用存储过程,然后生成可供调用的方法。无论是模型优先还是数据库优先,这种方法都是可行的。你可以使用工具中的model first -> tables功能对实体模型建模,同时将存储过程与数据库schema进行同步。

InfoQ:你有没有想过在LLBLGen设计器上支持NoSQL风格的数据库?

Frans Bouma:是的,从v5版本以来,我们已经开始支持“派生模型”,它是在实体模型上定义的模型,由派生元素组成,你可以将其视为文档或DTO。这些派生的元素是分层的,它们派生自实体,并且可以包含一些非正式的字段,比如其它相关的派生元素,这跟为NoSQL文档数据库设计文档有点类似。

设计器为这些派生模型生成.NET代码,然后可以与RavenDB、MongoDB或任何其他文档数据库(如微软的DocumentDB)一起使用。其背后的想法是,在nosql数据库中使用的文档模型实际上是从抽象实体模型派生出来的。通过在LLBLGen Pro设计器中对它们进行定义,对实体模型所做的更改将贯穿到在其上定义的派生模型,因此,在运行时使用文档数据库的文档定义有一个理论基础:它不存在于其他地方,因为不存在schema(除了序列化为JSON的POCO之外)。

目前实体模型仍然需要一个关系映射,但在不久的将来这种情况可能会改变。

关于被访者

Frans Bouma 是LLBLGen Pro的创建者和主要开发人员,LLBLGen Pro是领先的.NET的ORM和实体建模解决方案。Frans有超过22年的专业软件工程师经验。自2003年以来,他全职开发LLBLGen Pro和他的公司Solutions Design的另一个项目:ORM Profiler。在此之前,他在各种项目中广泛涉猎各种技术,从VB到C++。在业余时间,Frans喜欢在游戏中截图并编程实现必要的工具:从使用HLSL的着色程序到使用C++和x86/x64汇编程序的视频工具。

查看英文原文: Interview with Entity Modelling Tool Creator, Frans Bouma

感谢薛命灯对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们。

原文  http://www.infoq.com/cn/articles/Frans-Bouma-Interview
正文到此结束
Loading...