2016 年 7 月

第 31 卷,第 7 期

领先技术 - 关于 Code First、持久性和域建模的反思

作者 Dino Esposito | 2016 年 7 月

Dino Esposito你可以在实体框架 (EF) 中找到 Code First 这项功能,它让你可以使用普通的 .NET 类创建数据库表模型。坦白地讲,我认为 Code First 这个名称具有一定的误导性,但其实际工作原理非常清楚。Code First 展示了已使用的数据库的结构,并提供了面向对象的全面的 API,它与存储数据一起使用。

Code First 最初在 EF4.1 中被引入,并一直随附至 EF6,它是通过 C# 或 Visual Basic 类进行数据库建模的方法之一。在 EF6 之前,你还可以使用 Visual Studio 设计器来推断数据库的架构、将其保存到一个 XML 文件(使用 EDMX 扩展名),并创建临时类以供代码使用。借助 Visual Studio 设计器,你还可以创建一个抽象模型,以供日后创建物理数据库。

简言之,在 EF6 之前有两种方法可以实现此操作,但相较另一种方法,EDMX 方法存在更多问题,尽管它很实用。为此,即将发布的 EF7 将不再支持 EDMX。

多年来,我们一直把 Code First 与域驱动设计 (DDD) 联系在一起,这可能已经让我们形成了一种普遍概念,认为 Code First 和 EDMX 并不是执行同一操作的两种完全不同的方法。在本专栏文章中,我将提供有关 Code First 的更多体系结构方面的见解,并在域模型领域和持久性模型之间划清界限。Code First 和 LINQ 实现了大多数开发人员的旧梦: 它隐藏了面向对象的外观背后有关数据访问的错综复杂的问题(表、索引和约束),并且符合你从未有过的面向对象的数据定义语言。

历史背景

在使用关系数据库时,你需遵守 SQL 语言的规则。而对应用程序进行编码时,你需遵守所选编程语言的规则。因此,我们需要一个抽象层,将顶级编程语言面向对象(或过程化)的特性与 SQL 语言联系到一起。在 Microsoft .NET Framework 中,该抽象层是 ADO.NET 框架。

ADO.NET 是一个相对精简的抽象层,从某种意义上来说,它仅为你的 .NET 代码提供放置 SQL 命令的对象。ADO.NET 不会将已发送或从数据库检索到的数据映射到面向对象的临时数据结构。在 ADO.NET 中,获取数据的工具与周围的 .NET Framework 完全合并在一起,但是数据是平面的。

大概十年前,对象/关系映射程序 (O/RM) 框架出现了。O/RM 框架将某类的属性映射到表列。在此过程中,它实施了一系列设计模式,如数据映射器、工作单元和查询对象。O/RM 框架还会在内部维护一组映射规则以及关于目标数据库架构的信息。这是具体和有形的信息,必须将其存储在某个位置。NHibernate - .NET 空间中的第一个 O/RM - 将该信息存储为 XML 文件。EF 最初采用与 EDMX 文件相同的方法,并添加了非常棒的设计器,以在 Visual Studio 内对其进行管理。Code First 通过属性或 fluent(和更加丰富的)API 将类属性映射到列和表。

在几个月前发表的一篇博客文章中,EF 团队清楚地解释了在 EF7 中将 Code First 作为存储数据模型的唯一受支持方法背后的动机。(你可以在 bit.ly/1sLM3Ur 中阅读完整文章。) 相较于“Code First”这个名称,文章中“基于代码的建模”的表达方式能更好的解释其实际作用。我对此完全同意。

DDD 概述

DDD 是一种软件开发方法,最初被设计为一组用于系统地控制复杂性级别的规则(即大量的业务规则和实体)。DDD 的显著优势是它可以在大型系统中使用至少数百个规则和实体,但对于简单方案中的开发人员和架构师来说也具有很大价值。简言之,我们没有理由不在每个软件项目中应用 DDD 的某些部分。对任何项目来说,DDD 中有价值的部分是其战略设计 - 以几个众所周知的方法的应用程序为中心: 通用语言、界定的上下文和上下文映射。这些分析模式与你最终在应用程序中实际使用的类和数据库表并无太大关系,尽管使用它们的最终目的是更有效地编写代码。DDD 战略设计模式的目标是分析业务域并构想结果系统的顶级体系结构。图 1 提供了针对电子商务解决方案可能的顶级体系结构。每个图块代表一个界定的上下文,将在分析过程中对其标识并引入,以加速开发。

具有界定的上下文的顶级体系结构示例
图 1 具有界定的上下文的顶级体系结构示例

