TP 5 - associations entre entités (opérations bancaires)

Retour TPs

Introduction

Dans ce TP nous allons manipuler les associations.

Nous continuons le TP sur les comptes bancaires. Nous allons associer à chaque compte bancaire un historique pour suivre les dépôts et les retraits d'argent sur le compte : une liste d'opérations bancaires, sous la forme d'une relation 1-N. 1 compte bancaire sera associé à N opérations.

Par exemple :

Ce TP va vous permettre de voir si vous avez bien assimilé le cours et les TPs précédents. Vous allez apprendre de nouvelles notions sur JPA mais vous allez aussi revoir des situations, principalement liées à JSF, que vous avez déjà rencontrées dans les TPs précédents. Vous devrez écrire le code en étant moins guidé que dans les TPs précédents.

Ressources

Support sur JPA distribué pour ce cours.

Git : repasser dans la branche "master"

Si vous avez travaillé dans la branche "options" dans le TP précédent pour faire les tâches optionnelles, il faut repasser dans la branche "master" :

  1. Menu Team > Repository > Repository Brower.
  2. Clic sur Local. Clic droit sur "master" (ou "main") et choisir "Checkout Revision..." pour passer sur la branche master.
  3. Dans la nouvelle fenêtre pop-up, clic sur le bouton "Checkout" (la valeur du champ "Revision" doit être "master").
  4. Si dans le menu View vous avez coché "Show Versioning Labels", vous devriez voir "[master]" à droite du nom du projet.

Creation d'une classe entité OperationBancaire

Première étape : ajouter au projet une classe entité correspondant à une opération bancaire. Elle aura comme propriétés :

  • un id (sa clé primaire),
  • une description (par exemple "création du compte", "débit", "crédit"...),
  • la date de l'opération (de type java.time.LocalDateTime),
  • le montant de l'opération (positif pour un crédit, négatif pour un débit, le solde initial pour une création).

Ici il vaut mieux choisir LocalDateTime (précision à la nanoseconde près) pour pouvoir trier les opérations bancaires, même si elles ont eu lieu dans la même journée.

Vous ajouterez ensuite des getters et des setters au fur et à mesure des besoins.

@Entity  
public class OperationBancaire implements Serializable {
   
    private static final long serialVersionUID = 1L; 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String description;
   
    private LocalDateTime dateOperation;
  
    private int montant;
                    
    public OperationBancaire() { }
                    
    public OperationBancaire(String description, int montant) {
        this.description = description;
        this.montant = montant;
        dateOperation = LocalDateTime.now();
    }

Ajout de l'association dans la classe entité CompteBancaire

Il suffit de taper la ligne correspondant à l'attribut "liste d'opérations bancaires" dans l'entité CompteBancaire pour qu'une ampoule jaune vous propose d'insérer une annotation correspondant à une relation @OneToMany. Choisissez une association unidirectionnelle (les opérations n'ont pas besoin de "voir" les comptes) et l'annotation @OneToMany sera insérée par NetBeans. Ne copiez/collez pas le code, tapez le !

Vous compléterez l'annotation avec les attributs concernant le cascading et le chargement :

  • CascadeType.ALL signifie que si on persiste un compte on persiste aussi l'historique des opérations, idem pour une suppression.
  • L'attribut FetchType indique que lorsqu'on fait un select sur un compte, on charge aussi son historique.
  @Entity  
  public class CompteBancaire implements Serializable {  
                    
    ...
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)  
    private List<OperationBancaire> operations = new ArrayList<>();  
                    
    public List<OperationBancaire> getOperations() {  
      return operations;  
    }  
    ....

Passez en mode "Drop and Create", puisque le modèle de données va changer !

Double cliquez sur le fichier persistence.xml de votre projet et passez en mode "drop and create" (valeur drop-and-create pour la propriété javax.persistence.schema-generation.database.action). Ainsi, à chaque déploiement du projet, les tables seront supprimées et re-créées. Si vous changez les entités, les tables seront donc toujours bonnes. Les données sur les 4 premiers comptes seront recréées par l'EJB singleton à chaque fois.

Modifiez l'entité CompteBancaire

Il faut associer une operation à chaque création/modification d'un compte.

Chaque fois qu'on crée un compte on va ajouter une opération "Création du compte", on va faire cela dans le constructeur, il faut rajouter :

