DTO是由域实体生成还是由持久性生成

本文关键字:持久性 实体 DTO | 更新日期: 2023-09-27 18:28:14

当涉及到具有现代ORM的分层应用程序时,我经常不确定应该如何创建特定的类以遵守所谓的"最佳实践";同时也注意性能要求。

考虑一下应用程序中可能有任意数量的以下类型的对象:

  1. 域实体-这些是包含业务逻辑的丰富类(对吧?),并且根据ORM功能,可能与持久性设计直接相关。

  2. DTO-这些类比较简单,可以剥离业务逻辑,以便将数据传递给内部和外部客户端。有时这些会被压扁,但并不总是如此。

  3. 视图模型-它们与DTO相似,因为它们更简单,没有业务逻辑,但它们通常非常扁平,并且通常包含与它们所服务的UI相关的附加位。

我面临的挑战是,在某些情况下,域实体或任何面向持久性的类到更简单的实体(如DTO或ViewModel)的映射会阻止您进行重要的性能优化。

例如:

假设我有一些域实体看起来像这样:

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime EventDate { get; set; }
    
    // These would be reference types in most ORMs
    // Pretend in the setter I have logic to ensure the headliner =/= the opener
    public Band Headliner { get; set; }
    public Band Opener { get; set; }
}
public class Band
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Genre Genre { get; set; }
}

在现实世界中,这些可能要复杂得多,有各种业务逻辑,可能有一些验证调用等。

如果我公开一个公共的API,那么我的DTO可能看起来非常像这个例子,没有任何业务逻辑。

如果我也有一个MVC web应用程序,我想在上面显示事件列表,我可能想要一个看起来像这样的视图模型:

public class EventViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime EventDate { get; set; }
    
    public int HeadlinerId { get; set; }
    public string HeadlinerName { get; set; }
    public int OpenerId { get; set; }
    public string OpenerName { get; set; }
}

通常,人们只是提取一个带有引用的完整域实体,然后使用映射实用程序来水合视图模型。

然而,假设我有数以万计的记录。现在ORM可能正在创建一系列查询来填充完整的引用对象(这可能比本例复杂得多,因为它们有自己的引用)。没过多久,性能就开始受到严重影响。

问题是什么

我知道我不是唯一一个遇到这个问题的人,所以我很想知道人们是如何维护分层应用程序的,同时在生成表示相同底层域信息的多个对象时,仍然考虑到保持性能的需要。

让两个Event-ish对象表示相同的持久化数据是不对的,但同时持久化层似乎不应该知道DTO或视图模型,否则争取分离有什么意义?

那么你该如何解决这个问题呢?持久性是否知道域实体的严格、详细表示以及这些实体中数据的轻量级描述?那些较轻的描述是DTO还是一些领域实体lite?

DTO是由域实体生成还是由持久性生成

你的问题没有简单的答案,因为它实际上取决于你想用架构实现什么这是一种经典的架构权衡

这也意味着你需要自己决定。确保你知道每种方法的优点和缺点,然后为你的项目做决定。以下是一份利弊清单:

严格分离的优点

  • 根据特定层的职责调整结构的能力。例如,持久性DTO可以与域实体不同地存储数据,以支持复杂的查询情况
  • 支持数据迁移案例的能力。使用单独的持久性DTO,您可以选择加载"旧"DTO格式并将其转换为"新"域实体
  • 简化返回外部世界的DTO的能力,例如通过API。当使用DDD时,这几乎总是有意义的,因为使用DDD通常表明域是复杂的
  • 更好地分离开发人员的关注点。通常,严格的分层会增加团队并行处理同一功能的可能性,例如一个在持久性中,一个在域中
  • 根据ORM或数据库的功能集,在持久性中直接使用域实体甚至不是一种选择。如果这是一种选择,它可能比拥有专用DTO更复杂

共享类的优势

  • 相同功能的代码更少
  • 通常新功能的开发时间更快
  • 较小的概念开销。我认为这是一个次要的问题,因为DTO和视图模型是众所周知的概念,但这可能是一个取决于团队的问题

正如您所看到的,我不认为性能是共享方法的优势主要原因是设计良好的对象到对象映射比从数据库加载数据快几个数量级。所以我非常确信,严格分离方法中的性能问题是由其他问题引起的,而不是分层问题。

有了以上几点(以及可能针对您的环境的更多要点),您应该能够做出决定。我过去使用过这两种方法,但对于一定规模的项目,我总是选择严格的分离方法。

Josh,

域实体必须独立于ORM,事实上,如果您遵循DDD原则,则所有域层都不应依赖于任何其他层。DTO只是在层之间传输数据,在大多数情况下,它在Repository的接口中使用,作为方法的返回。存储库的接口,作为服务的接口,应该保留在域层中。

DTO没有行为,它们用于数据传输。

