Persisting DDD Aggregates – 持续存在的DDD聚合体

最后修改: 2018年 11月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll explore the possibilities of persisting DDD Aggregates using different technologies.

在本教程中,我们将探索使用不同技术持久化DDD聚合体的可能性。

2. Introduction to Aggregates

2.骨料的介绍

An aggregate is a group of business objects which always need to be consistent. Therefore, we save and update aggregates as a whole inside a transaction.

聚合是一组业务对象,它们总是需要保持一致。因此,我们在一个事务中把聚合体作为一个整体来保存和更新。

Aggregate is an important tactical pattern in DDD, which helps to maintain the consistency of our business objects. However, the idea of aggregate is also useful outside of the DDD context.

聚合是DDD中一个重要的战术模式,它有助于保持我们业务对象的一致性。然而,在DDD背景之外,聚合的概念也很有用。

There are numerous business cases where this pattern can come in handy. As a rule of thumb, we should consider using aggregates when there are multiple objects changed as part of the same transaction.

在许多业务案例中,这种模式都能派上用场。作为一个经验法则,当有多个对象作为同一事务的一部分被改变时,我们应该考虑使用聚合。

Let’s take a look at how we might apply this when modeling an order purchase.

让我们来看看在建立订单采购模型时,我们如何应用这个方法。

2.1. Purchase Order Example

2.1.采购订单示例

So, let’s assume we want to model a purchase order:

因此,让我们假设我们想对一个采购订单进行建模。

class Order {
    private Collection<OrderLine> orderLines;
    private Money totalCost;
    // ...
}
class OrderLine {
    private Product product;
    private int quantity;
    // ...
}
class Product {
    private Money price;
    // ...
}

These classes form a simple aggregate. Both orderLines and totalCost fields of the Order must be always consistent, that is totalCost should always have the value equal to the sum of all orderLines.

这些类形成了一个简单的集合OrderorderLinestotalCost字段必须始终一致,即totalCost的值应始终等于所有orderLines的总和。

Now, we all might be tempted to turn all of these into fully-fledged Java Beans. But, note that introducing simple getters and setters in Order could easily break the encapsulation of our model and violate business constraints.

现在,我们都可能想把所有这些变成成熟的Java Bean。但是,请注意,在Order中引入简单的getters和setters很容易破坏我们模型的封装并违反业务约束。

Let’s see what could go wrong.

让我们看看可能出现的问题。

2.2. Naive Aggregate Design

2.2.天真的集合体设计

Let’s imagine what could happen if we decided to naively add getters and setters to all properties on the Order class, including setOrderTotal.

让我们想象一下,如果我们决定天真地给Order类的所有属性添加getters和setters,包括setOrderTotal,会发生什么。

There’s nothing that prohibits us from executing the following code:

没有什么能禁止我们执行下面的代码。

Order order = new Order();
order.setOrderLines(Arrays.asList(orderLine0, orderLine1));
order.setTotalCost(Money.zero(CurrencyUnit.USD)); // this doesn't look good...

In this code, we manually set the totalCost property to zero, violating an important business rule. Definitely, the total cost should not be zero dollars!

在这段代码中,我们手动将totalCost属性设置为零,违反了一个重要的业务规则。肯定的是,总成本不应该是零美元!

We need a way to protect our business rules. Let’s look at how Aggregate Roots can help.

我们需要一种方法来保护我们的业务规则。让我们来看看Aggregate Roots如何帮助我们。

2.3. Aggregate Root

2.3.集合根部

An aggregate root is a class which works as an entry point to our aggregate. All business operations should go through the root. This way, the aggregate root can take care of keeping the aggregate in a consistent state.

聚合根是一个类,它作为我们聚合的入口点。所有的业务操作都应该通过根来进行。这样一来,聚合根可以负责保持聚合的一致状态。

The root is what takes cares of all our business invariants.

根是照顾我们所有业务不变性的东西

And in our example, the Order class is the right candidate for the aggregate root. We just need to make some modifications to ensure the aggregate is always consistent:

而在我们的例子中,Order类是聚合根的正确候选者。我们只需要做一些修改,以确保聚合始终是一致的。

class Order {
    private final List<OrderLine> orderLines;
    private Money totalCost;

    Order(List<OrderLine> orderLines) {
        checkNotNull(orderLines);
        if (orderLines.isEmpty()) {
            throw new IllegalArgumentException("Order must have at least one order line item");
        }
        this.orderLines = new ArrayList<>(orderLines);
        totalCost = calculateTotalCost();
    }