  operations.add(new OperationBancaire("Création du compte", solde));

De la même manière modifez votre code pour que les méthodes qui permettent de retirer ou de déposer de l'argent ajoutent une opération "Débit" ou "Crédit".

La sauvegarde de vos modifications a dû déclencher le déploiement de l'application et la création des tables. Vérifiez que la structure de la base de données a bien changé et que les opérations bancaires pour la création des comptes tests ont bien été créées. Remarquez la table association. Elle a été créée car l'association 1-N est unidirectionnelle dans le sens 1-N, sinon une simple clé étrangère aurait suffi. C'est un complément de JPA qui n'a pas été vu dans le cours.

Repassez en mode "create", ou même "none" dans persistence.xml. N'oubliez pas le Ctrl-S après avoir modifié persistence.xml. dans persistence.xml.

Modifiez la page JSF qui affiche la liste des comptes

Objectif : Chaque ligne dans le tableau d'affichage des comptes propose un lien qui va diriger vers une autre page permettant d'afficher la liste des opérations du compte (le lien s'appelle "Détails" dans la capture d'écran ci-dessous).

Regardez comment on avait fait dans le TP 1 pour faire afficher les détails sur un client. Cette fois-ci vous allez utiliser une autre façon, avec une balise <param> dans une balise <h:link> pour passer la valeur du paramètre (l'id du compte).

Ajoutez une colonne "Historique opérations" avec des liens pour afficher toutes les opérations effectuées sur les comptes :

Vous remarquez que le titre de la colonne occupe 2 lignes. Il suffit d'écrire 2 <h:outputText>, séparés par <br/> mais vous pouvez aussi utiliser l'attribut escape de <h:outputText>. Ça pourra vous servir dans d'autres occasions. Aide.

Écriture d'une page JSF pour l'affichage de l'historique d'un compte

Il reste maintenant à écrire la page JSF qui va se charger d'afficher l'historique des opérations effectuées sur un compte. Elle est très semblable à celle qui affiche les comptes, elle comprend une datatable (vous pouvez enlever la colonne "Id" si vous préférez) :

Pour faire générer le code de la table par NetBeans, comme dans le TP 1, il vous faut une méthode d'un backing bean, qui retourne une liste d'opérations bancaires. Utilisez cette méthode pour demander à NetBeans de générer le code de la datatable des opérations à partir de cette méthode (drag and drop à partir de la palette des composants). Pour que NetBeans génère les colonnes pour toutes les propriétés de l'entité OperationBancaire il faut d'abord ajouter des getters pour ces propriétés.

Aide si vous voulez mettre en forme la date de l'opération.

Commencez donc par écrire le backing bean de la page. Il contient une propriété pour le compte bancaire dont vous voulez l'historique des opérations. A cause de l'attribut fetch de l'association entre comptes et opérations, vous récupérez toutes les opérations associées à un compte quand vous récupérez un compte. Si vous avez un compte il est donc facile d'écrire dans le backing bean une méthode qui retourne la liste des opérations associées au compte.

Ce compte bancaire est obtenu à partir de l'id qui vient de la page qui affiche la liste des comptes bancaires.

Pour cela vous utiliserez un paramètre de vue (<f:viewParam>) qui met la valeur de l'id dans une propriété du backing bean de la page. Comme dans le TP 1, cet id est utilisé par une méthode qui charge l'entité qui a l'id, dans une balise <f:viewAction> de la balise <f:metadata>.

Remarque : n'oubliez pas d'ajouter la balise <metadata> dans la section "metadata" du client du template.

Optionnel : Écriture du convertisseur

Ce convertisseur va être utilisé dans le paramètre de vue pour mettre la valeur de l'id dans une propriété de type CompteBancaire (pas Long ou String) du backing bean de la page.

La balise <f:viewParam> a un attribut converter qui permet de convertir le paramètre de vue en un autre type. Pour ce cas, l'attribut converter va désigner un convertisseur (converter) qui convertit une String en CompteBancaire et vice versa (revoyez le TP 1 pour le convertisseur de DiscountCode).

Dans le TP 1 ce converter est retourné par une des méthodes du backing bean. Pour ce TP vous allez écrire une classe à part pour le convertisseur (vous auriez aussi pu faire comme dans le TP 1).

Comme le convertisseur aura besoin d'injecter un EJB, vous aller utiliser une nouveauté de Jakarta EE 8 expliquée dans le support de cours (la nouveauté est l'attribut managed) : une classe annotée par @FacesConverter(value = "compteConverter", managed = true) ; compteConverter est la valeur à donner à l'attribut converter de <f:viewParam>. Une autre petite différence avec le TP 1 : l'id n'est pas une String mais un Long et il faut donc en tenir compte dans le code du coonvertisseur.

Tests

Vérifiez en particulier que la création d'un nouveau compte fonctionne correctement. L'opération bancaire de création est-elle bien enregistrée, avec le bon montant ?

Quand tout est au point repassez en mode "None" dans le fichier persistence.xml, si vous ne l'avez pas déjà fait, et effectuez divers opérations. Par exemple, ajoutez ou retirez de l'argent d'un compte ou faites un transfert d'argent entre 2 comptes. Vérifiez que les opérations correspondantes ont bien été enregistrées.

Git

Si tout marche bien, faites un commit et un push sur la branche principale (master ou main) ; si vous avez fait des parties optionnelles du TP précédent, vous avez une autre branche "options".

Prise en compte des accès concurrents

Dans ce genre d'application il faut prendre en compte le fait qu'un compte puisse être accédé et même modifié par plusieurs utilisateurs de l'application.

Ajout d'un attribut dans l'entité CompteBancaire pour le numéro de version

Pour que la gestion de la concurrence soit portable entre plusieurs serveurs d'application, la spécification JPA dit que les entités doivent contenir un attribut annoté par @Version qui correspond à une colonne de la table relationnelle, qui contient le numéro de version de chacune des lignes des tables (ce numéro est incrémenté à chaque modification de la ligne ; voir cours).

Vous allez donc ajouter un tel attribut dans l'entité CompteBancaire :

@Version
private int version;

Juste pour une seule exécution repassez en mode "drop and create" pour que la colonne "version" soit ajoutée dans la table relationnelle. Vérifiez que la colonne a bien été ajoutée et ensuite repassez en mode "create" ou "none". Effectuez diverses opérations sur les comptes et vérifiez que le numéro de version est bien incrémenté automatiquement par JPA (allez dans l'onglet Services).

Stratégie optimiste par défaut

La gestion de la concurrence par défaut par JPA (si on ne fait rien de spécial pour bloquer les données) utilise la stratégie optimiste.

Les données sur lesquelles on travaille (ici un compte bancaire) ne sont pas bloquées au début et on espère (de façon optimiste) que personne d'autre ne va modifier le compe bancaire. Le nouveau cacul du solde est effectuée et c'est seulement au moment du commit qu'un blocage est automatiquement effectué par JPA pour vérifier qu'on a eu raison d'être optimiste, c'est-à-dire que les entités qui vont être traitées par le commit n'ont pas été modifiées par quelqu'un d'autre. Pour notre cas, le numéro de version de l'entité CompteBancaire utilisée pour le traitement est vérifié. Si le numéro de version du compte a été modifié depuis qu'il a été récupéré, notre transaction est annulée.

Vous allez utiliser cette stratégie par défaut.

Lancement de l'application depuis 2 navigateurs

Lancez l'exécution de l'application pour vérifier que tout marche bien. Effacez les logs du serveur d'application pour que les messages à venir soient plus facile à lire.

Maintenant lancez un mouvement (ajout ou retrait) sur un même compte bancaire depuis 2 navigateurs différents (si vous n'avez qu'un seul navigateur vous pouvez aussi ouvrir 2 onglets mais un des 2, pas les 2, doit être en mode navigation privée), appellons les N1 et N2 :

  1. Démarrez l'application sur N1 en tapant le bon URL. Commencez par lister la liste de tous les comptes. Puis cliquez sur un id pour effectuer un mouvement sur ce compte. Ne soumettez pas le retrait ou l'ajout d'argent.
  2. Faites de même sur N2 et cette fois-ci soumettez le formulaire.
  3. Revenez à N1 pour soumettre le formulaire. Normalement vous devez avoir une exception. Expliquez (cherchez "OptimisticLockException"). Correction.

Traitement de l'exception

Modifiez le code pour que l'utilisateur du navigateur N1 reçoivent un message l'informant de la modification concurrente par un autre utilisateur. Correction.

Git

Commit et push sur le dépôt distant.

Il faut maintenant intégrer ce que vous venez de faire dans la branche options pour avoir une version "complète" de l'application, avec options. Dans la réalité, l'intégration se fait plutôt dans l'autre sens, pour intégrer une nouvelle fonctionnalité dans la branche master.

Faites un merge de ce que vous venez de faire dans la branche "options", si vous en avez une. Aide.

Testez avec toutes les parties optionnelles.

Commit et un push sur le dépôt distant si tout fonctionne correctement.

Optionnel : Pour ceux qui ont déjà fini

Utiliser le blocage pessimiste

La stratégie optimiste fonctionne bien si la probabilité de devoir annuler notre transaction est faible.

Si la probabilité d'accès concurrent est importante, il faut utiliser la stratégie pessimiste. Des blocages explicites sont alors nécessaires sur les entités utilisées pendant le traitement.

Un blocage du compte dès qu'il est récupèré n'est pas faisable en CMT (Container Managed Transaction) ni souhaitable.

En effet, la récupération du compte se fait dans une méthode de l'EJB qui n'est pas la méthode qui va effectuer le commit. Si le compte est bloqu dès sa récupération, le blocage va se terminer avant l'exécution de la méthode qui va déclencher le commit, puisqu'un blocage ne dure que le temps d'une transaction. Un moyen de s'en sortir serait d'utiliser le mode BMT (Bean Managed Transaction) mais, le plus souvent ça n'est pas souhaitable car il n'est pas bon de bloquer trop longtemps les données, au risque de gêner les autres utilisateurs. Très souvent le mode BMT peut être évité (à voir au cas par cas).

Ce que l'on cherche : ne pas avoir notre transaction invalidée en cas d'accès concurrent. Voici comment on peut faire dans ce cas particulier, dans la méthode qui va enregistrer le nouveau solde du compte, juste avant le commit :

  1. Récupérer à nouveau le compte à partir de son id, avec blocage de type WRITE (puisqu'on veut pouvoir modifier le compte en évitant un éventuel rollback).
  2. Vérifier que le solde du compte n'a pas été modifié depuis qu'il a été récupéré dans la base de données. Dans notre cas, c'est seulement pour afficher un message d'avertissement à l'utilisateur mais dans d'autre cas, on pourrait refaire alors des calculs ou des procédures.
  3. Calculer le nouveau solde en tenant compte d'un éventuel nouveau solde récupéré à l'étape 1.
  4. La méthode étant finie, un commit est lancé et la modification du solde du compte est enregistrée dans la base de données.

Dans le cas où un accès concurrent aurait modifié le solde du compte depuis sa récupération première, vous pouvez le signaler par un message à l'utilisateur. Vous pouvez, par exemple, lancer une exception d'application (pas système qui provoquerait un rollback) qui est attrapée par le backing bean pour afficher le message à l'utilisateur (un message de type "warning" sans doute).

On a ainsi évité un rollback éventuel sans trop nuire aux performances de l'application.

Exemple de code

Corrections

Cette correction n'intègre pas les fonctionnalités de la section "Pour ceux qui veulent aller encore plus loin" du TP 4. Modifiez le nom et le mot de passe dans la définition de la source de données.

Cette correction intègre les fonctionnalités de la section "Pour ceux qui veulent aller encore plus loin" du TP 4. Modifiez le nom et le mot de passe dans la définition de la source de données.

Retour TPs