vendredi 26 mai 2023

Thymeleaf et htmx

Thymeleaf est un moteur de template serveur régulièrement utilisé le monde Java, principalement avec le framework Spring. Dans cet article, nous présentation quelques architectures possibles avec ce framework ainsi que l'utilisation de Htmx afin de rendre les applications thymeleaf plus dynamiques.

 

 Architecture d'application

Voici un ensemble d'architecture varié qu'il est possible de faire avec Thymeleaf

 
La première étant la plus simple et la plus utilisée dans les exemples montrés sur la toile.
J'ai pu expérimenter la cinquième. Le mvc était utilisé pour construire les interfaces utilisateurs, par contre toutes les opérations de suppression, de mise à jour ou de création de données utilisaient une architecture rest. Le javascript était utilisé afin de reloader la page.

C'est une solution simple et rapide à mettre en place face à une solution SPA avec Angular, React ou bien Vue.

HDA

L'augmentation du temps de développement, la complexité des frameworks, courbe d'apprentissage élevé, maintenance onéreuse ont fait reculé certains.

React à htmx

 

Hypermedia Driven Application combine la facilité des multiples pages application (mpa) avec l'expérience utilisateur des single page application (spa). 

Une panoplie de librairie existe pour apporter l'expérience utilisateur à des frameworks orientés serveur.

unpoly, htmx, alpine, hyperscript... permettent via l'ajout de mot clé dans le html de rendre des applications utilisant des framework serveurs: jsf, jsp, thymeleaf... plus dynamique, d'éviter de charger la page au complet..

Htmx

Htmx est une petite librairie qui donne accès à ajax, css, sse et websocket via des attributs en html.

Cette librairie  htmx-spring-boot-thymeleaf facilite l'utilisation de htmx avec spring boot et thymeleaf. Elle n'est pas obligatoire.

Vidéo d'une présentation de thymeleaf avec htmx

https://youtu.be/okCdaBTQsik

Une autre présentation

https://www.youtube.com/watch?v=38WAVRfxPxI

Une liste d'exemple est disponible via ce lien.

Éviter le rafraîchissement de la page 

Notre exemple est un template ayant un fragment pour les css, un autre pour le menu et un autre pour le javascript.
 
index.html
<!DOCTYPE html>
<html lang="en">
<head th:replace="~{fragments/css :: headcss}">
<meta charset="UTF-8">
<title>test</title>
</head>
<body class="container">
<div th:replace="~{fragments/navbarmenu :: menu}"/>

<section id="main" class="fade-me-out">

<div id="modals-here"></div>
</section>

<div th:replace="~{fragments/jsscripts :: jsscripts}"/>

</body>
</html>

 

 Fragment pour le css

<!DOCTYPE html>
<html lang="en">
<head th:fragment="headcss">

<title>Generic form UI</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" th:href="@{/css/datepicker-bs5.min.css}" href="/css/datepicker-bs5.min.css" />
</head>
<body>

</body>
</html>


Fragment pour le javascript

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js scripts</title>
</head>
<body>
<div th:fragment="jsscripts">

<script type="text/javascript" th:src="@{/js/genericui.js}" src="/js/genericui.js"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.bundle.min.js}" src="/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript" th:src="@{/js/datepicker-full.min.js}" src="/js/datepicker-full.min.js"></script>

<script type="text/javascript" th:src="@{/js/htmx.min.js}" src="/js/htmx.min.js"></script>
<script type="text/javascript" th:src="@{/js/inputmask.min.js}" src="/js/inputmask.min.js"></script>

<script type="text/javascript" th:src="@{/js/bootstrap-validation.js}" src="/js/bootstrap-validation.js"></script>

<script type="text/javascript">
document.addEventListener('htmx:afterRequest', function(evt) {

Inputmask().mask(document.querySelectorAll("input"));

const matchesDate = document.getElementsByClassName("date1");

for (let i = 0; i < matchesDate.length; i++) {
const datepicker = new Datepicker(matchesDate[i], {
buttonClass: 'btn',
});
}

});

