mardi 18 janvier 2022

Sauvegarder un formulaire avec un appel ajax avec le framework Thymeleaf

Dans cet exemple qui utilise Thymeleaf, nous allons effectuer un post en ajax. Les données envoyés sont de type json. Ce qui a un impact sur le body devant être envoyé et sur l'annotation à employé dans le controller spring.

 Le formulaire est composé de 3 champs.

<form class="row g-3" id="createAdsForm" th:action="@{/ads}">
<input name="id" type="hidden"/>
<div class="col-md-12">
<label for="adsTitle" class="form-label" th:text="#{ads.title}">Title</label>
<input type="text" class="form-control" id="adsTitle" name="title">
</div>

<div class="col-md-12">
<label for="descriptionAds" class="form-label" th:text="#{ads.description}">Example textarea</label>
<textarea class="form-control" id="descriptionAds" name="description" rows="3"></textarea>
</div>

<div class="col-12">
<button type="submit" class="btn btn-primary" th:text="#{ads.publish}">Publié</button>
</div>

</form>

 

FormDataJson provient du dépot: 

https://github.com/brainfoolong/form-data-json

document.addEventListener('DOMContentLoaded', function () {

let createAdsForm = document.getElementById("createAdsForm");

createAdsForm.addEventListener("submit", function(event) {
event.preventDefault();

let jsonSearchAdsForm = FormDataJson.toJson("#createAdsForm", { arrayify: false , flatList: false});
console.log(jsonSearchAdsForm);
fetch(createAdsForm.action, {
method: "post",
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonSearchAdsForm)
}).then(result => {
if (result.status != 200) {
throw new Error("Bad Server Response");
}
return result.text();

}).then((content) => {
console.log(content);
}).catch((error) => {
console.log(error);
});

});

});

 Au niveau du controller de Spring

@Controller
public class AdsController {

@ResponseBody
@PostMapping("/ads")
public ResponseEntity saveAds(@RequestBody AdsForm adsForm) {

return new ResponseEntity<>(HttpStatus.CREATED);
}

}

 

 Si les données n'auraient pas été envoyé en json, du côté de spring, l'annotation ModelAttribute aurait dû être utilisé.

vendredi 17 décembre 2021

Le patron décorateur avec Thymeleaf

Dans le précédent article de thymeleaf, nous avons utilisé l'approche par défaut des templates. Nous avions une page principal, product et about. Pour chacune d'elle, nous avions un fragment menu, footer. Tous ces pages avaient une structure similaire. 

Nous allons maintenant utiliser une approche différente. Nous allons utiliser le patron décorateur.

Nous allons utiliser le même exemple, mais avec cette approche.Le header sera mis dans un fragment cette fois.

Les sources sont disponibles sur github

Le fragment header

<head th:fragment="my-header">
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all" href="../../css/main.css" th:href="@{/css/main.css}" />
</head>

 

Layout Decorator

Nous allons créer un fichier main-layout avec les sections similaires au niveau des fragments.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<head th:replace="fragments/header :: my-header">
<body>
<div th:replace="fragments/menu :: my-menu"></div>
<div class="container" id="mainContent">
<div layout:fragment="content"></div>
</div>
<div th:replace="fragments/footer :: my-footer"></div>
</body>
</html>

Les templates

Le fichier products ressemblera a 

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="layout/mainLayout">
<head>
<title>Products</title>
</head>
<body>
<div layout:fragment="content">
<table>
<tr>
<td>Id</td>
<td>Name</td>
<td>Description</td>
<td>Price</td>
</tr>
<tr th:each="product: ${products}">
<td th:text="${product.id}"/>
<td th:text="${product.name}"/>
<td th:text="${product.description}"/>
<td th:text="${product.price}"/>
</tr>
</table>
</div>
</body>
</html>
 

Le fichier about ressemblera a 

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="layout/mainLayout">
<head>
<title>About</title>
</head>
<body>
<div layout:fragment="content">
<p>Grocery exist since 1985</p>
</div>
</body>
</html>

 
Il suffit d'indiquer dans nos templates  dans la section layout:decorate celui qu'on désire utiliser.


