【问题标题】:DDD: how to properly implement with JPA/Hibernate entities relation?DDD:如何正确实现 JPA/Hibernate 实体关系?
【发布时间】:2016-12-26 21:29:54
【问题描述】:

如果我们遵循 DDD 原则,一个聚合根应该只有(通过 id)对另一个聚合根的引用。

例子:

// Product Aggregate root
class Product { 

   // References to categories Aggregate Roots (to ids)
   Set<Long> categoryIds;
}

但是如何使用 JPA/Hibernate 来实现呢? 在 jpa 中,如果我们想拥有例如 OneToMany 关系,我们将其定义如下:

// Product Aggregate root
class Product { 

   // Holds category aggregate roots
   @OneToMany(mappedBy = "", cascade = CascadeType.ALL)
   Set<Category> categories;
}

因此 JPA-s 方法将自己保存类别聚合根,这在 DDD 中不推荐。

如果不符合 DDD 原则,您将如何设计与 JPA 的关系?

P.S.:我正在考虑将 categories 设为字符串类型的属性并保存以逗号分隔的类别 ID 列表,但有没有更好的解决方案?

【问题讨论】:

  • cascade = CascadeType.ALL 正朝着错误的方向前进,因为这会将两个聚合与同一个事务联系起来

标签: hibernate jpa domain-driven-design aggregateroot


【解决方案1】:

这是一个很好的问题,但你的例子不适用。从逻辑上讲,类别不是产品聚合根的一部分。 ProductCategory 都有全局 ID。当您删除一个产品时,您不会删除它所属的类别,当您删除一个Category你不会删除它所有的Products

在 Google 图书中免费提供了总结 Eric Evans's DDD book 的聚合使用情况的页面。以下是关于聚合的内容:

• 根 ENTITY 具有全局身份并最终负责 用于检查不变量。

• 根实体具有全局标识。 边界内的实体具有本地标识,仅在内部唯一 聚合。

• AGGREGATE 边界之外的任何东西都不能容纳 引用里面的任何东西,除了根实体。根 ENTITY 可以将对内部 ENTITIES 的引用传递给其他对象, 但那些对象只能暂时使用它们,它们可能不持有 上参考。 root 可以将 VALUE OBJECT 的副本交给 另一个对象,它发生了什么并不重要,因为它是 只是一个 VALUE,不再与 聚合。

• 作为上一条规则的推论,只有 AGGREGATE 根可以通过数据库查询直接获得。所有其他 对象必须通过关联的遍历才能找到。

• 内的对象 AGGREGATE 可以保存对其他 AGGREGATE 根的引用。

• 删除操作必须删除 AGGREGATE 边界内的所有内容 一次。 (使用垃圾收集,这很容易。因为没有 对除根以外的任何内容的外部引用,删除根并 其他所有内容都将被收集。)

• 当对 AGGREGATE 边界内的任何对象的更改> 提交时,必须满足整个 AGGREGATE 的所有不变量。

关于 JPA 实现,我想说多种方法都可以:

  1. @Embeddable 似乎是一个万无一失的解决方案,因为选民没有 ID。
  2. @OneToMany, @JoinTable 等 - 只要您不通过其他实体的 ID 引用成分,也可以使用。但是,这需要在实施过程中进行保险,并且可能会被意外违反。

【讨论】:

  • 亚历克斯·埃文斯?你的意思是Eric Evans
  • 是的,错字了,我的错。
【解决方案2】:

您可以使用连接表来避免像这样聚合根的类别:

@Entity
public class Product {

    @Id
    @GeneratedValue
    private int id;

    @OneToMany
    @JoinTable
    private Set<Category> categories;

    // constructor, getters, setters, etc...
}


@Entity
public class Category {
    @Id
    @GeneratedValue
    private int id;

    // constructor, getters, setters, etc...
}

作为一个例子,我将几个放在一起:

for (int n = 0; n < 3; ++n) {
    categoryRepository.save(new Category());
}

Set<Category> categories = categoryRepository.findAll();

productRepository.save(new Product(categories));

这会导致以下结果(您没有指定您的 DBMS,所以我只是假设...) MySQL:

MariaDB [so41336455]> show tables;
+----------------------+
| Tables_in_so41336455 |
+----------------------+
| category             |
| product              |
| product_categories   |
+----------------------+
3 rows in set (0.00 sec)

MariaDB [so41336455]> describe category; describe product; describe product_categories;
+-------+---------+------+-----+---------+----------------+
| Field | Type    | Null | Key | Default | Extra          |
+-------+---------+------+-----+---------+----------------+
| id    | int(11) | NO   | PRI | NULL    | auto_increment |
+-------+---------+------+-----+---------+----------------+
1 row in set (0.00 sec)

+-------+---------+------+-----+---------+----------------+
| Field | Type    | Null | Key | Default | Extra          |
+-------+---------+------+-----+---------+----------------+
| id    | int(11) | NO   | PRI | NULL    | auto_increment |
+-------+---------+------+-----+---------+----------------+
1 row in set (0.00 sec)

+---------------+---------+------+-----+---------+-------+
| Field         | Type    | Null | Key | Default | Extra |
+---------------+---------+------+-----+---------+-------+
| product_id    | int(11) | NO   | PRI | NULL    |       |
| categories_id | int(11) | NO   | PRI | NULL    |       |
+---------------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)

当然,他们的内容也不足为奇:

MariaDB [so41336455]> select * from category; select * from product; select * from product_categories;
+----+
| id |
+----+
|  1 |
|  2 |
|  3 |
+----+
3 rows in set (0.00 sec)

+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

+------------+---------------+
| product_id | categories_id |
+------------+---------------+
|          1 |             1 |
|          1 |             2 |
|          1 |             3 |
+------------+---------------+
3 rows in set (0.00 sec)

另外,当您使用关系数据库时,我会避免将关系存储在以逗号分隔的列表中。它会导致不健康的数据库设计,并且在某些时候会让您头疼。

【讨论】:

  • 但同样,Product 拥有一组 Categorys。根据 DDD,这是我们首先应该避免的。
  • 是的,在这种情况下,'Category' 应该只是 CategoryId
【解决方案3】:

在聚合之间导航时最好坚持按身份引用。在调用聚合行为之前使用服务加载所需的对象。例如:

public class MyProductApplicationService {
    ...
    @Transactional
    public void loadDependentDataAndCarryOutAggregateAction(Long productId, Long categoryId) {

        Product product = productRepository.findOne(productId);
        Category category = categoryRepository.findOne(categoryId);

        product.doActionThatNeedsFullCategoryAndMayModifyProduct(category);
    }
}

如果这太麻烦,那么至少不要将事务从一个聚合跨越到另一个聚合:

class Product {

   @OneToMany(mappedBy = "product")
   Set<Category> categories;
}

public class Category {

    @ManyToOne
    @JoinColumn(name = "productid", insertable = false, updatable = false)
    private Product product;
}

【讨论】:

  • 谢谢,但在第一种方法中,您将在哪里存储产品的类别参考?
  • @moreo 只需将@Column private Long productId 添加到Category
猜你喜欢
  • 2021-05-22
  • 1970-01-01
  • 2016-04-01
  • 2021-12-21
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多