</script>
</div>
</body>
</html>

Fragment pour le menu

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NavBar</title>
</head>
<body>

<nav th:fragment="menu" class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" th:href="@{/index2}">Norenda</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" th:href="@{/index}">Index</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" hx-push-url="true" hx:get="@{/modal}" hx-target="#main" hx-swap="innerHTML" hx-trigger="click">Modal</a>
</li>
<li class="nav-item">
<a class="nav-link" hx-push-url="true" hx:get="@{/success}" hx-target="#main" hx-swap="innerHTML swap:1s" hx-trigger="click">Succes</a>
</li>
<li class="nav-item">
<a class="nav-link" hx-push-url="true" hx:get="@{/editor}" hx-target="#main" hx-swap="innerHTML swap:1s" hx-trigger="click">Editeur</a>
</li>
</ul>
<form class="d-flex" role="search" hx:post="@{/search}" hx-target="#main" hx-swap="innerHTML">
<input class="form-control me-2" type="search" placeholder="Recherche" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Recherche</button>
</form>
</div>
</div>
</nav>

</body>
</html>

 

Les deux premiers liens font un chargement complet de la page. 
Le troisième lien affiche une modale
Le quatrième lien affiche un message 
Le cinquième lien affiche un formulaire.

Mis à part les deux premiers liens, il n'y a aucun chargement complet de la page. Il y a remplacement d'une zone par le fragment retourné du serveur.

Un des liens propose une animation.
 
Pour éviter de faire un chargement complet de la page, il faut utiliser certains mots clés.

On mentionne ici que get est fait sur le url mentionnée
hx:get="@{/editor}"

On mentionne ici quel sera la cible du fragment retourné, où ira le contenu
hx-target="#main" 

On mentionne ici qu'on remplace le contenu de la cible par celui du fragment, il serait possible de l'ajouter avant, après
hx-swap="innerHTML"

 

Animation

Quelques animations sont disponibles tels que des fade in, fade out. Il est aussi possible d’utiliser des css pour créer ses propres animations.

Conclusion

Htmx peut vous permettre de rendre encore plus dynamique votre application qui utilise un framework orienté serveur à une fraction du prix en termes de temps, argent et apprentissage. Pour encore plus de fonctionnalité, il faut regarder unpoly.


Vous pouvez télécharger les sources du projet ici.

mercredi 17 mai 2023

Spring data jpa et Hibernate’s Warning “firstResult/maxResults specified with collection fetch”

Nous verrons dans cet article comment corrigé le problème 
Hibernate’s Warning “firstResult/maxResults specified with collection fetch” qui peut survenir lorsqu'il y a un fetch d'effectuer et qu'il y a du paging. Nous utiliserons spring data jpa.

Détail structure

Imaginons une relation 1 à plusieurs


Lorsqu'on obtient un parent, on désire obtenir aussi sa liste d'enfants. En sql, il y aurait une jointure à effectuer. Il est question d'une recherche avancé où on pourrait recherche par plusieurs critères.

Spécification

Création d'une spécification pour construire une requête dynamique

@Component
public class ParentSpecification {

    public Specification<Parent> recherche(RechercheParent recherche) {
        return (root, cq, cb) -> {
            Join<Parent, enfant> enfantJoin;

             List<Predicate> rootPredicates = new ArrayList<>();

             if (Long.class != cq.getResultType()) {
                enfantJoin = (Join) root.fetch("enfants", JoinType.LEFT);
             } else {
                enfantJoin = (Join) root.join("enfants", JoinType.LEFT);
             }

             sqlLikeCondition("nom", recherche.nomParent(), root, cb, rootPredicates);
             sqlEqualCondition("age", recherche.age(), root, cb, rootPredicates);
             return  cb.and(rootPredicates.toArray(new Predicate[rootPredicates.size()]));

    }

}

Repository

Spring data Jpa a une méthode findAll qui accepte en paramètre une spécification. Il y a même une version de la méthode qui accepte aussi un objet Pageable. Afin de pouvoir l'utiliser, le repository dois hérité de JpaSpecificationExecutor.

@Repository
@Transactional(readOnly = true)
public interface ParentRepository extends JpaRepository<Parent, Long>, JpaSpecificationExecutor<Parent> {
}


Il sera alors possible d'effectuer un appel comme celui çi

