【问题标题】:How to query multiple aggregates efficiently with DDD?如何使用 DDD 高效查询多个聚合?
【发布时间】:2017-05-17 11:25:12
【问题描述】:

当我需要调用某个业务方法时,我需要获取与该操作相关的所有聚合根,即使该操作与下面给出的操作一样原始(只是将项目添加到集合中)。我错过了什么?或者是基于 CRUD 的方法,您在其中运行一个查询,包括表连接、选择和最后插入 - 数据库引擎为您完成所有工作 - 实际上在性能方面更好?

在下面的代码中,我需要查询单独的聚合根(创建另一个数据库连接并发送另一个选择查询)。在现实世界的应用程序中,我一直在查询不止一个聚合,单个业务操作最多查询 8 个。如何提高性能/查询开销?

域聚合根:

class Device
{
    Set<ParameterId> parameters;

    void AddParameter(Parameter parameter)
    {
        parameters.Add(parameter.Id);
    }
}

class Parameter
{
    ParameterId Id { get; }
}

应用层:

class DeviceApplication
{
    private DeviceRepository _deviceRepo;
    private ParameterRepository _parameterRepo;

    void AddParameterToDevice(string deviceId, string parameterId)
    {
        var aParameterId = new ParameterId(parameterId);
        var aDeviceId = new DeviceId(deviceId);

        var parameter = _parameterRepo.FindById(aParameterId);
        if (parameter == null) throw;

        var device = _deviceRepo.FindById(aDeviceId);
        if (device == null) throw;

        device.AddParameter(parameter);
        _deviceRepo.Save(device);
    }
}

可能的解决方案

有人告诉我,您可以像这样只传递另一个聚合的 Id:

class Device
{
    void AddParameter(ParameterId parameterId)
    {
        parameters.Add(parameterId);
    }
}

但 IMO 打破了封装(通过明确强调术语 ID 到业务中),也不能防止粘贴错误或其他不正确的身份(由用户创建)。

Vaughn Vernon 给出了使用第一种方法(传递整个聚合实例)的应用程序服务示例。

【问题讨论】:

  • 我可以直接添加/删除IDS,而不是自己聚合吗?

标签: repository domain-driven-design aggregateroot


【解决方案1】:

简短的回答是 - 根本不要查询您的聚合。

聚合是公开行为而非数据的模型。通常,在聚合上有 getter 被认为是代码异味(ID 是例外)。这使得查询有点棘手。

一般来说,有两种相关的方法可以解决这个问题。可能还有更多,但至少这些不会破坏封装。

选项 1:使用域事件 - 通过让您的域(聚合根)发出事件来说明内部状态的变化,您可以在数据库中建立专门用于查询的表。如果做得对,您将拥有高性能、非规范化的可查询数据,如有必要,可以对其进行线性缩放。这使得查询非常简单。我在这篇博文中有一个例子:How to Build a Master-Details View when using CQRS and Event Sourcing

选项 2:推断查询表 - 我是选项 1 的忠实拥护者,但如果您没有事件源方法,您仍然需要在某个时候保持聚合的状态。有多种方法可以做到这一点,但您可以插入持久性管道以进行聚合,从而将可查询数据提取到读取模型中以供查询使用。

我希望这是有道理的。

【讨论】:

  • 我正在使用领域事件和单独的读取模型/CQRS 模式。但为了执行 DOMAIN 操作,我需要查询这些聚合以获取它们的 ID。或者您是否建议业务方法应该采用 ID 而不是聚合根? (虽然我没有使用事件源,所以我正在保存域对象状态,我需要查询这些状态,而不是一些可能最终一致的读取模型)。
  • 那么当我需要给Device添加一些Parameter时,Device必须有一个接受ParameterId的方法,而不是Parameter本身? Vaughn Vernon 不是这样做的。目前我的设备有一个接受 Parameter 本身的方法,并在其中询问 Id 并保存它。
  • 假设它们都是 AR,那么是的,它必须是 ID,因为 AR 代表一致性边界。但是,如果这些模型在 AR 中,那么您所做的就很好。也许你会觉得这很有帮助:Inter-Aggregate Communication
  • 那我怎么知道给定的 Id 是现有的聚合而不是客户端生成的一些废话?我们最终可能会得到错误的引用。
  • 这就是验证的目的。
【解决方案2】:

如果您发现在这种情况下可以使用带有连接的 RDBMS 查询 - 可能您的聚合边界有误。

例如 - 为什么需要加载 Parameter 才能将其添加到 Device?你已经有了这个Parameter 的身份,你需要做的就是把这个id 添加到Device 中的引用列表Parameters 中。如果你这样做是为了满足你的 ORM - 你很可能做错了什么。

还请记住,您的聚合事务边界。您确实希望在一个事务和一个连接中完成所有数据库操作。

【讨论】:

  • 是的,你说的很对。我有一个 ID,我可以将这个 ID 附加到 ID 列表中。只是我的 AddParameter 方法接受 Parameter 聚合根(不是 ParameterId 值对象),有利于聚合之间的封装和紧密凝聚。这导致还需要组装这个其他聚合(只是为了在之后得到它的 ID)。
  • 您可以通过使用值对象作为 id 进行足够的封装,因此您将传递 ParameterId 对象(结构),但这完全有效,您不必传递预取的聚合。很明显,聚合之间没有任何其他类型的引用,而不仅仅是一个 id。
  • 封装是指业务逻辑(将参数传递给设备,而不是 parameterId 给设备)。各种ID有点封装破绽,因为业务不关心ID。而且我已经在为 id 使用值对象。无论如何,如果我们传递ParameterId 对象 - 还会引发另一个问题:如果用户将创建随机 ParameterId 对象或无效 ParameterId 对象或已删除聚合的某些 Id 怎么办?当您传递聚合本身时,会自动阻止这种情况。
  • 此外,Effective Aggregate Design II 中的 Vaughn Vernon 给出了与我的示例相同的来自多个存储库的聚合查询的示例。
  • 我现在正在看论文,找不到您所指的内容。一切都是使用身份值对象完成的。唯一的例外是ofTeam。我们看不到ofTeam 是如何使用的。可能是有调用的方法之类的。因为没有获得其他两个对象,而是仅引用。 “生成”的 id 问题是已知的。你必须控制你的客户。这是应用程序服务客户端的责任,以确保域模型仅使用正确的数据。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-18
  • 2023-03-26
  • 2016-04-26
  • 2016-11-29
相关资源
最近更新 更多