Sauvegarder l’état final d’un agrégat par ses événements

Des aggregats imaginés sous la forme de cellules organiques

Écrit par : Edouard Cattez

27 septembre 2024

craftdddjpa

Pré-requis pour ne pas être trop perdu

  • quelques bases en Domain Driven Design (DDD)
  • quelques bases en JPA / Hibernate

La notion d’agrégat du Domain Driven Design désigne un ensemble cohérent d’objets du domaine qui doit être manipulé comme une seule unité métier. Dans cette logique, les décisions métiers sont prises par l'agrégat et prennent la forme d'événements (Domain Event). L'événement est donc la représentation d'une mutation: l'agrégat a changé d'état.

Je vous propose ainsi de voir comme l'on peut persister l'état final d'un agrégat en ne se basant que sur les décisions qu'il a prises.

L'agrégat

L'exemple que nous allons prendre parle de l'accès d'un utilisateur dont les règles de gestion sont les suivantes:

  • l'accès peut être suspendu
  • si l'accès est suspendu, il ne peut pas être suspendu à nouveau (erreur)
  • l'accès peut être réactivé
java
@Aggregate // Annotation personnalisée à des fins de documentation
class UserAccess {

    // Le mot clé "transient" est faculatif, il me sert surtout à indiquer que les événements ne seront pas persistés en tant que tel
    private final transient List<UserAccessEvent> occurredEvents = new ArrayList<>();

    private final UserId id;
    private Instant suspendedAt;

    public List<UserEvent> occurredEvents() {
        return List.copyOf(occurredEvents);
    }

    public void suspend(Clock now) {
        if (suspendedAt != null) {
            throw new UserAccessIsAlreadySuspended(id);
        }

        UserAccessSuspended event = UserAccessSuspended.builder()
                .userId(id)
                .suspendedAt(now.instant())
                .build();

        // La décision prise par l'aggrégat est stockée
        this.occurredEvents.add(event);

        // L'état interne de l'aggrégat est muté
        this.apply(event);
    }

    private void apply(UserAccessSuspended event) {
        this.suspendedAt = event.suspendedAt();
    }

    // ... d'autres règles ici ...
}

La décision métier est représentée ici de deux manières:

  • l'exception UserAccessIsAlreadySuspended
  • l'événement UserAccessSuspended

Bon à savoir, un événement s'étant déjà produit, une bonne manière de le nommer est de l'écrire au passé. L'exception quant à elle est écrite avec la convention IsXX pour décrire un fait.

Persister l'agrégat

L'agrégat sera manipulé dans la couche application par un service qui couvrira la totalité du geste métier:

  • Récupération de l'agrégat
  • Prise de décision
  • Gestion des erreurs
  • Persistance de l'agrégat

Dans la clean architecture, ce service prend souvent le nom de Use case.

L'agrégat sera récupéré et persisté au travers d'une interface qui va abstraire toute la logique d'infrastructure.

Dans l'architecture hexagonale, cette interface porte le nom de port.

java
interface UserAccessPort {

    UserAccess getById(UserId userId);

    void save(UserAccess access);

}

Implémentation

Nous pourrions exposer un getter pour la propriété suspendedAt. Le problème s'il en est de cette méthode réside dans le fait que l'agrégat se mettrait à exposer tout ou partie de son état interne. D'une certaine manière, la modélisation de notre agrégat serait dépendante de la manière de la consommer. C'est là où les événements jouent un rôle important: ils sont faits pour être consommés.

Pour sauvegarder l'agrégat, nous allons parcourir chaque événement qu'il aura pu créer. Pour chaque événement, nous allons effectuer une opération JPA. A contrario, récupérer l'agrégat depuis la persistance se fera par un mapping direct des propriétés.

java
class UserAccessAdapter implements UserAccessPort {

    private final JpaUserAccesses jpaUserAccesses;

    @Override
    public UserAccess getById(UserId userId) {
        return jpaUserAccesses.findById(userId)
                .map(UserAccessAdapter::toUserAccess)
                .orElseThrow(() -> new UserAccessNotFound(userId));
    }

    private static UserAccess toUserAccess(UserAccessEntity entity) {
        // Ici, on recrée l'agrégat à partir d'un pattern builder
        // Demain, nous pourrions peut être le récréer à partir des événements
        return UserAccess.builder()
                .id(UserId.from(entity.getId()))
                .suspendedAt(entity.getSuspendedAt())
                .build();
    }

    @Override
    public void save(User user) {
        user.occurredEvents().forEach(event -> {
            switch(event) {
                case UserAccessSuspended e -> apply(e);
                case UserAccessUnlocked e -> apply(e);
                // ... other events ...
            }
        });
    }

    private void apply(UserAccessSuspended event) {
        jpaUserAccesses.apply(event);
    }

    private void apply(UserAccessUnlocked event) {
        jpaUserAccesses.apply(event);
    }

}

L’association de HQL et de SPEL nous permet ainsi d'écrire nos requêtes SQL à partir de nos événements.

java
@Repository
interface JpaUserAccesses extends JpaRepository<UserAccessEntity, String> {

    @Modifying
    @Query("""
        UPDATE UserAccess u
        SET u.suspendedAt = :#{#event.suspendedAt()}
        WHERE u.id = :#{#event.userId().value()}
    """)
    void apply(UserAccessSuspended event);

    @Modifying
    @Query("""
        UPDATE UserAccess u
        SET u.suspendedAt = null
        WHERE u.id = :#{#event.userId().value()}
        """)
    void apply(UserAccessUnlocked event);

    // ... apply other events ...
}

Notez qu’il est bien sûr possible d’appliquer les événements en utilisant pleinement l’entity manager de Hibernate plutôt que d’exécuter des UPDATE directement sur la base.

Conclusion

Il n'est pas très compliqué de modéliser les décisions métiers sous forme d'événements. Au travers d'une histoire, nous sommes capables de sauvegarder un état de fait. Serions-nous maintenant capables de reconstruire l'agrégat à partir de ses décisions ? L'event sourcing attendra une prochaine fois.

À propos de l'auteur

Edouard Cattez

Edouard Cattez

Cet article vous a inspiré ?

Vous avez des questions ou des défis techniques suite à cet article ? Nos experts sont là pour vous aider à trouver des solutions concrètes à vos problématiques.