Le patron décorateur n'est pas l'approche standard de thymeleaf. Cependant, c'est celle qui demeure la plus efficace pour une application de grande envergure.

jeudi 16 décembre 2021

La puissance des fragments avec Thymeleaf

Nous allons traiter une des particularités de Thymleaf, les fragments. Nous avons vue jusqu'à maintenant les templates.

Nous utiliserons spring boot et la structure du projet sera. Les sources sont disponible ici.

 

 

Un template peut être vue comme une page html, javascript, css. 

Par exemple, dans un fichier index.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/main.css" th:href="@{/css/main.css}" />
  </head>

  <body>
    <p th:text="#{home.welcome}">Welcome to our grocery store!</p>
    <p>Today is: <span th:text="${today}">13 February 2011</span></p>
  </body>

</html>
 

Un fragment est une portion de code qui peut être réutilisé dans un autre fragment ou template. Ils peuvent être accessible via un nom, id ou bien par un selector (comme un peu jquery).

Dans un fichier html, par exemple: menu.html

    <ul th:fragment="my-menu">
        <li><a th:href="@{/}">Home</a></li>
        <li><a th:href="@{/products}">Products</a></li>
        <li><a th:href="@{/about}">About</a></li>
    </ul>
    Ce Texte sera affiché aussi.   

Nous pourrions avoir un template pour products et un autre pour about et ce fragment serait utilisé dans chacun d'eux.

Pour utiliser ce fragment dans le fichier index.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/main.css" th:href="@{/css/main.css}" />
  </head>

  <body>
    <div th:replace="fragments/menu.html"/> 
    <p>
<span th:text="#{today}">Today</span>
<span th:text="${today}">16 december 2021</span>
</p> </body> </html>

Le nom du fragment a été utilisé, cette approche est utile uniquement si le fichier html contient totalement ce qui nous intéresse, car tous le fichier sera affiché.

Si nous désirons qu'une portion il faut utiliser un sélector, cette opération se fait via les deux points.

Nous devons préciser l'emplacement du fragment / le nom du fichier du fragment :: le nom du fragment.

Ce qui donne dans notre cas

fragments/menu :: my-menu
 

Un autre fragment a été ajouté pour le footer. Le fichier donne avec les fragments.


<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/main.css" th:href="@{/css/main.css}" />
  </head>

  <body>
    <div th:replace="fragments/menu :: my-menu"/> 
    <p>
<span th:text="#{today}">Today</span>
<span th:text="${today}">16 december 2021</span>
</p>
    <div th:replace="fragments/footer :: my-footer"/>  
  </body>

</html>

Pour le fichier about, il pourrait ressembler à

 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
   <title>Good Thymes Virtual Grocery</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" type="text/css" media="all"
href="../../css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
   <div th:replace="fragments/menu.html"/>
   <p>Grocery exist since 1985</p>
   <div th:replace="fragments/footer.html"/>
</body>
</html>

Pour le fichier products, la partie backend nous retournerais une liste de produit.

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

<head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link rel="stylesheet" type="text/css" media="all"
          href="../../css/main.css" th:href="@{/css/main.css}"/>
</head>

<body>
<div th:replace="fragments/menu.html"/>

<table>
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>Description</td>
        <td>Price</td>
    </tr>
    <tr th:each="product: ${products}">
        <td th:text="${product.id}"/>
        <td th:text="${product.name}"/>
        <td th:text="${product.description}"/>
        <td th:text="${product.price}"/>
    </tr>
</table>
<div th:replace="fragments/footer.html"/>
</body>

</html>
 

C'est assez simple. Il suffit de créer à chaque fois une page html et d'insérer les différents fragments qui nous intéresse. 

Dans notre exemple, la partie head pourrait être un fragment.

Dans une application web complexe, il faut bien cerner les portions qui peuvent être réutiliser et les définir comme des fragments pour éviter la réécriture de code et faciliter la maintenance.