 Page<Parent> pageParent = parentRepository.findAll(parentSpecification.search(parentRecherche), pageable);

Détail du problème

Par contre, dans les logs il y aura le message org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory


Dans la base de donnée avec une requête sql le résultat serait par exemple
Chaque enfant est relié à son parent.

Habituellement, Hibernate, le orm par défaut utilisé par spring boot, lorsque le paging est utilisé il va prendre x première valeurs jusqu'à y. Par exemple si on mentionne qu'on veut les 5 premières résultats on obtiendrait


Ce qui est pas vraiment ce qu'on voudrait, mais bien les 5 premiers parents.

Hibernate donne le message “firstResult/maxResults specified with collection fetch” car il applique alors la pagination en mémoire sans appliquer la pagination et ensuite retourne le résult voulue.
Si votre table à énormément de donnée, vous pouvez obtenir rapidement des problèmes de performance, surtout si vous effectuer des rêquetes sans aucun critères de recherche.

Solution

Afin de palier à ce problème, nous allons redéfinir une méthode spring data jpa

Dans la classe de spécification, il faudra ajouter

public Specification<Parent> idIn(Set<Long> parentIds) {
        return (root, cq, cb) -> {

            if (parentIds == null || parentIds.isEmpty()) {
                return null;
            }

            return root.get("parentId").in(parentIds);

        };
    }

Il faut créer une nouvelle interface avec la méthode findEntityIds

@NoRepositoryBean
public interface CustomJpaSpecificationExecutor<E, ID extends Serializable> extends JpaSpecificationExecutor<E> {

    Page<ID> findEntityIds(Specification<E> specification, Pageable pageable);
}

Par la suite il faut créer une classe implémentant cette interface


public class CustomBaseSimpleJpaRepository<E, ID extends Serializable> extends SimpleJpaRepository<E, ID> implements CustomJpaSpecificationExecutor<E, ID> {

    private final EntityManager entityManager;
    private final JpaEntityInformation<E, ID> entityInformation;

    public CustomBaseSimpleJpaRepository(JpaEntityInformation<E, ID> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = entityInformation;
    }

    private static long executeCountQuery(TypedQuery<Long> query) {

        Assert.notNull(query, "TypedQuery must not be null");

        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }

        return total;
    }

    @Override
    public Page<ID> findEntityIds(Specification<E> specification, Pageable pageable) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<ID> criteriaQuery = criteriaBuilder.createQuery(entityInformation.getIdType());
        Root<E> root = criteriaQuery.from(getDomainClass());

        // Get the entities ID only
        criteriaQuery.select((Path<ID>) root.get(entityInformation.getIdAttribute()));

        // Apply specification(s) to restrict results or to write dynamic queries
        Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);

        if (predicate != null) {
            criteriaQuery.where(predicate);
        }

        // Update Sorting
        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        if (sort.isSorted()) {
            criteriaQuery.orderBy(toOrders(sort, root, criteriaBuilder));
        }
        TypedQuery<ID> typedQuery = entityManager.createQuery(criteriaQuery);

        // Update Pagination attributes
        if (pageable.isPaged()) {
            typedQuery.setFirstResult((int) pageable.getOffset());
            typedQuery.setMaxResults(pageable.getPageSize());
        }

        return PageableExecutionUtils.getPage(typedQuery.getResultList(), pageable,
                () -> this.executeCountQuery(getCountQuery(specification, getDomainClass())));

    }

}