    void addLineItem(OrderLine orderLine) {
        checkNotNull(orderLine);
        orderLines.add(orderLine);
        totalCost = totalCost.plus(orderLine.cost());
    }

    void removeLineItem(int line) {
        OrderLine removedLine = orderLines.remove(line);
        totalCost = totalCost.minus(removedLine.cost());
    }

    Money totalCost() {
        return totalCost;
    }

    // ...
}

Using an aggregate root now allows us to more easily turn Product and OrderLine into immutable objects, where all the properties are final.

现在使用聚合根允许我们更容易地将ProductOrderLine变成不可变的对象,其中所有属性都是最终的。

As we can see, this is a pretty simple aggregate.

我们可以看到,这是一个相当简单的聚合。

And, we could’ve simply calculated the total cost each time without using a field.

而且,我们本来可以简单地计算每次的总成本,而不使用一个字段。

However, right now we are just talking about aggregate persistence, not aggregate design. Stay tuned, as this specific domain will come in handy in a moment.

然而,现在我们只是在讨论聚合持久性,而不是聚合设计。请继续关注,因为这个特定领域一会儿就会派上用场。

How well does this play with persistence technologies? Let’s take a look. Ultimately, this will help us to choose the right persistence tool for our next project.

这与持久性技术的作用如何?让我们来看看。最终,这将帮助我们为下一个项目选择合适的持久化工具

3. JPA and Hibernate

3.JPA和Hibernate

In this section, let’s try and persist our Order aggregate using JPA and Hibernate. We’ll use Spring Boot and JPA starter:

在本节中,让我们尝试使用JPA和Hibernate持久化我们的Order聚合。我们将使用Spring Boot和JPA>启动器。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

For most of us, this seems to be the most natural choice. After all, we’ve spent years working with relational systems, and we all know popular ORM frameworks.

对于我们大多数人来说,这似乎是最自然的选择。毕竟,我们已经花了多年时间与关系型系统打交道,而且我们都知道流行的ORM框架。

Probably the biggest problem when working with ORM frameworks is the simplification of our model design. It’s also sometimes referred to as Object-relational impedance mismatch. Let’s think about what would happen if we wanted to persist our Order aggregate:

在使用ORM框架时,最大的问题可能是简化了我们的模型设计。它有时也被称为对象-关系阻抗不匹配。让我们思考一下,如果我们想持久化我们的Order聚合,会发生什么。

@DisplayName("given order with two line items, when persist, then order is saved")
@Test
public void test() throws Exception {
    // given
    JpaOrder order = prepareTestOrderWithTwoLineItems();

    // when
    JpaOrder savedOrder = repository.save(order);

    // then
    JpaOrder foundOrder = repository.findById(savedOrder.getId())
      .get();
    assertThat(foundOrder.getOrderLines()).hasSize(2);
}

At this point, this test would throw an exception: java.lang.IllegalArgumentException: Unknown entity: com.baeldung.ddd.order.Order. Obviously, we’re missing some of the JPA requirements:

在这一点上,这个测试会抛出一个异常。java.lang.IllegalArgumentException。未知实体:com.baeldung.ddd.order.Order很明显,我们缺少一些JPA的要求:

  1. Add mapping annotations
  2. OrderLine and Product classes must be entities or @Embeddable classes, not simple value objects
  3. Add an empty constructor for each entity or @Embeddable class
  4. Replace Money properties with simple types

Hmm, we need to modify the design of Order aggregate to be able to use JPA. While adding annotations is not a big deal, the other requirements can introduce a lot of problems.

嗯,我们需要修改Order聚合的设计以便能够使用JPA。虽然添加注解不是什么大问题,但其他要求会带来很多问题。

3.1. Changes to the Value Objects

3.1.对价值对象的改变

The first issue of trying to fit an aggregate into JPA is that we need to break the design of our value objects: Their properties can no longer be final, and we need to break encapsulation.

试图将聚合体融入JPA的第一个问题是,我们需要打破我们的价值对象的设计。他们的属性不能再是最终的,而且我们需要打破封装。

We need to add artificial ids to the OrderLine and Product, even if these classes were never designed to have identifiers. We wanted them to be simple value objects.

我们需要为OrderLineProduct,添加人工标识符,即使这些类在设计上从来就没有标识符。我们希望它们是简单的值对象。

It’s possible to use @Embedded and @ElementCollection annotations instead, but this approach can complicate things a lot when using a complex object graph (for example @Embeddable object having another @Embedded property etc.).