ViewModels包含一些关于演示的行为,因此它们也不是DTO。如果没有任何视图特定行为,则可以在演示文稿中使用DTO。

域实体和ORM实体不相同。您正在做的可能是活动记录,而不是域模型。您应该能够用您喜欢的持久性逻辑替换ORM。它们必须脱钩。

我认为您将DTO与价值对象混淆了,价值对象是业务对象,确实具有行为并持久化。如果某个值对象没有标识,并且包含属于一起的行为或多个值,则通常使用值对象来描述该对象。例如,地址、电话号码、id等可以是值对象。

不能使用域对象将数据传输到演示文稿。演示不能直接访问和修改域对象,这就是为什么我们使用DTO在演示和应用程序服务之间发送数据。应用程序服务可以访问域对象。

让两个类似事件的对象表示相同的持久化数据是不对的。。。

事实上,这并不一定是坏事。您的EventViewModel最终可能与Event类一致。您的Event确保满足所有Event业务规则,而EventViewModel可能会通过侦听域事件来更新,域事件由(例如)Event类发出。这有时被称为投影-EventViewModelProjection侦听Event域事件(并非双关语),并将这些事件投影到EventViewModels上。

但同时,持久层似乎不应该知道DTO或视图模型。。。

好吧,如果您选择持久化DTO和视图模型,那么持久化逻辑应该在的某个地方进行编码。

否则,争取分离有什么意义。。。那么你该如何解决这个问题。。。那些较轻的描述是DTO还是一些领域实体lite?

不可能给你一个明确的答案——这些都是设计考虑因素,很大程度上取决于你的具体环境。如果遇到性能问题,那么像我提到的那样使用域事件是个好主意。

你可能有兴趣阅读关于cqrs和最终一致性的文章,以获得一些想法。

域实体代表您的域/业务。例如,在抵押贷款领域,托管账户是一个领域实体。托管账户由其账号标识。此实体不代表您的表架构,这对您的数据库一无所知。

public class Escrow
{
public Guid AccountId {get; set;}
public decimal GetBalance()
}

查看模型

我总是将视图模型与域和DTO分开,因为我希望视图模型代表我的视图,而不是其他视图。这将有数据注释、验证逻辑等。

DTO和ORM实体

现在这是棘手的一点。我将DTO和ORM实体都放在同一个项目中,并确保它们对任何东西都没有任何依赖性,它们只是POCO。我从创建ORM实体开始,并在需要时添加或创建DTO。

我跨层使用为ORM创建的实体,尽可能将它们用作DTO。我不会向这些ORM实体添加或删除属性。如果我的服务或应用程序中的任何其他层需要与ORM实体略有不同的结构,我会为该需求创建新的POCO,并且它们对所有层都可用。

例如,如果我需要一个计算值,我想将其传递到ORM实体中不可用的UI层(因为它们没有持久化),那么我只创建一个具有必需字段的新POCO。

我使用AutoMapper在对象之间复制数据

通常,人们只是提取一个带有引用的完整域实体,然后使用映射实用程序来水合视图模型。

然而,假设我有数以万计的记录。现在ORM可能会创建大量查询来填充完整引用对象(可能比本例复杂得多他们自己的参考文献)。性能启动不需要很长时间遭受严重痛苦。

为了最大限度地减少查询数量并提高性能:

  1. 在一次调用中请求多个对象(例如,100个事件)

  2. 在单个调用中请求具有主对象的相关对象(例如,具有标题和Opener的100个事件)

  3. 缓存对象以查找已请求的对象,而不是再次请求

  4. 对来自ViewModel的请求进行排队(每个ViewModel告诉它需要哪些对象,然后在单个调用中请求所有对象,每个ViewModel都会返回它所请求的对象)

根据查询的层,Object在服务层上下文中表示DTO,在域/持久层上下文中指Entity

我认为这是当您开始尝试DDD时出现的第一个问题之一:查询结束显示数据时的性能。

这里的关键概念是,域模型必须专注于操作、执行业务规则并最终触发事件,而不是提供信息。当然,您仍然可以将其用作向用户显示的数据源,但是,如果您遇到性能问题,最好评估命令查询责任分离模式(CQRS)的使用情况。

使用它,要显示的数据由另一个模型(特别是数据模型)表示,在您的示例中,该模型可以是EventViewModel类。数据模型的设计独立于域模型,并且通常以从数据源构建数据模型的方式进行设计(即:不需要对象映射)。

在域驱动设计(DDD)中,域层应该不知道持久性、表示、缓存、日志记录和其他基础设施服务。这可以通过使用抽象(在依赖服务上使用接口而不是具体服务)来实现。您可以应用SOLID原则,这可以帮助您创建良好的软件架构:

S是单一责任原则(SRP)
O代表开闭原理(OCP)
利斯科夫替代原理(LSP)
I接口隔离原则(ISP)
D依赖注入原理(DIP)