Il faudra faire deux appels

Page<Long> pageParentId = parentRepository.findEntityIds(parentSpecification.search(parentRecherche), pageable);

List<Parent> parents = parentRepository.findAll(parentSpecification.search(parentRecherche).and(parentSpecification.idIn(Set.copyOf(pageParentId.getContent()))));

return new PageImpl<>(parents, pageable, pageParentId.getTotalElements());

Spring data jpa lancera 3 requêtes.

Une première requête pour obtenir les id des parents.
Une autre requête pour obtenir le nombre total de donnée 
Une troisième requête nécessitant les id de la première requête sera effectué



mardi 25 avril 2023

Logitech Brio 500, zoom sous linux

 Logitech Brio 500

Vous avez une caméra logitech Brio 500 et vous trouvez que vous êtes éloignez ans l'image dans les logitiels de messageries. Il est possible d'améliorer cela 


Lister les périphériques

v4l2-ctl --list-devices

Brio 500 (usb-0000:00:14.0-4):
       /dev/video0
       /dev/video1
       /dev/media0

Lister les fonctionnalités du périphériques

v4l2-ctl -d /dev/video0 --list-ctrls

User Controls

                    brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=129
                      contrast 0x00980901 (int)    : min=0 max=255 step=1 default=128 value=128
                    saturation 0x00980902 (int)    : min=0 max=255 step=1 default=128 value=128
       white_balance_automatic 0x0098090c (bool)   : default=1 value=1
                          gain 0x00980913 (int)    : min=0 max=255 step=1 default=0 value=0
          power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=2 value=2 (60 Hz)
     white_balance_temperature 0x0098091a (int)    : min=2800 max=7500 step=1 default=5000 value=5000 flags=inactive
                     sharpness 0x0098091b (int)    : min=0 max=255 step=1 default=128 value=128
        backlight_compensation 0x0098091c (int)    : min=0 max=1 step=1 default=1 value=1

Camera Controls

                 auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 (Aperture Priority Mode)
        exposure_time_absolute 0x009a0902 (int)    : min=3 max=2047 step=1 default=156 value=156 flags=inactive
    exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=1
                  pan_absolute 0x009a0908 (int)    : min=-72000 max=72000 step=3600 default=0 value=0
                 tilt_absolute 0x009a0909 (int)    : min=-72000 max=72000 step=3600 default=0 value=0
                focus_absolute 0x009a090a (int)    : min=0 max=255 step=1 default=0 value=0 flags=inactive
    focus_automatic_continuous 0x009a090c (bool)   : default=1 value=1
                 zoom_absolute 0x009a090d (int)    : min=100 max=400 step=1 default=100 value=135


Modifier des valeurs

v4l2-ctl --set-ctrl=zoom_absolute=135


Ces modifications peuvent être faite pendant que vous utilisez la caméra. 

jeudi 6 avril 2023

Multiple fetch dans une requête avec jpa

Hibernate et fort probablement les autres ORM sont limité dans la capacité de ramener tout une structure d'object imbriqué.

@Entity
private class Student{
 
    @Id
    private Long studentId; 
    private String firstname;
    private String lastname;
 
    @OneToMany(mappedBy = "student")
    private List<Course> courses

    @OneToMany(mappedBy = "student")
    private List<Book> books

}


MultipleBagFetchException

Si vous tentez de lancer cette requête provenant d'un repository
 
@Query("""
  select s 
  from Student s 
  join fetch s.courses
  join fetch s.books
""")
 List<Student> findStudentWithCoursesBooks();

vous obtiendrez une errreur de type MultipleBagFetchException. Il n'est pas possible de fetcher plus qu'une entité qui va généré un produit cartésien.

Il pourrait être possible d'éviter cette erreur en changeant les list pour des set dans l'entité Student, cependant le produit cartésien se produira toujours.


Solution avec transaction

