E-HBNT-1 : Hibernate

:timer_clock: Temps estimé: 3 à 5h

Objectifs du laboratoire

Le laboratoire utilisera également hibernate, un ORM qui est très présent dans l’entreprise. Un des objectifs est également d’être capable d’apprendre rapidement une nouvelle technologie en feuilletant de la documentation, chose que vous ferrez toute votre vie!

Déroulement

Bien lire le laboratoire avant de commencer. Dans certains cas, plusieurs solutions sont possibles, mais nous vous en imposerons une particulière soit pour des fins pédagogique, soit pour que ça soit plus facile à corriger!

Cloner le code

Le code du laboratoire est diposnible ici.

Utilisez le lien suivant pour créer un fork dans github : https://classroom.github.com/a/rQ76qLH8

Analyser le code existant

Actuellement, il est possible de démarrer le code seulement avec la version “in memory” de la persistence. Pour ce faire, vous pouvez utiliser intellij avec l’option -Dgarage.persistence=memory (VM options - l’autre option étant hibernate).

Un projet postman est également fournis. Vous pouvez l’utiliser pour vous assurer que tout démarre bien.

Quelques règles d’affaires en vrac pour comprendre le projet :

Quelques décisions architecturales prises (et qui ne peuvent pas être changées) :

Quelques limitations pour le cadre du cours (certaines consignes feront du sens pour plus tard :P) :

Crash course hibernate

Lire la section 8.5.2 de ce document (note : Session et EntityManager sont équivalents, le second est l’implémentation du pattern JPA de java).

Il nous faudra donc un EntityManagerFactory et un EntityManager. Cependant, notre contexte web demande quelque chose de spécial : on doit avec un et un seul EntityManager par requête. Pour ce faire, on aura :

Implémenter les repository hibernate

Hibernate et H2 sont déjà importés dans le projet.

Voici la structure de table à laquelle on s’attend (celle-ci doit être respectée à 100% - nom des tables et nom des colonnes). Lire la documentation sur les annotations afin de savoir comment arriver à ce résultat :

    create table appointments (
       id integer not null auto_increment,
        appointmentNumber varchar(255),
        clientName varchar(255),
        clientPhone varchar(255),
        primary key (id)
    );

    create table orders (
       id integer not null auto_increment,
        date datetime,
        appointmentNumber varchar(255),
        primary key (id)
    );

    create table parts (
       id integer not null auto_increment,
        name varchar(255),
        quantity integer,
        order_id integer,
        primary key (id)
    );

Vous pouvez regarder le titre des tests (ou l’implémentation de ceux-ci pour le repository in memory) afin d’avoir une idée des comportements à avoir!

Question: Pourquoi est-ce acceptable en java de mettre ces annotations dans le domaine? Dans quel cas voudrions-nous plutot avoir des DTO pour la couche persistence?

BD utilisée

Pour les fins du lab, vous devez utiliser H2 pour votre code et vos tests.

Voici un exemple de configuration pour hibernate et H2:

<?xml version="1.0"  encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">

    <persistence-unit name="XYZ" transaction-type="RESOURCE_LOCAL">
        <description>H2 In Memory DB</description>
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.connection.driver_class" value="org.h2.Driver" />
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
            <property name="hibernate.show_sql" value="true" /> <!-- DEBUG -->
            <property name="hibernate.format_sql" value="true" /> <!-- DEBUG -->

            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" />
            <property name="javax.persistence.jdbc.user" value="sa" />
            <property name="javax.persistence.jdbc.password" value="" />

            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <property name="hibernate.id.new_generator_mappings" value="true" />

        </properties>
    </persistence-unit>
</persistence>

Prenez le temps de comprendre ce que ça fait par contre!!

Gestion des transactions

La gestion des transaction (qu’est-ce qui doit être atomique) est en fait un problème qui appartient à la logique d’application (couche d’application service). Les repositories ne devrait pas avoir à gérer de transaction.

Il est possible de faire ceci assez facilement en utilisant le fait que dans jetty, 1 requête = 1 thread. On utilise par exemple un threadlocal et un servlet filter.

Le filter est déjà fournis dans le code de base du projet (prenez le temps de voir comment il est enregistré), mais il ne fait rien. Vous pouvez par contre y placer du code avant et après la requête!

Si on décortique un “save” dans hibernate, on a :

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("XYZ"); // Une seule fois par application! C'est très long à faire!!

