【问题标题】:Rich domain model with ORM使用 ORM 的丰富领域模型
【发布时间】:2011-12-03 04:38:25
【问题描述】:

我似乎遗漏了一些东西,并且广泛使用 google 并没有帮助提高我的理解...
这是我的问题:
我喜欢以不了解持久性的方式创建我的领域模型,例如:

  1. 如果我不需要,我不想添加virtual
  2. 我不喜欢添加默认构造函数,因为我喜欢我的对象始终是完全构造的。此外,在依赖注入的上下文中,对默认构造函数的需求是有问题的。
  3. 我不想使用过于复杂的映射,因为我的域模型使用了 ORM 不容易支持的接口或其他构造。

对此的一种解决方案是使用单独的域对象和数据实体。使用存储库模式并从 ORM 返回的数据实体构建域对象,可以轻松解决构建的域对象的检索问题。使用 AutoMapper,这将是微不足道的,并且没有太多的代码开销。

但是我对这种方法有一个大问题:如果不自己编写代码,我似乎无法真正支持延迟加载。此外,对于同一个“事物”,我会有很多类,尤其是在 WCF 和 UI 的扩展上下文中:

  1. 数据实体(映射到 ORM)
  2. 域模型
  3. WCF DTO
  4. 查看模型

所以,我的问题是:我错过了什么?这个问题一般是怎么解决的?

更新:
到目前为止的答案表明了我已经担心的事情:看起来我有两个选择:

  1. 在域模型上做出妥协以匹配 ORM 的先决条件,从而获得 ORM 泄漏到的域模型
  2. 创建大量附加代码

更新:
除了接受的答案之外,请参阅my answer 了解有关我如何为我解决这些问题的具体信息。

【问题讨论】:

  • 关于2(默认构造函数),NH允许你做构造函数注入;我不确定EF。关于 3(复杂的映射),我不确定您的意思 - 如果对象模型与关系模型不紧密匹配,则存在不可避免的复杂性。 Fluent NHibernate 的约定在这里可能会有所帮助。关于virtual,我也不喜欢它,但它是无痛的。
  • 关于 1(虚拟):对于非延迟加载的实体,您不必这样做。您还可以将 NH 的要求更改为想要他们

标签: c# .net architecture


【解决方案1】:

一年多过去了,我现在已经为我解决了这些问题。

使用 NHibernate,我能够将相当复杂的领域模型映射到不会让 DBA 畏缩的合理数据库设计。

有时需要创建IUserType 接口的新实现,以便NHibernate 可以正确地持久化自定义类型。由于 NHibernates 的可扩展性,这没什么大不了的。

我发现没有办法避免将virtual 添加到我的属性而不丢失延迟加载。我仍然不是特别喜欢它,尤其是因为代码分析中关于虚拟属性的所有警告,没有派生类覆盖它们,但出于实用主义,我现在可以忍受它。

对于默认构造函数,我还找到了一个我可以接受的解决方案。我添加了我需要的构造函数作为公共构造函数,并添加了一个过时的受保护构造函数供 NHibernate 使用:

[Obsolete("This constructor exists because of NHibernate. Do not use.")]
protected DataExportForeignKey()
{
}