@Query("""
  select s 
  from Student s 
  join fetch s.courses
""")
 List<Student> findStudentWithCoursesBooks(); 


Dans un service

@Transactional
public List<Student> getStudent(){
     List<Student>  students = studentRepository.findStudentWithCoursesBooks();
     for(Student student: students){
          student.getBooks().size();
     } 
}

Il y aura chargement des livres, puisque l'annotation Transactional a été utilisé l'erreur LazyInitializationException ne survientdra pas. Cependant pour chaque étudiant, une requête sql pour aller chercher les Book. C'est le problème n+1 souvent mentionné dans le domaine des orm.

S'il y a que très peu de student, et que la méthode getStudent() est très peu utilisé. Cela pourrais être une solution possible. Il y a toujours possibilité d'ajouter du cache dans l'application afin de limiter les dégats

Solution avec multiple requêtes

Il est possible de combiner de multiple requete, une pour chaque fetch que vous désirez.
Le problème du n+1 est ainsi évite.
Il faut cependant que les deux retournes les même Students dans notre cas. Il faut donc ajouter une condition

@Query("""
  select distinct(s)
  from Student s 
  join fetch s.courses
  where s.studentid  < 10
""")
 List<Student> findStudentWithCourses();
 
@Query("""
  select distinct(s)
  from Student s 
  join fetch s.books
  where s.studentid  < 10
""")
 List<Student> findStudentWithBooks(List<Student> students); 
 
 
 
Dans une classe au niveau du service
 
@Service
public StudentService{

    private StudentRepository studentRepository;

    @Transactional
    public List<Students> getStudentWithCoursesBooks(){

        List<Student> students = studentRepository.findStudentWithCourses();

        return !students.isEmpty() ?
            studentRepository.findStudentWithBooks(
               minId,
               maxId
             ) :
           students;

        }

}
 
Au niveau des requêtes seul deux requetes sont exécutés .
  

Conception d'api REST

 Il y a une multitude de question quand nous commencons à convevoir un API rest. Un débat qui reviens souvent est les ressources imbriqués, parent / enfant, sous-ressource.


Hierachie

Pourquoi opter pour

/parents/{idParent}/enfants/{idEnfant}

au lieu de 

/enfants/{idEnfant}
 
La première approche peut être préférable si on veut montrer le lien hierarchique qui unit les ressources. Il faut cependant ne pas exagérer du niveau hierachique, car l'url peut rapidement devenir très long et rendre le tout moins lisible. Il est d'ailleurs rare de voir un api avec plus de 2 niveau.

Dans le deuxième cas, pour une sauvegarde, il faudrait que la ressource est le nom du parent.

Une autre approche serait

/parents/{idParent}?enfants={idEnfant}
 

Doublon

 
Rien n'empêche d'utilisé plusieurs approches, dans ce cas, on pourrait se retrouver dans le cas qu'il y a différent endpoint pour obtenir la même ressource.
 

Longueur des url

/companies/{idCompany}/departments/{idDepartment}/projects/{idProject}/employees
 
Il faut cependant ne pas exagérer du niveau hierachique, car l'url peut rapidement devenir très long et rendre le tout moins lisible. Il est d'ailleurs rare de voir un api avec plus de 2 niveau.

Changement de la relation

Avec une approche hierarchique,  s'il y a changement dans la relation dépendant si vous désirez continuer d'offrir la ressource ou non, il faudrat penser à utiliser le versionnement de votre api. Une redirection est aussi possible via en autre un api gateway

Sécurité

L'approche est une hierachie dévoile une relation entre les ressources qui pour dans certain casne pas être dévoilé.

Par exemple sur un site de rencontre

/users/{id}/pictures/

Il est possible qu'on ne veuille pas permettre à n'importe qui d'accéder à toutes les images d'une personne. Un autre niveau de sécurité peut être ajouté afin de permettre qu'a certain type d'utilisateur d'y accéder via par exemple la notion de rôle.