来自你的分析的每个界定的上下文都具有各自的业务语言、软件体系结构(包括技术)以及与其他界定的上下文的关系组。然后可能实施每个界定的上下文(使用最适合指定数目和团队技术的软件体系结构、预算和时间约束以及其他利益干系人关注的问题,如现有软件许可、成本、专业技能以及策略等相关事项。DDD 针对为界定的上下文进行生成的有效方法提供了明确的建议:分层式体系结构。

分层式体系结构中的域模型

图 2 提供了分层式体系结构的要点。它有四层 - 从表示层到体系结构 - 应用程序层和域层处于中间位置。简言之,它是众所周知的三层体系架构的通用形式 - 表示层、业务层和数据层 - 它是随着你在表示层中考虑的用例进行更改的用例逻辑和对开展业务的特定方法所固有的域逻辑之间的巧妙分离,对所有用例和表示层来说都很常见。

分层式体系结构的架构
图 2 分层式体系结构的架构

基础结构层包括实施和支持用例所要求的所有内容,并将域实体的状态持久化。因此,基础结构层包括了解连接字符串以连接到数据库的组件。

DDD 方法的核心是“域模型”的概念。 显而易见,域模型是你创建的软件模型,它可以完全体现业务域。换句话说,你可以使用该软件处理你所面对的域。通常情况下,域模型中充满了实体、事件和值对象,以及由一些实体和值对象共同组成的一个不可分解的单元。DDD 将其称为“聚合”,而聚合的根是聚合根。持久性出现在聚合根的层级,而聚合根通常负责对聚合中的所有其他实体和值对象进行持久化。

如何编写实体和值类型聚合的代码? 这取决于你使用的编程模式。大多数情况下,域模型是面向对象的模型,其中实体是具有属性和方法的类,而值对象是固定不变的数据结构。使用函数化语言和固定不变的数据结构是一种方法,但它们至少要处在业务域的某个类型中。

Code First 是具体的技术,它与数据访问任务的性能息息相关。Code First 最具特征的方面是使用类来体现表的底层架构以及应用程序使用的数据。应用程序使用的数据与通过关系表由应用程序进行持久化的数据是否相同? 或者换一种方法提问:使用 Code First 类组对关系数据库中的表进行映射与使用应用程序的域模型是否相同? 对于这个问题,我的答案很可能是否定的。但对于软件体系结构,和往常一样,需要视情况而定。

分层式体系结构中的域模型

我们有时将 Code First 与 DDD 关联在一起,因为它具有通过类对应用程序的数据进行建模的功能。不过有时候,使用单个类组处理域的业务逻辑和持久性问题是不能被接受的,简言之,域模型和持久性模型是截然不同的。域模型是软件模型,你可以使用它表示系统的域逻辑并实施它的业务规则。它可能是一个面向对象的模型,也可能是一个函数模型,甚至可能是从帮助程序类公开的静态方法的普通集合。

DDD 的要点是避开域模型的持久性问题,并且在域模型的设计中,你更关注业务实体的作用(以及如何使用),而不是关注其包含和管理的数据。行为中心方法将复杂性的难度级别分解为可以使用代码进行有效处理的级别。让我们举一个简单的例子 - 运动匹配 - 如图 3 所示。

行为与域模型实体中的数据
图 3 行为与域模型实体中的数据

若要表达评分系统上下文中的匹配实体的行为,需要在特定方案中对操作进行建模,如:开始、完成、目标、超时和任何其他有意义的操作。这些方法将实施所有业务规则并确保以编程方式仅执行与实体的当前状态一致的操作。例如,如果在匹配实例上的调用由于超时被挂起,则方法“Goal”将被丢弃。匹配实体的内部状态包括通常在普通关系模型中与实体关联的所有属性,除非这些属性为只读,且只通过方法在内部进行更新。

在域模型中,并非所有的类都必须持久化,持久化可能包括所有属性,也可能只包括几个属性。所以,总的来说,Code First 与域建模无关,而与将属性映射到表列的 API 有关,你可以将其用于对域模型中需要持久化的类进行持久化。这样,你就具有该域的单个模型,它覆盖了业务和持久性需求。

私有资源库的问题

从域建模的角度来看,你仅使用实体 - 按照域专家概述的业务工作流进行操作。再回到匹配评分示例,它可能与业务规则设置的匹配状态或以编程方式评分不一致。实际上,状态和评分会在工作流执行时发生变化。同样,你不会具有默认的无参数构造函数,因为它将返回匹配实体(排除了一些关键信息,如团队名称以及合理地将匹配连接到竞赛的 ID)。不过,如果你使用业务和持久性的单个模型,则需要无参数构造函数;否则,EF 在查询后无法返回类型的实例。

但是需考虑更多的情况。当 EF 执行查询并返回匹配类的实例时,它需要访问所有属性的资源库,以便在返回的实例中保存与数据库中的信息一致的状态。这是对与域模型的设计规则相冲突的 EF 的合法要求。总的来说,将状态强制应用到域模型的实体的方法必须存在,而且大多数情况下,它必须是内部的,不能通过代码在层外部公开获取。这是域服务的目的之一,它与域一起组成了图 2 中的域层。如果使用 Code First,你只需将资源库标记为非公开(内部、受保护甚至私有),并使用非公开可见性添加默认构造函数,即可达到同样的目的。EF 仍将查找方法(通过反射)以访问私有成员并强制应用某个状态,但是域 API 的公共客户不会执行此操作,除非他们亲自使用反射。

总结

进行网上冲浪时,我们经常能看到将 Code First 与 DDD 关联在一起的文章。Code First 是面向对象的模型(显式映射到一组表)的持久化。从概念上讲,域模型是完全不同的概念,它甚至处于不同的层。但是,因为 Code First API 的一些特定功能(处理私有资源库和构造函数),在某些情况下,我们可以使用单个面向对象的模型,其中包含行为和业务规则,且可以轻松地持久化到关系数据库。


Dino Esposito是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。作为 JetBrains 的 .NET 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents@wordpress.com 上以及 Twitter @despos 上的推文中分享他对于软件的愿景。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Jon Arne Saeteras