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.