Cette approche d'utiliser les templates est celle par défaut au sein de thymeleaf. Lorsque le site possède plusieurs dizaines, voir centaines de pages, d'autre possibilités sont offerte pour éviter de devoir réécrire les sections redondantes à une multitude de page. Nous la verrons dans un prochain articles.





lundi 1 novembre 2021

Les transactions en JPA avec Springs

Comme bien souvent avec le framework spring, une annotation permet de lui confier une tâche. Dans cet article, nous détaillerons quelques points qu'il faut faire attention afin d'obtenir les résultats escompté.

Transaction

Les transactions sont disponibles dans la majorité des bases de données. Mais qu'est-ce qu'une transaction. C'est une opération avec un début et une fin. Elle est définie par 4 propriétés qui sont désignés par l'acronyme ACID.

Atomicité

Tous les opérations qui composent la transaction sont soit réussite soit en échec. C'est tout ou rien.

Cohérence

La transaction doit emmener le système valide dans un état valide

Isolation

La transaction doit s'exécuter comme si seul elle était seul à fonctionner.

Durabilité

Les modifications apportés lors d'une transaction doivent être conservés.

Spring permet de respecté ces propriétés avec sa gestion des transactions.

Nous utiliserons spring boot avec Hibernate comme implémentation de JPA.

Dans le fichier application.properties ajouter ces lignes

logging.level.ROOT=INFO
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.transaction=DEBUG
 
# for Hibernate only
logging.level.org.hibernate.engine.transaction.internal.
TransactionImpl=DEBUG

 
afin d'avoir plus de détails sur les opérations effectués. 

Particularité de spring

Spring ignore les méthodes private


@Service
public class ProductService{
 
    private final ClientRepository clientRepository;
    
    public ProductService(ClientRepository clientRepository){
         this.clientRepository=clientRepository;
    }
 
    public void mainClient(){
         Client client = new Client();
         saveClient(client);
    }

   @Transactional(propagation = Propagation.REQUIRES_NEW)
    private long saveClient(Client client){
        clientRepository.save(client);
        return clientRepository.count();
    }
    
}

Dans les logs générés, il y a pas de transactions qui exécute saveClient comme un tout.
L'opération save et count sont exécuté dans des transactions séparés.

L'annotation @Transactional fonctionne uniquement sur les méthodes public.
De plus, cela ne fonctionnera pas si l'annotation est sur une méthode dans une classe où elle est aussi appelé.

Afin de palier à cette particularité de Spring, une nouvelle classe peut être créé.

@Service
public class ProductHelperService{
 
    private final ClientRepository clientRepository;
    
    public ProductHelperService(ClientRepository clientRepository){
         this.clientRepository=clientRepository;
    }

   @Transactional(propagation = Propagation.REQUIRES_NEW)
    private long saveClient(Client client){
        clientRepository.save(client);
        return clientRepository.count();
    }
    
}
 
On doit aussi modifié notre classe ProductService
 
@Service
public class ProductService{
 
    private final ProductHelperService productHelperService;
    
    public ProductService(ProductHelperService productHelperService){
         this.productHelperService=productHelperService;
    }
 
    public void mainClient(){
         Client client = new Client();
         productHelperService.saveClient(client);
    }
    
}
 
Les logs afficheront maintenant les deux opérations dans la même transaction. 

Méthode manuellement créer dans une interface

Ces méthodes ne sont pas transactionnels par défaut.
 
@Repository
public interface ClientRepository extends JpaRepository<Client, Long>{
    @Query("select c from Client c where c.firstname = ?1")
    public Client getByFirstname(String firstname) ;

Aucune transaction n'est créé, idem si vous utilisez les requêtes générés par le Spring Data Query Builder

@Repository
public interface ClientRepository extends JpaRepository<Client, Long>{
    @Query("select c from Client c where c.firstname = ?1")
    public Client getByFirstname(String firstname) ;

