Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 436
Teimuraz Avatar asked Dec 26 '16 21:12

Teimuraz


People also ask

What are the key JPA classes for interaction with a database?

EntityManager is the class that performs database interactions in JPA.

Which method can be used to store records in a database in JPA?

In JPA, we can easily insert data into database through entities. The EntityManager provides persist() method to insert records.

Is used for persistence of the data in JPA?

Hibernate is an Object-Relational Mapping (ORM) tool which is used to save the state of Java object into the database. It is just a specification. Various ORM tools implement it for data persistence. It is one of the most frequently used JPA implementation.


2 Answers

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.
like image 121
Alex T Avatar answered Sep 29 '22 17:09

Alex T


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.

like image 26
oschlueter Avatar answered Sep 29 '22 16:09

oschlueter