Aggregate to JPA Entity mapping
Asked Answered
E

3

7

In a DDD-project I'm contributing to, we're seeking for some convenient solutions to map entity objects to domain objects and visa versa.

Developers of this project agreed to fully decouple domain model from data model. The data layer uses JPA (Hibernate) as persistence technology.

As we all reckon that persistence is an implementation detail in DDD, from a developers' point of view we're all seeking for the most appropriate solution in every aspect of the application.

The biggest concern we're having is when an aggregate, containing a list of entities, is mapped to a JPA entity that in it's turn contains a one-to-many relationship.

Take a look at the example below:

Domain model

public class Product extends Aggregate {
    private ProductId productId;
    private Set<ProductBacklogItem> backlogItems;

    // constructor & methods omitted for brevity
}

public class ProductBacklogItem extends DomainEntity {
    private BacklogItemId backlogItemId;
    private int ordering;
    private ProductId productId;

    // constructor & methods omitted for brevity
}

Data model

public class ProductJpaEntity {
    private String productId;
    @OneToMany
    private Set<ProductBacklogItemJpaEntity> backlogItems;

    // constructor & methods omitted for brevity
}

public class ProductBacklogItemJpaEntity {
    private String backlogItemId;
    private int ordering;
    private String productId;

    // constructor & methods omitted for brevity
}

Repository

public interface ProductRepository {        
    Product findBy(ProductId productId);
    void save(Product product);
}

class ProductJpaRepository implements ProductRepository {        
    @Override
    public Product findBy(ProductId productId) {
        ProductJpaEntity entity = // lookup entity by productId

        ProductBacklogItemJpaEntity backlogItemEntities = entity.getBacklogItemEntities();        
        Set<ProductBacklogItem> backlogItems = toBackLogItems(backlogItemEntities);

        return new Product(new ProductId(entity.getProductId()), backlogItems);
    }

    @Override
    public void save(Product product) {
        ProductJpaEntity entity = // lookup entity by productId

        if (entity == null) {
          // map Product and ProductBacklogItems to their corresponding entities and save
          return;
        }

        Set<ProductBacklogItem> backlogItems = product.getProductBacklogItems();
        // how do we know which backlogItems are: new, deleted or adapted...?
    }
}

When a ProductJpaEntity already exists in DB, we need to update everything. In case of an update, ProductJpaEntity is already available in Hibernate PersistenceContext. However, we need to figure out which ProductBacklogItems are changed.

More specifically:

  • ProductBacklogItem could have been added to the Collection
  • ProductBacklogItem could have been removed from the Collection

Each ProductBacklogItemJpaEntity has a Primary Key pointing to the ProductJpaEntity. It seems that the only way to detect new or removed ProductBacklogItems is to match them by Primary Key. However, primary keys don't belong in the domain model...

There's also the possibility to first remove all ProductBacklogItemJpaEntity instances (which are present in DB) of a ProductJpaEntity, flush to DB, create new ProductBacklogItemJpaEntity instances and save them to DB. This would be a bad solution. Every save of a Product would lead to several delete and insert statements in DB.

Which solution exists to solve this problem without making too many sacrifices on Domain & Data model?

Eliseoelish answered 1/5, 2020 at 11:3 Comment(1)
Your ProductBacklogItemJpaEntity should be an @Entity having its own @PrimaryKey, then when you receive the Product just transform the list of ProductBacklogItem into a list of ProductBacklogItemJpaEntity set it into the ProductJpaEntity and save it. This works if the list in the Product domain object contains all the backlog items when you call the repository save method (spoiler, it is a must). If this is not your case some details on when that save method is used are needed in order to respond.Forby
C
1

You can let JPA/Hibernate solve problem for you.

public void save(Product product) {
    ProductJpaEntity entity = convertToJpa(product);
    entityManager.merge(entity);
    // I think that actually save(entity) would call merge for you,
    // if it notices that this entity already exists in database
}