    public Client findByFirstName(String firstname);
}

L'exécution de la méthode findByFirstName ne sera pas faite dans une transaction

Voici un autre cas

@Modifying
@Query("DELETE FROM Client c WHERE a.genre <> ?1")
public int deleteByNeGenre(String genre);

Dans une classe 
 
public void deleteClientByGenre(String genre){
     clientRepository.deleteByNeGenre(genre);
}

En exécutant deleteClientByGenre, vous allerz obtenir une erreur qu'une transaction est requis.
Vous pouvez ajouter l'annotation @Transactional à la méthode
deleteClientByGenre.
 
Si vous désirez exécuter différentes opérations dans une méthode, ajouter l'annotation @Transactional
 
@Transactional
public void findClientAndUpdate(Long id){
     Client client = clientRepository.findById(id). orElseThrow();
     client.modifiedDate(LocalDate.now());
}

Puisque qu'un champs de l'entité est modifié, elle sera automatiquement sauvegardé. Rien ne vous empêche d'ajouter


clientRepository.save(client);
 
si vous trouvez cela plus compréhensible. 

Les deux opérations seront exécuter dans la même transaction.

Bonne pratique

Toute opération au niveau du repository devrait être dans une transaction. Cette pratique permet d'éviter d'avoir de grosse transaction bloquante.


@Repository
@Transactional(readOnly=true)
public interface ClientRepository extends JpaRepository<Client, Long>{
    @Query("select c from Client c where c.firstname = ?1")
    public Client getByFirstname(String firstname) ;

    public Client findByFirstName(String firstname);

    @Transactional
    @Modifying
    @Query("DELETE FROM Client c WHERE a.genre <> ?1")
    public int deleteByNeGenre(String genre);
}

Dans une classe de service on va appeler ce repository et simuler un long appel

@Transactional
public void longCallTransaction(){
    Thread.sleep(50000); //50 secondes
    Client client = clientRepository.getByFirstname("Paul");
     clientRepository.deleteByNeGenre("Entreprise");
}

Dans les logs, vous pouvez remarquer qu'une transaction est démarré au niveau de la classe de service. Spring invite celle relatif au repository de participer à celle déjà existante. Spring attend l'appel au repository avant d'aquérir la connection à la bd. Cette particularité permet d'éviter une longue transaction.

En ajoutant l'annotation Transaction au niveau du repository, vous évitez de potentiel erreur.


@Repository
@Transactional(readOnly=true)
public interface ClientRepository extends JpaRepository<Client, Long>{
    @Query("select c from Client c where c.firstname = ?1")
    public Client getByFirstname(String firstname) ;
}

@Service
public class ProductService{
 
    private final ClientRepository clientRepository;
    
    public ProductService(ClientRepository clientRepository){
         this.clientRepository=clientRepository;
    }
 
    public void mainClient(){
         Client client = fetchClient
         System.out.println(client);
    }

