【问题标题】:Domain driven design and ORM limitations领域驱动设计和 ORM 限制
【发布时间】:2014-09-21 04:04:53
【问题描述】:

我看到的大多数 DDD 示例都是用 Java 编写的,并且绝大多数使用 Hibernate 来持久化和获取实体。我真的对两者都没有任何经验,我假设 Hibernate 是一个足以解决依赖关系、处理值对象等的工具。我选择的 ORM 是 Doctrine2,据我所知,这是 PHP 目前拥有的最好的工具,但在我看来,它不足以支持 DDD 原则。

这是一个领域层的例子:

/**
 * Simple value object
 */
class ProductId
{
    private $value;

    function __construct($value)
    {
        $this->value = $value;
    }

    public function value()
    {
        return $this->value;
    }
}

/**
 * Example dependency
 */
class Dependency
{
    public function doNothing()
    {

    }
}

/**
 * Game class done in a DDD manner
 */
class Game
{
    /**
     * @var ProductId
     */
    private $id;

    /**
     * @var string
     */
    private $title;

    /**
     * @var Dependency
     */
    private $dependency;

    function __construct(ProductId $id, $title, Dependency $dependency)
    {
        $this->id    = $id;
        $this->title = $title;

        // Validation
        Assertion::minLength(25, $title);
    }

    /**
     * @return ProductId
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }

    public function someBehavior()
    {
        $this->dependency->doNothing();
    }
}

现在有了 Doctrine,您可以使用 XML 或 YAML 映射将 Game 映射到某个表。 但是,在调用 $gameRepository->productOfId($someId); 时,您会得到一个格式错误的对象。原因如下:

  1. 由于在调用getId() 时Doctrine2 不支持值对象,您将得到一个普通的int,因为它的映射将指向一个整数列。最新的测试版有点支持它们,但仍然不是很灵活,在更复杂的场景中也不容易配置。
  2. 由于 Doctrine2 在对获取的实体调用 someBehavior() 时创建了用于检索的代理对象,因此您会收到一个致命错误,因为代理对象不解析构造函数依赖项。这实际上可以通过创建一些DomainRegistry 单例并从那里获取依赖项来克服,但我真的不喜欢我暗示的那样。然而,由于没有调用构造函数,我们仍然跳过验证,而且我不会只依赖数据库完整性。

我应该如何克服它?我宁愿不在我的域模型中使用一些if is int return ProductId(int) 的东西,因为我希望我的域层是持久性无知的。

我想到的一件事是(假设我会坚持使用 ORM,而不仅仅是 DBAL)将 Doctrine 实体视为 DTO(我希望我在这里正确使用该术语)并从中组装域对象。所以除了提到的课程之外,我还有类似的东西:

/**
 * Doctrine entity treated like a pure value container
 */
class GameDTO
{
    private $id;
    private $title;

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }
}

class DoctrineGameRepository
{
    /**
     * @var Dependency
     */
    private $dependency;

    /**
     * Doctrine's entity manager
     */
    private $em;

    function __construct(Dependency $dependency, $em)
    {
        $this->dependency = $dependency;
        $this->em = $em;
    }

    public function productOfId($id)
    {        
        /** @var GameDTO $gameDto */
        $gameDto = $this->em->find($id);

        return new Game($gameDto->getId(), $gameDto->getTitle(), $this->dependency);        
    }
}

这样我克服了基础设施的限制,但它增加了另一层复杂性。另外,我应该如何处理需要反映到我的 DTO 的对象的更改,以便我可以更新我的数据库?

我的第二个想法是适应 CQRS,但这对我来说只是一个流行词,因为我还没有进行太多研究。我知道它的原理,但我不知道我应该为此建模哪些类和存储库。

你会如何处理这一切?