【讨论】:

    【解决方案2】:

    我会质疑匹配 ORM 的先决条件必然是“做出妥协”。但是,从高度 SOLID、松耦合架构的角度来看,其中一些是公平的。

    ORM 框架的存在只有一个原因;采用您实现的域模型,并将其持久化到类似的数据库结构中,而无需实现大量容易出错、几乎不可能进行单元测试的 SQL 字符串或存储过程。它们还可以轻松实现延迟加载等概念;在需要对象之前的最后一分钟为对象补水,而不是自己构建大型对象图。

    如果您想要存储过程,或者拥有它们并需要使用它们(无论您是否愿意),大多数 ORM 都不是适合该工作的工具。如果您有一个非常复杂的域结构,以至于 ORM 无法映射字段与其数据源之间的关系,我会严重质疑您为什么要使用该域和该数据源。如果你想要 100% POCO 对象,而不知道背后的持久性机制,那么你可能最终会绕着 ORM 的大部分功能进行结束运行,因为如果域没有虚拟成员或子集合可以用代理替换,那么你就不得不急切地加载整个对象图(如果你有一个巨大的相互关联的对象图,这很可能是不可能的)。

    虽然 ORM 在领域设计方面确实需要一些持久性机制领域的知识,但 ORM 仍然会产生更多的 SOLID 设计,IMO。如果没有 ORM,这些是您的选择:

    • 滚动您自己的存储库,其中包含一种在您的域中生成和持久化每种“顶级”对象的方法(“上帝对象”反模式)
    • 创建每个处理不同对象类型的 DAO。这些类型要求您对 ADO DataReaders 和您的对象之间的 get 和 set 进行硬编码;在一般情况下,映射大大简化了该过程。 DAO 还必须相互了解;要持久化 Invoice,您需要 Invoice 的 DAO,它还需要 InvoiceLine、Customer 和 GeneralLedger 对象的 DAO。而且,所有这些都必须内置一个通用的、抽象的事务控制机制。
    • 设置一个 ActiveRecord 模式,其中对象会自行持久化(并将有关持久性机制的更多知识放入您的域中)

    总的来说,第二个选项是最可靠的,但通常情况下,它会变成一个需要维护的三分之二的野兽,尤其是在处理包含反向引用和循环引用的域时。例如,为了快速检索和/或遍历,InvoiceLineDetail 记录(可能包含运输单或税务信息)可能直接引用 Invoice 以及它所属的 InvoiceLine。这将创建一个 3 节点循环引用,该引用需要 O(n^2) 算法来检测对象是否已被处理,或者需要对反向引用的“级联”行为进行硬编码逻辑。我以前必须实现“图形步行者”;相信我,如果有任何其他方式来完成这项工作,你不会想这样做。

    因此,总而言之,我的观点是,考虑到足够复杂的领域,ORM 是所有弊端中最小的。它们封装了关于持久性机制的大部分非 SOLID 内容,并将域关于其持久性的知识减少到非常高级的实现细节,这些细节分解为简单的规则(“所有域对象必须将其所有公共成员标记为虚拟”)。

    【讨论】:

    • 感谢您的回答,但是,我认为您缺少一个选项,基本上是我目前喜欢的选项:在没有任何 ORM 知识的情况下创建高度复杂的域模型,然后创建更相似的简单实体合适的数据库结构。延迟加载可以在存储库中实现(我想你称之为 DAO)。没错,这种延迟加载实现只有在属性返回一个接口或者是虚拟的时才有效……
    • 此外:让成员 virtual 是我可以接受的,但其他妥协不是(例如默认构造函数)
    • 我只想补充一点,数据库不需要那么相似。数据建模非常成熟,系统中数据的最佳设计可以建模为适合在 RDBMS 中实现的形式,而与操作它的系统无关。它应该受到整个系统的启发,但应该能够针对数据库需要进行重构而不影响系统的其他部分。因此 ORM 系统的重要性——在两个完全不同的系统之间进行映射,每个系统都对自己的领域是最优的。
    • @Daniel - 除了间接层太多之外,没有任何问题无法通过添加另一层间接来解决。这是一个完美可行的解决方案,但它添加了 ORM 将水合的 DTO 层,以及将 DTO 转换为水合域类的层。而且,就像我说的,如果你不直接使用 ORM 使用的对象,你就会失去很多 ORM 的查询能力;您不能使用可查询对象,并且转换过程基本上会强制急切加载顶级对象的完整图形。
    • @KeithS:在对此进行试验并在两个复杂的现实世界应用程序中使用 NHibernate 之后,我开始同意您的观点。感谢您抽出宝贵的时间,很抱歉没有感谢您的回答。请参阅my answer,了解有关我如何为我解决这些问题的更具体概述。
    【解决方案3】:

    所有优点。

    我没有答案(但是当我决定添加一些关于存储过程的内容时,评论变得太长了)除了说我的哲学似乎与你的相同,并且我编写代码或生成代码。

    像部分类这样的东西使这比早期的 .NET 时代要容易得多。但是 ORM(作为一种独特的“东西”,而不是在进出数据库时才完成的东西)仍然需要很多妥协,坦率地说,它们对我来说过于抽象。而且我对有很多欺骗课程并不大,因为我的设计往往有很长的寿命,并且多年来(甚至几十年)变化很大。

    就数据库方面而言,在我看来,存储过程是必需的。我知道 ORM 支持它们,但大多数 ORM 用户倾向于不这样做,这对我来说是一个巨大的负面影响 - 因为他们谈论最佳实践,然后他们耦合到基于表的设计,即使它是创建的来自代码优先模型。在我看来,如果他们不想以利用其优势的方式使用关系数据库,他们应该查看对象数据存储。我首先相信代码和数据库 - 即同时来回对数据库和对象模型进行建模,然后从两端向内工作。我将在这里列出:

    如果您让开发人员针对您的表编写 ORM 代码,那么您的应用将无法运行多年。表需要改变。越来越多的人想要打击这些实体,现在他们都在使用从表生成的 ORM。随着时间的推移,你会想要重构你的表。此外,只有存储过程才能为您提供任何可用的基于角色的可管理性,而无需在每列 GRANT 的基础上处理每个表 - 这是非常痛苦的。如果你在 OO 中编程得很好,你就必须了解受控耦合的好处。这就是所有存储过程 - 使用它们,因此您的数据库具有定义明确的接口。或者,如果您只想要一个“愚蠢”的数据存储,请不要使用关系数据库。

    【讨论】:

    • 这个答案完全表达了我的担忧。我还喜欢“孤立地”设计 ORM 和数据库,即不会因为映射的其他部分而做出妥协……
    • 从事相当繁重的项目已经一年多了。除了小的升级脚本外,它的 sql 代码行数为零,存储过程更少。尝试关注用户需求而不是外键 - 然后代码“自行解析”。
    • 相信“系统的核心”存在于数据库中是误导和有害的。只有我们对业务领域的了解及其问题的解决方案才是重要的。
    • @Arnis L. 这是客户端代码。数据库必须保护其边界——您需要能够审核数据库中的更改并知道它们来自何处。如果您的数据库不保护其边界,您就不得不质疑对其数据的信任。一个系统可能有一个“心脏”,但一个系统是一组相互连接的组件。数据库就像 OO 中的任何其他组件一样,它们也保护它们的边界。基于 ORM 的系统有什么特别之处,他们更愿意将数据留在裸露的哑数据存储中?
    • @Arnis L. 我的意思是,将 ORM 与愚蠢的数据存储一起使用是矛盾的。为什么要从一个优秀的 OO 模型映射到一个不利用关系数据库的许多/任何优势的平庸数据模型?为什么要地图?绝大多数 ORM 爱好者并不专注于他们的数据库。他们只关注 OO 模型——从而降低了 R 和 M 部分的价值。他们只是口头上说数据库的价值——我们正在使用行业标准的 Oracle 或 SQL Server 数据库,却不承认他们没有明智地使用这项投资。
    【解决方案4】:

    这是我们在最新项目中所做的,效果很好

    1. 为我们的业务对象使用带有虚拟关键字的 EF 4.1,并拥有我们自己的 T4 模板自定义实现。将 ObjectContext 包装在存储库样式数据访问的接口后面。
    2. 使用 automapper 在 Bo To DTO 之间进行转换
    3. 使用 autoMapper 在 ViewModel 和 DTO 之间进行转换。

    你会认为 viewmodel 和 Dto 和 Business 对象是同一个东西,它们可能看起来一样,但它们在关注点方面有一个非常明确的分离。 视图模型更多的是关于 UI 屏幕,DTO 更多的是关于你正在完成的任务,而业务对象主要关注领域

    在此过程中会有一些妥协,但如果你想要 EF,那么好处大于你放弃的东西

    【讨论】:

    • 您在哪里使用 DTO?如果你给你的 BO 提供 virtual 关键字,看起来你是先使用代码并在 ORM 中使用你的 BO,对吗?
    • DTO 正在通过网络传输有关 Bos 的选定信息,是的,我们首先在 ORM 中使用代码。
    • "over the wire":您是在谈论 WCF,还是在某处将 DTO 与您的 ORM 结合使用?
    • DTO 被用于通过 WCF 传输数据
    【解决方案5】:

    您是否看过 Entity Framework 4.1 Code First? IIRC,领域对象是纯 POCO。

    【讨论】:

    • NHibernate 也支持代码优先。那不是问题。问题是我认为将相当复杂的域模型映射到关系数据库或多或少是不可能的。
    • @DanielHilgarth 和你是对的......另一方面 - 你永远无法表达心智模型而没有任何损失。所以 - 接受它。更重要的是要彻底了解您正在做出的权衡。
    【解决方案6】:

    简而言之 - 它没有解决

    (这里有额外的无用字符来发布我的精彩答案)

    【讨论】:

    • 这个答案真的是真棒,因为这就是事实!
    猜你喜欢
    • 2023-04-05
    • 2014-01-05
    • 1970-01-01
    • 1970-01-01
    • 2012-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-29
    相关资源
    最近更新 更多