可以使用@Embedded@ElementCollection注解来代替,但是当使用复杂的对象图时(例如@Embeddable对象有另一个@Embedded属性等),这种方法会让事情变得非常复杂。

Using @Embedded annotation simply adds flat properties to the parent table. Except that, basic properties (e.g. of String type) still require a setter method, which violates the desired value object design.

使用@Embedded注解只是将平面属性添加到父表中。只是,基本属性(例如String类型)仍然需要一个setter方法,这违反了所需的值对象设计。

Empty constructor requirement forces the value object properties to not be final anymore, breaking an important aspect of our original design. Truth be told, Hibernate can use the private no-args constructor, which mitigates the problem a bit, but it’s still far from being perfect.

空的构造函数要求迫使值对象的属性不再是最终的,破坏了我们最初设计的一个重要方面。说实话,Hibernate可以使用私有的no-args构造函数,这在一定程度上缓解了这个问题,但离完美还很远。

Even when using a private default constructor, we either cannot mark our properties as final or we need to initialize them with default (often null) values inside the default constructor.

即使使用私有的默认构造函数,我们也不能将我们的属性标记为final,或者我们需要在默认构造函数中用默认值(通常是null)初始化它们。

However, if we want to be fully JPA-compliant, we must use at least protected visibility for the default constructor, which means other classes in the same package can create value objects without specifying values of their properties.

然而,如果我们想完全符合JPA的要求,我们必须对默认构造函数至少使用保护可见性,这意味着同一包中的其他类可以在不指定其属性值的情况下创建值对象。

3.2. Complex Types

3.2.复杂类型

Unfortunately, we cannot expect JPA to automatically map third-party complex types into tables. Just see how many changes we had to introduce in the previous section!

不幸的是,我们不能期望JPA自动将第三方复杂类型映射到表中。看看我们在上一节中引入了多少变化就知道了!

For example, when working with our Order aggregate, we’ll encounter difficulties persisting Joda Money fields.

例如,在处理我们的Order聚合时,我们会遇到坚持Joda Money字段的困难。

In such a case, we might end up with writing custom type @Converter available from JPA 2.1. That might require some additional work, though.

在这种情况下,我们最终可能会写出JPA 2.1中的自定义类型@Converter。不过,这可能需要一些额外的工作。

Alternatively, we can also split the Money property into two basic properties. For example String for currency unit and BigDecimal for the actual value.

另外,我们也可以将Money属性分成两个基本属性。例如,String表示货币单位,BigDecimal表示实际价值。

While we can hide the implementation details and still use Money class through the public methods API, the practice shows most developers cannot justify the extra work and would simply degenerate the model to conform to the JPA specification instead.

虽然我们可以隐藏实现细节,并且仍然通过公共方法API使用Money类,但实践表明大多数开发者无法证明额外的工作是合理的,他们会简单地退化模型,以符合JPA规范。

3.3. Conclusion

3.3.结论

While JPA is one of the most adopted specifications in the world, it might not be the best option for persisting our Order aggregate.

虽然JPA是世界上采用最多的规范之一,但它可能不是持久化我们的Order集合的最佳选择。

If we want our model to reflect the true business rules, we should design it to not be a simple 1:1 representation of the underlying tables.

如果我们希望我们的模型能够反映真正的业务规则,我们应该将其设计为不是底层表格的简单的1:1表示。

Basically, we have three options here:

基本上,我们在这里有三个选择。

  1. Create a set of simple data classes and use them to persist and recreate the rich business model. Unfortunately, this might require a lot of extra work.
  2. Accept the limitations of JPA and choose the right compromise.
  3. Consider another technology.

The first option has the biggest potential. In practice, most projects are developed using the second option.

第一个选项具有最大的潜力。在实践中,大多数项目都是采用第二种方案开发的。

Now, let’s consider another technology to persist aggregates.

现在,让我们考虑另一种技术来坚持聚合。

4. Document Store

4.文件存储

A document store is an alternative way of storing data. Instead of using relations and tables, we save whole objects. This makes a document store a potentially perfect candidate for persisting aggregates.

文档存储是一种存储数据的替代方式。我们不使用关系和表格,而是保存整个对象。这使得文档存储有可能成为持久化聚合的完美候选者

For the needs of this tutorial, we’ll focus on JSON-like documents.

为了本教程的需要,我们将专注于类似JSON的文件。

Let’s take a closer look at how our order persistence problem looks in a document store like MongoDB.

让我们仔细看看我们的订单持久性问题在MongoDB这样的文档存储中是怎样的。

4.1. Persisting Aggregate Using MongoDB

4.1.使用MongoDB持久化聚合体