EntityManager entityManager = entityManagerFactory.createEntityManager(); // L'équivalent d'une "connexion", ou souvent 1 entity manager = 1 transaction. Ici on veut partager le même pour toute la requête rest!

entityManager.getTransaction().begin(); // Doit être fait dans l'application service (en étant idéalement abstrait de entity manager - d'où l'interface DomainTransaction)

entityManager.persist(object); // Doit être fait dans l'implémentation hibernate du repository

entityManager.getTransaction().commit(); // ou rollback, aussi dans l'application service.

Si vous n’êtes pas sur de comprendre comment les servlet filters fonctionnent, vous pouvez revoir l’exercice de la semaine 6 : E-COUC-2.

Vous devez trouver une façon d’arriver à cela! N’hésitez pas à nous le demander au laboratoire si vous êtes bloqués.

Écrire les tests de bordure

Vous devez vous assurer que le comportement ne change pas et que votre code fonctionne bien avec hibernate! Entre autre, est-ce que les annotations sont les bonnes? Est-ce que la gestion des duplicats est géré?

Pour ce faire, vous avez déjà deux *ITest présent, il faut simplement les compléter. Si vous avez trouvé une solution élégante pour la gestion des transactions, ces tests devraient être simple à écrire!

Vous aurez probablement un bug avec l’utilisation du persistence.xml dans les tests. Voir cette explication de stack overflow.

Indices supplémentaires

  1. N’essayez pas d’obtenir exactement le schéma de BD montré. Les noms de table et de colonnes doivent être identiques, mais la syntax exacte est celle de MySQL et non H2.

  2. Relire les limitations/contraintes ci-haut

  3. Informations supplémentaires sur la gestion des objets de JPA

Premièrement, l’EntityManagerFactory sert à créer des connections à la BD. Pour le créer, on utilise Persistence.createEntityManagerFactory (d’où la suggestion d’utiliser un singleton). Créer cette objet est extrèmement long, donc on en veut 1 seul et on doit le démarrer en même temps que l’application. Il y a quelques options ici, dans le main() du serveur, dans l’initialisation du servlet filter, etc…

Ensuite, pour faire des opérations sur la BD (transactions, select, insert, etc), on doit utiliser un EntityManager. On peut voir un entity manager comme une connexion à la BD. On doit ouvrir une connexion par requête dans ce laboratoire-ci (dans la vrai vie il est possible de réutiliser les connexions entre autre - n’essayez pas de le faire). On doit donc ouvrir la connexion au début de la requête et la fermer après. C’est pas mal la définition d’un servlet filter d’agir avant/après la requête.

Afin de conserver la connexion par requête, il est intéressant de noter que Jetty va partir 1 thread par requête, d’où la suggestion ci-haut d’utiliser un ThreadLocal (dans un deuxième singleton? Il y a plusieurs solutions possibles ici).

Maintenant le vrai défi du laboratoire est de trouver une façon élégante pour que le code dans la couche d’infrastructure ait accès à l’entity manager créer au début de la requête. Vous ne pouvez pas le passé à chaque appel de méthode. Il y a moyen d’utiliser l’injection de dépendences, mais si vous avez un singleton déjà, il existe des façons plus simples de faire.

L’objectif final était que c’est le use case qui dicte la portée de la transaction (begin/commit).

En bref:

  1. Relire les limitations/contraintes ci-haut

  2. Mettez les annotations JPA dans le domaine directement, c’est plus simple.

Ce n’est pas la seule façon de faire et ce n’est pas toujours souhaitable, mais ici ça va grandement vous simplifier la vie!! Il y a une raison pourquoi en java c’est OK de faire ainsi, essayez de la trouver! Indice: regardez ça vient de quel librairie…

Il serait possible de mettre des DTO ici avec des mappers, mais c’est trop complexe pour ce lab, n’essayez pas. Les annotations sont dans le domaine, et hibernate s’occupe de faire le DTO vers la BD. Ce n’est pas une mauvaise solution si vous voyez ça en entreprise, mais ça ajoute quand même une bonne compelxité avec hibernate. Vous pouvez tout faire ce qui est demandé juste avec des annotations (LISEZ LA DOC!!!!)

  1. Relire les limitations/contraintes ci-haut

  2. Concernant DomainTransaction.

Le but de cette classe est “d’enrober” un bout de code dans une transaction. La portée de la transaction est décidée par le use case, mais on ne veut pas le faire dépendre de EntityManager directement (sinon la version en mémoire ne marcherait plus). On a donc créé cette abstraction!