DDD:如何正确实现JPA / Hibernate实体关系?

时间:2016-12-26 21:29:55

标签: hibernate jpa domain-driven-design aggregateroot

如果我们遵循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中是不推荐的。

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

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

3 个答案:

答案 0 :(得分:2)

这是一个很好的问题,但您的示例不适用。逻辑上, 类别 不是 产品 汇总根的一部分。 产品 类别 都具有全局ID。删除 产品 时,您不会删除它所属的 类别 ,并且在删除< strong> 类别 ,您不会删除它拥有的所有 产品

Google图书免费提供一个概述Alex Evans's DDD book中汇总使用情况的页面。这是有关聚合的内容:

  

•根ENTITY具有全球性,并最终负责   用于检查不变式。

     

•根实体具有全球性。   边界内的实体具有本地身份,仅在内部具有唯一性   聚集。

     

•AGGREGATE边界之外的任何东西都不能容纳   引用除根ENTITY之外的任何内容。根   ENTITY可以将对内部ENTITIES的引用传递给其他对象,   但是这些对象只能暂时使用它们,并且它们可能无法容纳   继续参考。根可以将VALUE OBJECT的副本交给   另一个对象,发生什么都不重要,因为它是   只是一个VALUE,将不再与   骨料。

     

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

     

•内的物体   AGGREGATE可以保留对其他AGGREGATE根目录的引用。

     

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

     

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

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

  1. @Embeddable似乎是防弹解决方案,因为三方成员将没有ID。
  2. @OneToMany, @JoinTable等-在您不通过其他实体的ID引用成分的情况下也可以使用。不过,在实施过程中需要对此进行保险,并且可能会偶然违反。

答案 1 :(得分:1)

您可以使用连接表来避免汇总根的类别,如下所示:

@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)

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

答案 2 :(得分:0)

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

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;
}