What this will do is:

  • It will take your newly created JPA Entity and attach it
  • It will examine what is in database and update all relations accordingly, with priority given to your created entity (if mappings are set correctly)
Chronometry answered 16/8, 2022 at 22:21 Comment(0)
D
0

This is a perfect use case for Blaze-Persistence Entity Views.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

Entity views can also be updatable and/or creatable i.e. support flushing changes back, which can be used as a basis for a DDD design. Updatable entity views implement dirty state tracking. You can introspect the actual changes or flush changed values.

You can define your updatable entity views as abstract classes to hide "implementation specifics" like e.g. the primary key behind the protected modifier like this:

@UpdatableEntityView
@EntityView(ProductJpaEntity.class)
public abstract class Product extends Aggregate {
    @IdMapping
    protected abstract ProductId getProductId();
    public abstract Set<ProductBacklogItem> getBacklogItems();
}
@UpdatableEntityView
@EntityView(ProductBacklogItemJpaEntity.class)
public abstract class ProductBacklogItem extends DomainEntity {
    @IdMapping
    protected abstract BacklogItemId getBacklogItemId();
    protected abstract ProductId getProductId();
    public abstract int getOrdering();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

Product p = entityViewManager.find(entityManager, Product.class, id);

Saving i.e. flushing changes is easy as well

entityViewManager.save(entityManager, product);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features and for flushing changes, you can define a save method in your repository that accepts the updatable entity view

Donnelldonnelly answered 2/5, 2020 at 14:52 Comment(6)
The library looks interesting, but in above example I see the problem, that the view classes will still depend on the Jpa classes through the annontation. The idea of the question's approach (and behind onion architecture) is to avoid this dependency.Coben
How do you want to avoid the dependency? At some point you must connect the models somehow. Since this dependency is on annotation level only, you can use the runtime dependency scope so consumers of your domain model don't need to depend on your persistence model.Donnelldonnelly
Absolutely agree with @robbit. @christian-beikov, you should not mess your domain model with your data model. Not even with annotations. You want to make your model independent and decoupled of... where and how it will be persisted. Because of that, the interface for repository public interface ProductRepository and all those [model]JpaEntity classes in place. In a long run, if you start to mix all those notations into your model to avoid the creation of JpaEntities, other problems you will go through.Rossi
I my eyes, abstraction is a tool to manage complexity, but it seems to me that you are being very dogmatic about this. What does this "complete separation" gain you? You just move the details of how the two things(domain/persistence) connect somewhere else. Such an external mapping also is usually hard to read/write, which is why the popularity of JPA XML mappings is more and more declining. Annotations won, because they put the mapping where it belongs, on the Java source elements. YMMV, but please think about the "real" benefit of complete separation.Donnelldonnelly
It should work the other way around. You should be able to annotate your JPA entity classes so they depend on the domain classes. This way your domain classes would be framework agnostic.Benitobenjamen
Annotating anything other than the domain classes essentially is external configuration where you have to specify names (domain type and attribute names) for every kind of mapping. When annotating the domain model directly, you can avoid all this indirection and when navigating to your domain model classes, immediately see how it is mapped to your persistence. It's all opinion based and every approach has pros and cons, but it seems the industry mostly accepted that annotations are just metadata and are too convenient to be replaced with e.g. XML.Donnelldonnelly
R
0

I believe you need to address the issue in a different way. It is really hard to determine which has been changed when you have a complex graph of objects. However, there should be someone else (maybe a service) which really knows what have changed in advance.

In fact, I did not see in your question the real business "Service" or a class which address the business logic. This will be the one who can solve this issue. As a result, you will have in your repository something more specific removeProductBacklogItem(BacklogItemId idToRemove) or... addProductBacklogItem(ProductId toProductId, ProductBacklogItem itemToAdd). That will force you to manage and identify changes in other way... and the service will be responsible for.

Rossi answered 29/6, 2022 at 10:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.