【问题讨论】:

  • DDD 不依赖于 Hibernate。在 Java 项目中甚至没有必要使用 Hibernate。
  • @duffymo,我从没这么说过。我用它作为理论对立的例子来说明 Doctrine 做事的方式。就像我说的 - 我真的不知道 Hibernate 是否能解决所有问题,但这超出了问题的范围。
  • 就我个人而言,我总是向人们抱怨如何最好地在存储库中切断您的 ORM。列出的原因太多,但请始终考虑“我是否希望我的域模型被以数据为中心的工件和属性污染?”。我强烈建议从 DDD 的角度来看,将您的 ORM 实体视为 DTO,使用它们来填充域实体并从您的存储库中返回这些实体。
  • @AdrianThompsonPhillips,但是您如何处理更新数据库数据?由于 DTO 而不是 DDD 实体在 ORM 下,因此我在域模型中所做的每一项更改都需要以某种方式反映到 DTO。
  • @acid 在我的情况下(.NET 开发)我遇到的实体框架 ORM 最多,我在它的“数据库优先”范式中使用它。所以我只是简单地更改域模型,然后更改数据库并让实体框架自动重新生成 DTO。当我遇到使用“代码优先”范例的 NHibernate 或 EF 时,我也必须维护对 DTO 的更改。或者,在我自己的项目中,我使用了 RavenDB(一个文档数据库),我可以在其中保留域实体,而无需任何 ORM、DTO 或映射。

标签: php orm doctrine-orm domain-driven-design


【解决方案1】:

我认为这个问题提出了一个很好的观点。

ORM 的限制是否会影响您的 DDD 设计?

我大部分时间都用java编写代码,我知道hibernate是如何发展的,3-4年前甚至hibernate都不支持值对象,对嵌入式对象的支持也很幼稚,看起来Doctrine2在那个阶段

我认为一旦你倾向于 CQRS,ORM 的魔力就不再那么需要了,我认为 CQRS 和 DDD 齐头并进,如果 CQRS 单独实现,它是漂亮的simple,它只有在将 ES 添加到等式时它变得复杂(ES 有它的好处)。

因此,我建议您为所有要使用 DDD 概念的项目使用基本 CQRS。所以从这里我们可以假设你有一个 Finder/Query 类来处理你的查询,这些应该是直接的 SQL

不要让我们深入了解存储库。一旦我们从存储库中查询出来,存储库实际上很简单,它公开的接口将非常紧凑,这里是一个示例。

public interface Repository<T> {

    T load(Object aggregateIdentifier);    
    void add(T aggregate);
    void update (T aggregate);
}

您可以对系统中的每个聚合都有一个实现。

在这里,您可以按照您认为合适的方式将聚合从结果集中编组出来。这可能需要一些努力,但成本可以跨项目摊销,并且您可以摆脱所有 ORM 的黑魔法:)

【讨论】:

  • 我真的很喜欢你对 CQRS 的保证,但我仍然需要处理 ORM 的黑魔法,因为写入模型将在 ORM 之下。示例 - Product 有两个 Price&lt;amount,currency&gt; 对象 - regularfinal。它们代表 db (reg_amount, reg_currency, fin_amount, fin_currency) 中的总共 4 列。 Doctrine 要求我创建四个原始列来保存它。转型应该发生在哪里?我真的不想用我的 ORM 强加给我的额外属性来污染我的写入模型。
  • 我鼓励您考虑转储 ORM 并编写自己的插入/更新查询,这样您就可以管理域模型和关系模型之间的映射。
  • 但是即使我更改一列void update (T aggregate);,您是否建议我进行整行更新?或者我应该以某种方式检查要针对什么运行更新?你能提供一些实现的例子吗?
  • 我会做一个完整的更新。我们使用的是Axon框架,我会尝试通过它的源代码找到相关的实现。
  • ES 代表什么?
猜你喜欢
  • 1970-01-01
  • 2016-09-29
  • 1970-01-01
  • 2011-10-06
  • 2011-06-21
  • 2016-07-18
  • 2013-09-16
  • 1970-01-01
相关资源
最近更新 更多