Le code
@Entity
public class SessionEnrollment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(mappedBy = "sessionEnrollment", cascade = CascadeType.ALL, optional = true)
private Feedback feedback;
public void addFeedback(String comment, int rating) {
if (this.getFeedback() != null) throw new DomainException("Feedback already enrolled");
if (comment == null || comment.isBlank()) throw new DomainException("Comment is required");
if (rating < 0 || rating > 5) throw new DomainException("Rating must be between 0 and 5");
if (!session.isSessionComplete()) {
throw new DomainException("Session not completed");
}
Feedback feedback = new Feedback(rating, comment);
feedback.setCompany(session.getTraining().getCompany());
feedback.setComment(comment);
feedback.setRating(rating);
feedback.setSessionEnrollment(this);
this.feedback = feedback;
}
}
public class FeedbackService {
@Transactional
public void giveFeedback(FeedbackRequestModel feedbackRequestModel) {
SessionEnrollment sessionEnrollment = sessionEnrollmentRepository.findByFeedbackToken(feedbackRequestModel.getAccessToken()).orElseThrow(() -> new DomainException("Invalid feedback token"));
sessionEnrollment.addFeedback(feedbackRequestModel.getComment(), feedbackRequestModel.getRating());
sessionEnrollmentRepository.save(sessionEnrollment);
}
}Le problème
Nous avons deux agrégats (SessionEnrollment et Feedback), mais on continue à traiter Feedback comme un enfant interne de SessionEnrollment (référence d’entité bidirectionnelle, cascade et invariants imposés par le parent). Vernon recommande explicitement que les agrégats références à d’autres agrégats uniquement par leur identifiant (id).
Solution : Feedback dans son propre Agrégat
- Dans SessionEnrollment supprimer la relation vers Feedback
- Dans Feedback remplacer la relation par
Long sessionEnrollmentId; - Dans la couche Application Service orchestrer la création d’un feedback
Feedback Entity
@Entity
public class Feedback {
@Id private UUID id;
private UUID sessionEnrollmentId;
private int rating;
private String comment;
public static Feedback create(UUID enrollmentId, String comment, int rating) { ... }
}
Application Service Layer
@Service
public class FeedbackService {
@Transactional
public void giveFeedback(FeedbackRequestModel req) {
var enr = enrollmentRepo.findByFeedbackToken(req.getAccessToken())
.orElseThrow(...);
boolean sessionCompleted = sessionService.isCompleted(enr.getSessionId());
if(!sessionCompleted) {
throw new BusinessException(...)
}
var fb = enr.createFeedback(req.getComment(), req.getRating(), sessionCompleted);
feedbackRepo.save(fb); // separate repo
}
}Pourquoi avons-nous ce problème ?
Ressource
On essaie de mixer les bonnes pratiques JPA et DDD :
- JPA avoir une relation bidirectionnelle entre nos entités
- Tandis que DDD nous dit de référencer une entité d’un agrégat extérieur uniquement par son id.
Une première solution consisterait à séparer le Domain Model du Persistence Model (architecture hexagonale stricte). Mais cela implique la création d’adaptateur et beaucoup de code boiterplate (https://stackoverflow.com/questions/31400432/ddd-domain-entities-vo-and-jpa)
Une seconde solution est d’utiliser @JoinColumn
@Entity
public class Feedback {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Store the ID directly
@Column(name = "session_enrollment_id")
private Long sessionEnrollmentId;
// Optional: Keep JPA relationship for queries but don't use in domain logic
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "session_enrollment_id", insertable = false, updatable = false)
private SessionEnrollment sessionEnrollment;
}