DDD: how to properly implement with JPA/Hibernate entities relation?

4.8k views Asked by At

If we follow DDD principles, one aggregate root should have only references (by id) to another aggregate root(s).

Example:

// Product Aggregate root
class Product { 

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

But how can it be achieved with JPA/Hibernate? In jpa, if we want to have, for example, OneToMany relation, we defined it as follow:

// Product Aggregate root
class Product { 

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

So JPA-s approach will hold category aggregate roots itself, which is not recommended in DDD.

How would you design relations with JPA but to fit DDD principles?

P.S.: I was thinking to make categories property of string type and hold comma separated list of category ids, but is there any better solution?

3

There are 3 answers

2
oschlueter On

You could use a join table to avoid the categories aggregating the roots like this:

@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...
}

Just as an example I'll plug a few together:

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

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

productRepository.save(new Product(categories));

Which results in the following (you didn't specify your DBMS, so I just assumed...) 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)

And of course no surprise with respect to their content:

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)

Also I would avoid storing relations in a comma-separated list when you're using a relational database. It leads to unhealthy database design and will cause you headaches at some point.

2
Markus Pscheidt On

It's best to stick with referencing by identity when navigating between aggregates. Use a service to load required objects before calling aggregate behavior. For example:

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

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

        product.doActionThatNeedsFullCategoryAndMayModifyProduct(category);
    }
}

If that is too cumbersome, then at least do not span transactions from one aggregate to another:

class Product {

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

public class Category {

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

It’s a good question, but your example is not applicable. Logically a Category is not part of a Product Aggregate Root. Both Product and Category have global IDs. When you delete a Product, you would not delete the Categories that it belongs to and when you delete a Category you would not delete all the Products that it has.

A page that summarizes Aggregates usage from Eric Evans's DDD book is available for free at Google Books. Here is what it says about Aggregates:

• The root ENTITY has global identity and is ultimately responsible for checking invariants.

• Root ENTITIES have global identity. ENTITIES inside the boundary have local identity, unique only within the AGGREGATE.

• Nothing outside the AGGREGATE boundary can hold a reference to anything inside, except to the root ENTITY. The root ENTITY can hand references to the internal ENTITIES to other objects, but those objects can use them only transiently, and they may not hold on to the reference. The root may hand a copy of a VALUE OBJECT to another object, and it doesn't matter what happens to it, because it's just a VALUE and no longer will have any association with the AGGREGATE.

• As a corollary to the previous rule, only AGGREGATE roots can be obtained directly with database queries. All other objects must be found by traversal of associations.

• Objects within the AGGREGATE can hold references to other AGGREGATE roots.

• A delete operation must remove everything within the AGGREGATE boundary at once. (With garbage collection, this is easy. Because there are no outside references to anything but the root, delete the root and everything else will be collected.)

• When a change to any object> within the AGGREGATE boundary is committed, all invariants of the whole AGGREGATE must be satisfied.

Regarding the JPA implementation I would say that multiple approaches would work:

  1. @Embeddable seems like a bulletproofed solution because the constituents will not have IDs.
  2. @OneToMany, @JoinTable, etc. - also works as far as you don't reference constituents by IDs from other entities. This needs to be insured during implementation though, and can be violated accidentally.