   @Transactional(readOnly = true)
    private Client fetchClient(){
        return clientRepository.getByFirstname("Paul");
    }
    
}
 
Si on appelle la méthode mainClient, cette dernière appelle fetchClient qui a l'annotation Transactional. Cependant c'est un appel dans la même classe, Spring va ignorer la transaction. Par chance au niveau du repository, l'annotation est là et une transaction sera bien créé.
 

Cascading et les transactions 

Imaginons une classe Auteur et Livre qui ont une relation bidirectionnel de type lazy. En persistant un auteur, les livres seront aussi sauvegardé.
 
public saveAuteur(){
 
    Auteur auteur = new Auteur
    auteur.prenom="Martin";
 
    Livre livre1 = new Livre();

    Livre livre2 = new Livre();
 
    auteur.addLivre(livre1);
    auteur.addLivre(livre2);
 
    auteurRepository.save(auteur);
 
L'appel à saveAuteur va générer 3 insert. Spring va mettre tous les mettres dans la même transaction. Si un insert échoue, la transaction sera annulée.

mardi 12 octobre 2021

Lenovo mini5i et Linux

J'ai récemment fait l'acquisiton d'un Lenovo mini 5i au lieu d'un ordinateur portable qui ont de plus en plus de composant soudé.

Un petit intel Core i3-10100, qui est au final autrement plus puissant que le i5-5300U de mon T450S

Tous les composants sont accessible: ssd, disque dur, carte wifi, mémoire vive. J'ai mis 32 gig de ram. Le ssd a été remplacé par un WD_BLACK  SN850 compatible pcie 4.Ordinateur portable IdeaCentre mini 5i | Lenovo CanadaTous les composants de la machines sont bien supportés par linux. Suse tumbleweed y a été installé sans aucune anomalie.

La machine possède plusieurs ports USB 3.2. Au niveau graphique, il y a un port display port et hdmi 1.4b.

Au démarrage, il y a quelques erreur dans les logs

CPI BIOS Error (bug): Failure creating named object [\_SB.PCI0.XHC.RHUB.GPLD], AE_ALREADY_EXISTS (20210331/dswload2-326) 

ACPI Error: AE_ALREADY_EXISTS, During name lookup/catalog (20210331/psobject-220)
ACPI: Skipping parse of AML opcode: Method (0x0014)
ACPI BIOS Error (bug): Failure creating named object [\_SB.PCI0.XHC.RHUB.TPLD], AE_ALREADY_EXISTS (20210331/dswload2-326)
ACPI Error: AE_ALREADY_EXISTS, During name lookup/catalog (20210331/psobject-220) 

La mise en veille est effectué avec succès. Je n'ai pas remarqué de différence en terme de performance que je démarre sous Linux ou Windows.

Afin d'améliorer les performances, thermald peut être installé, ce n'est pas fait par défaut sous tumbleweed.

J'arrive sans peine à faire du montage vidéo, développement informatique et faire du traitement sur base de données.

Un point négatif est l'absence de port pour les cartes mémoire. J'ai résolue en acheté un lecteur Unitek usb 3.1.

Au final une petite machine professionel, bien supporté sous Linux et qui est silentieuse.


jeudi 31 décembre 2020

Architecture hexagonale

Introduction

Dans une application conventionnel, il ne sera pas rare d'avoir une couche de contrôleur,  de service et de dao. Pour chacune de nos entités, une classe existera sous chacune de ces couches. Il est possible de se retrouver rapidement avec des centaines de classes. Ce type d'architecture utilise un modèle d'objet anémique, c'est-à-dire qu'il ne contient pas de logique métier.

Il n'est pas rare de voir un appel à la couche de persistance directement de la première couche.

Les entités, les domains object n'ont pas de règle métier. Elle est alors mise dans les classes de service, utilitaire. Ces classes chargent des entités, modifient leurs états et persistent le tout. Les services commencent à utiliser d'autres services et il n'est pas rare de se retrouver avec des classes de plusieurs milliers de lignes. Pour une petite application, de type CRUD, ça peut être plus rapide à développer, mais imaginer maintenant une application avec 200K, 300K... lignes de code.

C'est une architecture souvent basée sur le design de la base de données. Une modification au sein de celle-ci et des modifications à chaque couche est nécessaire.

Plus l'application prendra de l'expansion, plus il deviendra difficile de la modifier sans causer de multiples anomalies.

Une bonne architecture doit pouvoir s'adapter à de nouvelles exigences sans tout chambouler.

Une architecture hexagonale pourrait être une approche à songer pour débuter sans trop devoir tout changer si on désirait passer à des microservices par la suite.

Architecture hexagonale

Le but de ce type d'architecture est d'avoir un noyau relié à la logique métier. Tout le reste est externalisé tel que la sauvegarde de donnée ou bien la réception de donnée. Cette conception est priorisée en DDD: domain-driven design.  

 


Avantages

  • Indépendance de la couche métier aux autres couches, technologie
  • Test plus facile à effectuer

 Désavantage

  • Plus de code nécessaire
  • Structure plus complexe

 

Couche

Cette architecte implique de diviser l'application en trois couches:

  1. Application
  2. Domaine
  3. Infrastructure

Application

Cette couche est le point d’interaction de l’application. Il peut s'agir de contrôleur rest, de service de messages. C'est la porte d'entrée pour exécuter le domaine métier.  Cette portion est parfois appelée espace utilisateur.

Domaine

C'est l'implémentation de la couche métier, c'est le noyau central de l'application. Il se trouve aussi les interfaces de communication avec les deux autres couches. Il est possible de rencontrer cette couche sous l’appellation règle d'affaires.

Infrastructure

Cette couche possède l'implémentation pour sauvegarder dans une base de données, fichier, web service externe. C'est le point de sortie du domaine métier. Un synonyme pour cette couche est espace serveur.

Principe

Le domaine n'a pas connaissance de l'application et de l'infrastructure. C'est là qu'entrent en jeu les adapteurs et les ports. Les ports sont des interfaces que les adapteurs implémenteront. Chaque couche peut être testée individuellement, de plus l'utilisation d'interface ajoute un niveau d'indépendance au niveau de l'implémentation. Pour un test, les données pourraient être sauvegardées dans un fichier alors qu'en production la sauvegarde se ferait dans une base de données.

Contexte, limite d'un domaine

Contrairement à une application monolithique, l'architecture hexagonale nécessite de découper selon le contexte (bounded context). On se retrouve avec plusieurs petits modèles au lieu d'un modèle unique. 

Un contexte peut être vu comme les limites d'un sous-domaine dans une application auquel une certaine logique métier peut être appliqué.

Si on prend une application telle qu'amazon: il en existe plusieurs inventaire, prix, produit, commentaire.

 
L'interface graphique accède une multitude de microservice délimité par leur domaine. Si les instances du service de commentaire sont en arrêt, un client pourra tout de même acheter un livre.
 

Objet valeur (Value Object)

C'est un objet immuable qui décrit un aspect du domaine mais qui n'a pas d'identité. De la logique métier est fortement conseillé d'y être présent. Un âge ou bien une adresse pourrait être présenté de la sorte. Il faut considérer que deux VO sont égales si l'ensemble de ses champs sont identiques.

public final class Age{
     private final int value;
 
     public Age(int value){
        if(age < 0 || age > 120){
            throw new InvalidAgeException()
        }
        this.value=value;
     }

     public Age change(int value){
         return new Age(value);
     }
}            

Il serait tout à fait possible de ne pas utiliser un objet dédié, mais il faudrait externaliser la logique métier.  D'ailleurs c'est souvent cette façon de faire qu'il est possible de voir, on se retrouve avec des validations à maint endroit et le risque d'erreur de tout genre s'accroît.

Agrégat

C'est un groupe d'objet métier: objet de valeur, entité qu'on manipule comme si ça serait qu'une seule entité métier. Pour une facture, il y a différents éléments qui peuvent la composer: émetteur, destinataire, date, liste des produits ou service, coût, taxe. Toute modification devrait se faire au sein de notre agrégat afin de garantir sa cohérence. Si la facture est déjà payée, il ne devrait pas être possible d'ajouter d'élément dedans. Si un agrégat a besoin des informations d'un autre agrégat, son identifiant peut-être utilisé. Dans une transaction, il devrait avoir qu'une sauvegarde pour un agrégat.

Il faut prendre en considération que pour une situation donnée, un objet pourrait être un objet de valeur et dans un autre cas une entité ou même un agrégat.

Est-ce qu'on se soucie des valeurs de l'objet ou bien de l'identification d'une instance? Ce questionnement peut nous aider à choisir entre une value objet ou bien un agrégat. Pour spécifier l'identité d'un agrégat, une bonne pratique est d'utiliser une VO. 

Au lieu de faire

public class Invoice{
    int id;
}
 
Passer par un VO dans l'agrégat 

public class Invoice{
    InvoiceId id;
}

public class InvoiceId{
    int id; 
    InvoiceId(int id){
        if(id < 1){
            throw new ArgumentException("Id must have a minimum value of 1");
        } 
        this.id=id; 
    }
}
 
Dans le cas de notre InvoiceId, le id pourrait être la date du jour: 20201229 suivit d'un chiffre  qui s'incrémente et se réinitialise chaque jour. 

Il est plus aisé de changer le type de donnée par la suite, il y a moins d'endroits à effectuer les changements. Il est possible d'ajouter de la logique pour en autre effectuer des validations. Il est plus difficile de passer une mauvaise valeur que si on utilisait le type primitif.

Mise en pratique

Notre exemple consistera à une application de facturation, il pourrait avoir un contexte de facturation, client, paiement.... nous développerons celui de facturation. Plus précisément le scénario d'aller chercher une Invoice selon son Id. Notre application sera monolithique mais sous-divisée. Chacun des modules devrait utiliser une architecture hexagonale.

Notre objet Invoice va représenter toutce qui doit être facturé à un customer. LineInvoice représenterait chaque item produit ou service acheté. Invoice est un bon candidat pour un agrégat, mais LineInvoice?

public class Invoice {

    private final InvoiceId id;
    private final InvoiceStatus status;
    private final Long customerId;
    private final LocalDate paidDate;
    private final LocalDate dueDate;
    private final Flux<InvoiceItem> invoiceItems;
    
    private Invoice(InvoiceId id, InvoiceStatus status, Long customerId,
            LocalDate paidDate, LocalDate dueDate,
            Flux<InvoiceItem> invoiceItems) {
        this.id = id;
        this.status = status;
        this.customerId = customerId;
        this.paidDate = paidDate;
        this.dueDate = dueDate;
        this.invoiceItems = invoiceItems;
    }
    
    public static Invoice withoutId(InvoiceStatus status, Long customerId,
            LocalDate paidDate, LocalDate dueDate,
            Flux<InvoiceItem> invoiceItems) {
        return new Invoice(null, status, customerId, paidDate, dueDate, invoiceItems);
    }
    
    public BigDecimal calculateTotalPrice() {
        if (invoiceItems == null) {
            return BigDecimal.ZERO;
        }

        return invoiceItems.map(ii -> ii.calculateTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add).block();
        
    }
}

LineInvoice pourrait être aussi un bon candidat pour un agrégat,  d'autres entités auraient besoin d'un identifiant. Il pourrait être possible d'appliquer une taxe, un rabais ou un frais d'envois spécifiques.

 public class InvoiceItem {
    private String description;
    private int quantity;
    private BigDecimal unitPrice;
    
    public InvoiceItem(String description, int quantity, BigDecimal unitPrice){
        if(quantity < 1){
            //quantity must be over 0
        }
        
        if(unitPrice.compareTo(BigDecimal.ZERO)!=1){
            //unit price must be over 0
        }
        this.description=description;
        this.quantity=quantity;
        this.unitPrice=unitPrice;
    }
    
    public BigDecimal calculateTotalPrice(){
      return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
    
}

 L'adapteur web

@RestController
@RequestMapping("/invoices")
public class GetInvoiceController {

    LoadInvoiceQuery loadInvoiceQuery;
    
    GetInvoiceController(LoadInvoiceQuery loadInvoiceQuery){
        this.loadInvoiceQuery=loadInvoiceQuery;
    }
    
    @GetMapping("/{id}")
    public Mono<Invoice> getInvoicesById(@PathVariable Long id){
        return loadInvoiceQuery.getInvoice(new InvoiceId(id));
    }
}

Le port pour la partie web    

public interface LoadInvoiceQuery {
    Mono<Invoice> getInvoice(InvoiceId invoiceId);
}

Le service au niveau du domaine

@Service
public class GetInvoiceService implements LoadInvoiceQuery{

    private final LoadInvoicePort loadInvoicePort;

    public GetInvoiceService(final LoadInvoicePort loadInvoicePort) {
        this.loadInvoicePort = loadInvoicePort;
    }
    
    @Override
    public Mono<Invoice> getInvoice(InvoiceId invoiceId){
        return loadInvoicePort.loadInvoice(invoiceId);
    }

}

L'adapteur de persistence

@Component
public class InvoicePersistenceAdapter implements LoadInvoicePort {

    @Autowired
    InvoiceMapper invoiceMapper;

    SpringPostgresInvoiceRepository invoiceRepository;
    SpringPostgresInvoiceLineRepository invoiceLineRepository;
    
    public InvoicePersistenceAdapter(SpringPostgresInvoiceRepository invoiceRepository,
            SpringPostgresInvoiceLineRepository invoiceLineRepository) {
        this.invoiceRepository = invoiceRepository;
        this.invoiceLineRepository=invoiceLineRepository;
    }


    @Override
    public Mono<Invoice> loadInvoice(InvoiceId invoiceId) {

        Mono<InvoiceEntity> invoiceEntity = invoiceRepository.findById(invoiceId.getValue());
        Flux<InvoiceLineEntity> invoiceLineEntity = invoiceLineRepository.findByInvoiceId(invoiceId.getValue());
        return invoiceMapper.mapToDomainEntity(invoiceEntity, invoiceLineEntity);
    }

}

Pour la persistence, il est possible d'utiliser une base de données ou un schéma par contexte ou bien dans utiliser un global à l'application. Seul un contexte a été développé, cependant même en optant pour une seule base de données pour l'ensemble des contextes, si on a besoin d'une information d'un autre contexte, en utilisant le id tel que customerId dans notre exemple,  pour aller effectuer la requête. Ce qui est une bonne approche pour bien séparer les domaines.

Si vous avez besoin d'augmenter les performances, vous pouvez utiliser un load balancer ou un reverse proxy pour démarrer de multiple instance de votre application avant d'opter pour des microservice. 

 Les packages sont organisés par couche. Certains préfèrent une organisation par fonctionnalité.

Responsabilité de l'adapteur pour la bd

  • Prendre les données en entrée
  • Les convertir dans le format de la bd
  • Les envoyer à la bd
  • Retourner les donner de sortie de la bd

Si vous désirez utiliser un ORM,  il faut faire le pont entre la structure objet et ceux utilisés pour le domaine, car tel que déjà dit, la couche du domaine ne devrait pas être imprégnée d'un framework. Cependant, j'ai pu constater à quelques reprises que ce n'était pas toujours respecté. 

Dans une architecture hexagonale, on se retrouve avec un petit domaine, je trouve alors l'emploi d'un ORM moins intéressant. Si vous optez pour cette solution, MapStruct peut grandement simplifier la conversion en vos objets métier.

Dans notre exemple, nous avons utilisé r2dbc, afin que l'application soit de type reactive. Contrairement au ORM, les relations ne sont pas géré, il faut le faire manuellement.

Autrement, Spring Data JDBC peut être une autre solution.

À partir d'un Id, nous avons été chercher un objet de type InvoiceEntity, il a été ensuite convertir en Invoice pour la couche domaine, nous aurions pu le convertir dans un autre format selon les besoins au niveau du web. Certains pourraient trouver que le passage entre ces couches est inutile dans ce cas. Un appel direct à partir du web vers la bd pourrait être effectué. Je suis d'accord, à moins de gros problème de performance, je continuerais d'utiliser cette approche, une fois que des raccourcies sont mis en place, ils deviennent rapidement la norme et on se trouve avec une architecture loin des attentes initiales.


Communication / synchronisation

Vous voudriez développer un autre module tel que le Customer. Comment les différents modules pourraient communiquer entre eux?

Dépendant si le type de communication: synchrone, asynchrone, vous pouvez optez pour des requêtes HTTP, service d'agent de message (RabbitMQ, JMS...)

Conclusion

Vous êtes une startup, vous voyez en grand. Vous regardez ce qu'utilise Netflix et vous voulez faire pareil. Vous n'avez aucun produit, aucune ligne de code de fait, mais vous pensez que la seule solution pour gérer votre hypothétique centaine de millions de requêtes est les microservices.

Il ne faut pas négliger la complexité niveau infrastructure: une base de données par microservice, la gestion des multiples instances des microservices, la communication entre eux et leur surveillance. Ces particularités amènent à avoir une équipe devops très efficace.

L'utilisation d'une architecture hexagonale pourrait grandement simplifier la mise en place de votre application avant qu'un passage au microservice soit réellement nécessaire. Le but de l'article était de vous montrer une façon différente de faire une architecture logiciel, libre à vous de l'adapter selon vos besoins.