Il n'y a pas de bonne ou mauvaise approche, il faut juste connaitre les différentes possibilités et employé la méthode la plus adéquate pour nos besoins. N'oublier pas de bien documenter votre api par exemple avec un outil tel que spring-doc, swagger.

mercredi 5 avril 2023

Versionnement de son api rest

 Vous avez un ensemble d'api qui est utilisé par de multiple clients. De nouvelle fonctionnalités prévue vont changer les valeurs de retour et les paramètres de certaines méthode. Afin de ne pas casser l'existant, il est possible de mettre en place plusieurs versions des apis.

Nous verrons différentes approche pour mettre en place le versionnement d'un api.

Versionnement des entités, payload

Dans la première mise en place d'un api vous avez une entité  Person

Si vous devez ajouter un nouveau champs dans cette entité, l'ancienne pourrait être renommé PersonV1 et la nouvelle PersonV2. PersonV2 ayan un champ prenom


Versionnement des url

@RestController
public class PersonController {

  @GetMapping("v1/person")
  public PersonV1 personV1() {
    return new PersonV1("Collin");
  }

  @GetMapping("v2/person")
  public PersonV2 personV2() {
    return new PersonV2("Collin", "Marc");
  }
}
 
 

Versionnement par un RequestParam


@RestController public class PersonController { @GetMapping(value="/person", params="v1) public PersonV1 personV1() { return new PersonV1("Collin"); } @GetMapping(value="/person", params="v2") public PersonV2 personV2() { return new PersonV2("Collin", "Marc"); } 
}

L'appel se ferait de cette façon

http://localhost:8080/person?v2

 
 

Versionnement par l'headers de la requête

@RestController
public class PersonController {

  @GetMapping(value="/person", headers="api-version=1)
  public PersonV1 personV1() {
    return new PersonV1("Collin");
  }

  @GetMapping(value="/person", headers="api-version=2")
  public PersonV2 personV2() {
    return new PersonV2("Collin", "Marc");
  } 
 } 

Si vous utilisez un outils tel que rester, postman, il faut ajouter dans la section headers la clé

api-version

et la valeur

1


Versionnement par le media type

@RestController
public class PersonController {

  @GetMapping(value="/person", produces="application/vnd.api-v1+json")
  public PersonV1 personV1() {
    return new PersonV1("Collin");
  }

  @GetMapping(value="/person", produces="application/vnd.api-v2+json")
  public PersonV2 personV2() {
    return new PersonV2("Collin", "Marc");
  } 
 } 

Le media type doit être mis le headers avec la clé Accept.


Plusieurs approches ont été spécifiés et tous ont un moins gros acteurs du marché utilise chacune de ses approches.


GitHub utilise le média type

Microsoft utilise le heders

Twitter utilise le url 

Amazon utilise le request param



Mettre à jour la sa feature branch avec sa branche parent

Branche de fonctionnalité

Les branches de fonctionnalité sont un bon moyen de ne pas polluer la branche principale. Imaginer que l'utilisateur fait une multitude de commit pour une fonctionné x dans la branche principale entrecoupé de plusieurs autre concernant une fonctionnalité y.

Il peut devenir complexe en cas de problème de revenir en arrière ou bien simplement de suivre le développement.

Une branche de fonctionnalité permet de centralisé tous le développement autour d'une fonctionnalité avant de la mettre dans la branche principale. C'est une branche temporaire

Mettre à jour sa branche

Vous avez une branche de fonctionnalité feature/AddTaxManagement qui se base sur la branche principal par exemple develop.

De nombreuse personne envoie leur changement dans la branche develop.

Retourner dans la branche develop

>git checkout develop

 

 

Ramener les changements

>git fetch -p origin


Fusionner les changements remote en local

>git merge origin/develop


git checkout feature/AddTaxManagement

Fusionner la branche develop avec votre feature branch

>git merge develop

 

Pousser ses modifications dans la branche remote

>git push origin feature/AddTaxManagement