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

2

21

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)
}
Lune answered 19/9, 2019 at 20:29 Comment(5)
You may check out JDX ORM for Java. JDX is non-intrusive to the domain model (entity) classes. Mapping is defined externally and in a declarative way based on a simple grammar. No annotations polluting your code. No XML complexity. Disclaimer: I am the architect of JDX ORM.Caponize
Have you found some solution? I have the exact same problem. I was thinking of creating a separate domain class and separate data class. I think that makes sense in situations like we have. Check this link: #14025412. But still, even if we create 2 models(domain and data model) I cannot realize how to properly connect them.Narial
@SpasojePetronijević Unfortunately I have not found a satisfactory answer. I am actually, thinking about writing such a library myself by studying the code in Entity Framework and NHibernate. Another possibility is to create a class that can generate XML mapping automatically. Its frustrating though, a mapping configuration class with fluent API like in .NET ORM would be so much better, but we dont have it in Spring and Hibernate. All they have in the JVM world are annotations and XMLs, its 2019 already and lambda expressions have been available since Java 8, Come on...Lune
@LordYggdrasill any news? Some people said they should not use DTO because it is verbose...Alkali
@BraianSilva Unfortunately I have not found a solution to the problem, other than using DTO and writing a lot of boilerplate code, or cope with annotations in domain entities. One day I will write a library similar to Entity Framework's fluent mapping, but until then...Lune
R
8

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.

Rigidify answered 5/4, 2020 at 12:18 Comment(5)
Thats interesting. However, how do you deal with the domain entities creation? Maybe Factories in the infra layer? and if you have to create a child domain entity from within the parent (i.e Forum entity creates a Discussion entity), you need to inject the Factory into the enitity.Pilar
Another approach that I came by, is a separation between the domain logic and the state. so the domain entity has a state, which is an interface that is implemented by the db entity.Pilar
@OfirWinegarten Yes, I use factories defined at the domain layer and implemented at the infra layer. About creating children entities, you can just have an abstract method in the parent domain class taking care of the instantiation. The concrete implementation can take care of dealing with Spring and setting the appropriate references up. Also, the State pattern certainly works here too. I just get annoyed of delegating getters all the time.Rigidify
OK. One last question - how do you test your domain if all the entities are abstract? how do you instantiate them? Mockito? Stubs?Pilar
You have a number of options there, but I usually create test subclasses which return random values in domain-appropriate ranges from all abstract methods, then use Mockito for anything I need that's more specific for each test. Not everyone likes random tests, but I find them quite useful.Rigidify
E
1

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).

Earp answered 17/11, 2019 at 18:35 Comment(1)
I have no idea what you mean by implementing domain/jpa entities mapping with FP like approach. I am not doing FP with ORM, and I do not think its a superior technique anyway.Lune

© 2022 - 2024 — McMap. All rights reserved.