任何 .NET ORM 是不是“正确”使用构造函数?

Posted

技术标签:

【中文标题】任何 .NET ORM 是不是“正确”使用构造函数?【英文标题】:Do any .NET ORMs use constructors "properly"?任何 .NET ORM 是否“正确”使用构造函数? 【发布时间】:2009-05-29 15:41:03 【问题描述】:

这在概念上与我的问题 here 有关。但是,我一直在玩 NHibernate,并意识到我的问题的真正核心是什么。

在经典的 OO 设计中,为了正确封装数据,将值传递给对象的构造函数并存储在数据成员(字段)中是一种常见模式。那些应该被更改的值仅通过访问器(只读属性)公开。那些允许更改的有访问器和修改器(读写属性)。在我看来,适当的 O/RM 应该尊重这些约定,并在创建对象时使用可用的构造函数。依赖读写属性、反射或其他骇人听闻的(恕我直言)方法似乎……是错误的。

有没有 .NET O/RM 解决方案可以做到这一点?

编辑

为了解决 Praveen 的观点,我知道有些项目具有用于选择构造函数的“默认”算法 - 例如,StructureMap 始终使用具有最多参数的构造函数,除非您标记具有自定义属性的构造函数。我可以看到这是处理这种情况的有效方法。也许使用 IoC 容器除了 ORM 会提供我需要的那种解决方案 - 尽管在我看来这虽然不是天生的坏事,但对于使用 ORM 来说是一个不必要的额外步骤.

【问题讨论】:

还有一个有趣的问题:构造函数可以包含一些业务逻辑吗? IE。当我创建类 A 的实例时,它的构造函数可以创建持久类 B 的实例并将其分配给具有私有设置器的属性。 @Alex:我的目标是让域对象对持久性无知。从域对象创建持久对象的逻辑违反了这一点,并将域对象与持久方法紧密耦合 - 另一个坏事。 【参考方案1】:

我认为大多数 ORM 实际上都支持这个概念,至少 DataObject.Net 支持。此代码按预期工作:

[HierarchyRoot(typeof(KeyGenerator), "Id")]
public class Message : Entity

  [Field]
  public int Id  get; private set; 

  [Field(Length = 100)]
  public string Text  get; private set; 

  public Message(string Text)
  
    Text = text;
  

编辑: DataObjects 以内部事务状态存储数据,并使用 PostSharp 生成的特殊实现构造函数。当然如果 ORM 使用 poco 对象也不是那么简单。

【讨论】:

【参考方案2】:

不幸的是,如果不以某种方式标记构造函数,这在 .NET 中是不可能的。

在程序集元数据中为每个构造函数存储的方法签名只包含构造函数的每个参数的类型。任何 .NET ORM 都无法真正知道要使用哪个构造函数。 ORM 看到的都是这样的:

.ctor()
.ctor(string, string)
.ctor(string, string, string)

ORM 无法知道哪个 .ctor 参数对应于您的 Customer 对象的 FirstName、LastName 和 MiddleName。

为了提供这种支持,.NET ORM 必须支持读取您为每个参数定义的自定义属性。你需要像这样标记你的构造函数:

public Customer([Property("FirstName")] string FirstName, [Property("LastName")] string LastName, [Property("MiddleName")] string MiddleName)

这有两个缺点:

    没有办法(我能想到,有人可能会纠正我),这可以进入映射文件。 您仍然需要编写与以往相同的映射,因为 ORM 仍然需要能够获取每个属性的单独值。

所以你需要做所有这些额外的工作来标记构造函数,同时你仍然需要像以前一样映射你的类。

【讨论】:

我完全不反对映射类——事实上,我更喜欢它而不是标记源代码。不过,您确实提出了一些关于固有困难的有趣观点 - 谢谢。 “程序集元数据中为每个构造函数存储的方法签名”还包括参数名称,而不仅仅是其类型。 ORM 可以按名称将参数与属性匹配,而不需要任何映射(约定优于配置)。不过,仍然存在使用哪个 ctor 的问题。 @Lucas:非常正确 - .NET Reflector 确实显示了参数的名称。谁知道——也许我刚刚发现了我需要在这里抓挠的痒。【参考方案3】:

为什么你觉得这感觉不对? 您是否希望您的 OR/M 在重构对象时执行业务逻辑?恕我直言,没有。

