Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot: How to keep DDD entities clean from JPA/Hibernate Annotations?

I am writing an application that I wish to follow the DDD patterns, a typical entity class looks like this:

@Entity
@Table(name = "mydomain_persons")
class Person { 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name="fullname") 
    private String fullName;

    @OneToMany(cascade=ALL, mappedBy="item")
    private Set<Item> items;
}

As you see, since the JPA/Hibernate heavily relies on annotations on entity classes, my domain entity classes are now polluted by persistence-aware annotations. This violates DDD principles, as well as separation of layers. Also it gives me problems with properties unrelated to ORM, such as events. If I use @Transient, it will not initialize a List of events and I have to do this manually or get weird errors.

Id like the domain entity to be a POJO(or POKO as I use Kotlin), so I do not want to have such annotations on the entity class. However I definitely do not wish to use XML configurations, its a horror and the reason why Spring developers moved on to annotations in the first place.

What are the options I have available? Should I define a DTO class that contains such annotations and a Mapper class that converts each DTO into the corresponding Domain Entity? Is this a good practice?

Edit: I know in C# the Entity Framework allows creation of mapping classes outside of Entity classes with Configuration classes, which is a way better alternative than XML hell. I aint sure such technique is available in the JVM world or not, anyone knows the below code can be done with Spring or not?

public class PersonDbContext: DbContext 
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    //Write Fluent API configurations here

    //Property Configurations
    modelBuilder.Entity<Person>().Property(p => p.id).HasColumnName("id").IsRequired();
    modelBuilder.Entity<Person>().Property(p => p.name).hasColumnName("fullname").IsRequired();
    modelBuilder.Entity<Person>().HasMany<Item>(p => p.items).WithOne(i => i.owner).HasForeignKey(i => i.ownerid)
}
like image 486
Lord Yggdrasill Avatar asked Sep 19 '19 20:09

Lord Yggdrasill


2 Answers

The solution I have found for this problem is having abstract domain entities, implemented by my classes at the persistence layer (may or may not be the Hibernate entities themselves). That way, my domain classes know nothing about the persistence mechanisms, my persistence classes know nothing about business logic, and I mostly avoid mapping code. Let me expand on that:

Imagine a project laid out like this (this is pretty much the way I organize my projects):

-
|-business_logic
| |-person
| | |-Person.java
| | |-Item.java  //assuming "item" is inside the Person aggregate
| | |-FullName.java  // Let's make FullName a Value Object.
| | |-DoXWithPersonApplicationService.java
| |-aggregateB
| |-aggregateC
|
|-framework
| |-controllers
| |-repositories
| |-models
| | |-JpaPerson.java
| | |-JpaItem.java
| | |-etc.

Then your Person class might look something like this:

public abstract class Person {
    public abstract int getId();

    public abstract FullName getName();
    protected abstract void setName(FullName name);

    public abstract ImmutableSet<Item> getItems(); // Say you're using Guava
    protected abstract void addItem(String itemName, int qtd);
    protected abstract void removeItem(Item item);

    void doBusinessStuff(String businessArgs) {
        // Run complex domain logic to do business stuff.
        // Uses own getters and setters.
    }

}

Your FullName class might look like this:

public final class FullName {

    private final String firstName;
    private final String lastName;

    // Constructors, factories, getters...

}

And then, finally, your JpaPerson class should look something like:

@Entity
@Table(name = "mydomain_persons")
public class JpaPerson extends Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name="firstName") 
    private String firstName;

    @Column(name="lastName") 
    private String lastName;

    @OneToMany(cascade=ALL, mappedBy="item")
    private Set<Item> items;

    @Override
    public int getId() { return id; }

    @Override
    public FullName getName() { return FullName.of(firstName, lastName); }

    @Override
    protected void setName(FullName name) {
       firstName = name.getFirst();
       lastName = name.getLast();
    }

    // Implementations for the rest of the abstract methods...
    // Notice the complete absence of "business stuff" around here.    
}

A few points to notice:

  1. Anything that modifies entity state is protected, but getters can be public (or not). This makes so it's actually pretty safe to traverse relationships across aggregates to get data you need (entities look just like Value Objects from outside their packages).
  2. Application services that modify state of an aggregate must be inside the same package as the aggregate, because of the above.
  3. Your repositories might have to do a bit of casting, but it should be pretty safe.
  4. All state changes across aggregate boundaries are done with domain events.
  5. Depending on how you set up your FK's, deleting entities from the database can get a bit tricky if you have pre-delete domain logic to run in multiple aggregates, but you really should be thinking twice before doing that anyway.

That's it. I'm sure it's not a silver bullet of any kind, but this pattern has served me well for some time now.

like image 161
gflorio Avatar answered Nov 16 '22 03:11

gflorio


The lack of solution might be a good thing for several reasons. Typically it seems to me pretty sane that domain structures and persitence strategy are decoupled. You may want to apply somme persitence patterns in an independant way regarding the way you design your domain model. You don't care about dealing with legacy tables while designing from the top to the bottom, and you could have jpa entities pretty different from domain entities. And what's the problem with that? So it's not a problem since you keep implementing domain/jpa entities mapping in your repo with a FP like approach,reducing the bolerplate thing and setting aside the side effect to the DAO(s) call(s).

like image 40
Mandrifolie Avatar answered Nov 16 '22 02:11

Mandrifolie