Now, there are quite a few databases which can store JSON data, one of the popular being MongoDB. MongoDB actually stores BSON, or JSON in binary form.

现在,有相当多的数据库可以存储JSON数据,其中一个流行的数据库是MongoDB。MongoDB实际上是存储BSON,或二进制形式的JSON。

Thanks to MongoDB, we can store the Order example aggregate as-is.

由于MongoDB,我们可以将Order示例聚合按原样存储

Before we move on, let’s add the Spring Boot MongoDB starter:

在我们继续之前,让我们添加Spring Boot MongoDB启动器。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

Now we can run a similar test case like in the JPA example, but this time using MongoDB:

现在我们可以运行一个类似于JPA例子的测试案例,但这次是使用MongoDB。

@DisplayName("given order with two line items, when persist using mongo repository, then order is saved")
@Test
void test() throws Exception {
    // given
    Order order = prepareTestOrderWithTwoLineItems();

    // when
    repo.save(order);

    // then
    List<Order> foundOrders = repo.findAll();
    assertThat(foundOrders).hasSize(1);
    List<OrderLine> foundOrderLines = foundOrders.iterator()
      .next()
      .getOrderLines();
    assertThat(foundOrderLines).hasSize(2);
    assertThat(foundOrderLines).containsOnlyElementsOf(order.getOrderLines());
}

What’s important – we didn’t change the original Order aggregate classes at all; no need to create default constructors, setters or custom converter for Money class.

重要的是–我们完全没有改变原有的Order聚合类;不需要为Money类创建默认的构造函数、设置函数或自定义转换器。

And here is what our Order aggregate appears in the store:

这里是我们的Order总量在商店中的显示。

{
  "_id": ObjectId("5bd8535c81c04529f54acd14"),
  "orderLines": [
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "10.00"
          }
        }
      },
      "quantity": 2
    },
    {
      "product": {
        "price": {
          "money": {
            "currency": {
              "code": "USD",
              "numericCode": 840,
              "decimalPlaces": 2
            },
            "amount": "5.00"
          }
        }
      },
      "quantity": 10
    }
  ],
  "totalCost": {
    "money": {
      "currency": {
        "code": "USD",
        "numericCode": 840,
        "decimalPlaces": 2
      },
      "amount": "70.00"
    }
  },
  "_class": "com.baeldung.ddd.order.mongo.Order"
}

This simple BSON document contains the whole Order aggregate in one piece, matching nicely with our original notion that all this should be jointly consistent.

这个简单的BSON文档包含了整个Order集合,与我们最初的概念非常吻合,即所有这些应该是共同一致的。

Note that complex objects in the BSON document are simply serialized as a set of regular JSON properties. Thanks to this, even third-party classes (like Joda Money) can be easily serialized without a need to simplify the model.

请注意,BSON文档中的复杂对象只是被序列化为一组常规的JSON属性。得益于此,即使是第三方类(如Joda Money)也可以轻松地进行序列化,而不需要简化模型。

4.2. Conclusion

4.2.结论

Persisting aggregates using MongoDB is simpler than using JPA.

使用MongoDB坚持聚合比使用JPA更简单。

This absolutely doesn’t mean MongoDB is superior to traditional databases. There are plenty of legitimate cases in which we should not even try to model our classes as aggregates and use a SQL database instead.

这绝对不意味着 MongoDB 优于传统数据库。有很多合理的情况下,我们甚至不应该尝试将我们的类建模为聚合,而是使用 SQL 数据库。

Still, when we’ve identified a group of objects which should be always consistent according to the complex requirements, then using a document store can be a very appealing option.

尽管如此,当我们确定一组对象应该根据复杂的要求始终保持一致时,那么使用文档存储可能是一个非常有吸引力的选择。

5. Conclusion

5.结论

In DDD, aggregates usually contain the most complex objects in the system. Working with them needs a very different approach than in most CRUD applications.

在DDD中,聚合体通常包含系统中最复杂的对象。与它们打交道,需要采取与大多数CRUD应用非常不同的方法。

Using popular ORM solutions might lead to a simplistic or over-exposed domain model, which is often unable to express or enforce intricate business rules.

使用流行的ORM解决方案可能会导致简单化或过度暴露的领域模型,这往往无法表达或执行复杂的业务规则。

Document stores can make it easier to persist aggregates without sacrificing model complexity.

文件存储可以在不牺牲模型复杂性的情况下,更容易地坚持聚合。

The full source code of all the examples is available over on GitHub.

所有例子的完整源代码都可以在GitHub上找到