当您从数据库加载对象时,无论如何,OR/M 都应该能够重构该对象。在重新构造时设置一些值,不应导致触发某种改变另一个值的逻辑(可能也应该由 ORM 赋予一个值......)

即便如此,我认为构造函数应该只包含那些在对象创建时必须使对象处于“有效”状态的字段的参数。 除此之外,您可能有一些公共只读属性,这些属性已通过某种计算获得了值,并且也需要持久化。 如果您觉得反射是重构对象的“气味”,您将如何处理这种情况?您必须以某种方式创建一个能够设置“只读”值的公共方法,但这会破坏封装,您也不希望这样做。

【讨论】:

存在什么样的公共只读属性,由计算创建,不是对象持久状态的一部分?该计算将使用哪些数据?如果它是来自对象的数据,那么您可以轻松地重构它。如果它是对象外部的数据,我会争辩说数据或只读字段位于错误的位置。 @Harper:“如果它是来自对象的数据,那么您可以轻松地重构它”,在一个方便的只读属性中!它类似于数据库中的计算列 或者,你的对象中的某种“状态”标志怎么样。假设您在用户更改属性时设置了一个标志“PropertyChanged”,但您不希望在从数据库加载对象时设置此标志。【参考方案4】:

我同意之前的一张海报,即构造函数应该用于在其生命周期开始时创建对象。使用构造函数来水合当前处于归档状态的现有对象充其量是违反直觉的。

需要考虑的是,所有主要的 ORM 在这一点上似乎都缺少的是,从存储在数据库或任何其他数据存储中的最后一个已知状态重新构建域对象并不是天生的构造或修改操作;因此不应使用构造函数或属性设置器。相反,与这种操作最紧密对应的框架机制是序列化!已经有一些公认的模式用于存储对象的状态,然后通过 ISerializable 接口、标准序列化构造函数等对其进行重构。将对象持久化到数据库与将对象持久化到流基本上没有区别;事实上,在序列化期间使用的 StreamingContextStates 枚举的值之一是 Persistence!。恕我直言,这应该是设计任何持久性机制时的标准方法。不幸的是,我不知道有任何开箱即用的 ORM 支持。

还需要注意的是,已经为序列化而设计的对象仍然是POCO;没有违反持久性无知。这里的关键点是域对象应该最清楚地保存和恢复它需要哪些数据,以及应该以什么顺序恢复数据(这通常很重要)。对象不必知道存储它的特定机制:关系数据库、平面文件、二进制 blob 等。

【讨论】:

早在 ISerializable 存在之前,使用构造函数来反序列化对象是很常见的(而且一点也不违反直觉!)。构造函数创建对象的实例 - 构造函数调用和 域实体 的生命周期没有内在联系。对象,是的...但是对象是域实体的表示,而不是实体本身。【参考方案5】:

天哪,我想我明白了!

感谢大家的意见,但我将不得不回答我自己的问题。我只花了一个小时左右的时间来研究PoEAA catalog,思考面向对象的原则,并将其与对 C# 语言和 .NET 框架的深入思考相结合。

我想出的答案,我无法使用构造函数正确解决的一个需求,最终与构造函数本身无关。这是lazy loading!

基本上,如果不在域类中实现延迟加载(对持久性无知和灵活性的主要禁忌),如果不对域类进行子类化,就无法做到这一点。这种子类化是 NHibernate 需要虚拟属性的原因。

我仍然认为使用构造函数而不是反射或其他方法来填充父类的字段会更好(至少对于非集合......延迟加载确实有它的位置),但我肯定看到无参数构造函数的位置。

【讨论】:

我不太明白你在这里得出的结论......你是否认为域对象有一个默认构造函数是可以的,因为它表达了关于域的一些东西,即。对象能够被延迟加载吗?这似乎仍然与持久性有关,而不是域逻辑...... 我的结论是,涉及“机械”问题,使“纯粹主义”方法难以实施,如果不是不可能的话。我仍然不是很喜欢它,但我务实的一面愿意配合它完成工作。【参考方案6】:

在一般情况下,OR 映射器不可能只使用构造函数。假设一个对象由传递给构造函数的值初始化,然后通过方法调用修改状态。在这种情况下,对象可能会以没有有效初始状态的状态持续存在,因此被构造函数拒绝。

