问题
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 theCollection
ProductBacklogItem
could have been removed from theCollection
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?
回答1:
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
来源:https://stackoverflow.com/questions/61541229/aggregate-to-jpa-entity-mapping