因此,这样的 OR 映射器可能存在,但它肯定有局限性。如给定示例所示,我不会将 OR 映射器绕过对象封装视为糟糕的设计,而是在某些情况下作为要求。

【讨论】:

我看不出这是一个有效的论点。如果对象的持久化状态对构造函数有效,则它应该对对象生命周期中的任何时间点都有效。任何在其生命周期内有效状态发生变化的对象都可能是您不持久(或仅部分持久)的对象,或者可能需要重构。 论据走向另一个方向 - 在生命周期内有效的状态在构建时不一定有效。以任何项目的生命周期管理系统为例。它们始终以初始状态创建,然后在项目继续进行时状态会发生变化,直到您达到完成、关闭或存档状态。您甚至不需要将状态作为参数的构造函数,因为状态总是隐含的初始状态。但当然,您必须能够在任何状态下持久化和检索项目。 我认为您的断言是错误的 - 我看不出为什么项目对象无法在打开、完成、存档或任何其他 有效 状态下实例化。如果一个项目在完成之前无法存档,那么具有尚未发生的完成日期的存档状态是无效的,但是让新实例化的项目处于存档状态则不一定如此。 'greedy' 构造函数选择方法实际上也支持这一点(默认情况下不会选择更少或无参数的 ctor,但如果有它是有意义的,则可以使用它)。 从设计的角度来看,让构造函数接受状态是错误的——每个新创建的项目都必须以初始状态开始。在非初始状态下创建新项目没有任何意义。因此,添加一个接受状态的构造函数是可能的,但设计不好。 怎么样?对象(由构造函数创建)是域实体的表示 - 它不是实体本身。对象生命周期不等同于域实体生命周期——否则我们根本不需要讨论 O/RM 工具!【参考方案7】:

先生。 Skeet 不久前提出了一种“建造者”模式。我将它设为 POCO 类中的公共类。它具有与 POCO 相同的属性,但都是读/写的。 POCO 在必要时是只读的,但 PRIVATE SET。这样,构建器可以在实例化 POCO 时设置属性,并且构造函数没有时髦的参数。有一种“popcicle 不变性”。

【讨论】:

这似乎违反了 DRY 原则 - 我个人反对重复性任务,尤其是当我认为它们实际上不是正确的方法时。 我明白了。让我对构造函数参数方法感到困惑的是,它是我列出属性的另一个地方。构造函数参数列出它们;构造函数设置私有字段列出它们;和领域本身......必须改变一个参数? brrrr 是的...即使是 C++ 方法(初始化列表)也没有很多好。然而,构造函数参数方法确实更接近于匹配我当时被教导的 OO 原则(好吧,10 年前 - 我没那么老!)。【参考方案8】:

这取决于您认为不应更改的值。自动增量、计算列等都是很好的选择。

这当然可能,我使用我编写的 ORM,如果您尝试设置只读属性的值,它会引发异常。

更新:

记住构造函数也用于持久化数据。让您的对象在构造函数中接受 PK 是一种常见模式,它会自动获取该记录。

【讨论】:

如果它正在创建属性,那岂不是让它成为代码生成器而不是 O/RM(或者两者兼而有之)? 两者兼有!没有规定 ORM 必须动态生成映射。 如果您的 PK 是代理键,对我来说,在构造函数中使用 PK 会有点代码味道。如果 business 键是 PK,那么这更有意义,但在这种情况下,我建议构造对象是错误的方法 - 将 PK 传递到存储库将是更好的路线。 如果这对您来说是一种代码味道,那么您对哈希表有什么看法? 我没有看到类似的情况——数据库主键(假设它是代理键)与域模型无关。如果您有 DAL 之外的代码(在业务层或 UI 代码中)通过数据库 PK 请求一个对象,那么您刚刚向您的实现细节开放了非 DAL 层。

以上是关于任何 .NET ORM 是不是“正确”使用构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

Kohana 3 ORM:构造函数“加载后”

任何 .NET ORM 是不是支持开箱即用的本地化实体?

ORM 是不是有任何方法可以确定 SQLite 列是不是包含日期时间或布尔值?

轻量级.Net ORM SqlSuger项目实战

无法正确单步执行静态构造函数中的代码(VS2019、C#、.NET 4.7.2)

PHP中的构造函数