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 modifications effectuées sur les comptes, en particulier les dépôts et les retraits d'argent. Il y aura une association 1-N entre les comptes bancaires et les opérations sur ces comptes : 1 compte bancaire sera associé à N opérations. L'association sera unidirectionnelle, des comptes vers les opérations.

Voici, par exemple, ce qui pourrait être affiché pour les opérations effectuées sur le compte de Georges Harrison :

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 que vous avez déjà rencontrées. Vous serez donc 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" (sinon passez au point suivant) :

  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), N'oubliez pas d'indiquer le type IDENTITY pour la génération de la clé.
  • 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. Remarquez comment la valeur de dateOperation est donnée automatiquement dans le constructeur que vous ajoutez. N'oubliez pas que, comme pour les getters et les setters, NetBeans peut vous aider à écrire le code du constructeur (Alt-Insert ou bien menu Source > Insert Code...).

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.IDENTITY)
    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

Dans l'entité CompteBancaire tapez l'annotation et la ligne qui donne l'association avec OperationBancaire.

Remarque : NetBeans vous proposera de compléter les attributs dès que vous commencerez à taper leur nom. Si vous tapez Ctrl-[espace], il vous propose aussi les valeurs des attributs.

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 (voir cours pour une définition plus complète).
  • 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;  
    }  
    ....
Remarque importante sur le mode de récupération des opérations bancaires.

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

Dans le fichier persistence.xml de votre projet, passez en mode "drop and create" (valeur drop-and-create pour la propriété jakarta.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 le bean CDI Init à 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", avec la bonne valeur pour le montant (montant négatif si c'est un débit).

Lancez l'exécution du projet. Depuis NetBeans, onglet Services, 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 (un "refresh" de l'entrée "Tables" peut être nécessaire pour voir les nouvelles tables). Remarquez la table association. Elle a été créée car l'association 1-N est unidirectionnelle dans le sens 1-N, si l'association était bidirectionnelle ou unidirectionnelle dans le sens N-1, une simple clé étrangère dans operationbancaire aurait suffi. C'est un complément de JPA qui n'a pas été vu dans le cours.

Dans l'application, ajoutez une somme d'argent à un des comptes. Allez voir dans NetBeans si l'opération bancaire a bien été créée dans la table relationnelle et si la table association a bien enregistré cet ajout.

Si tout est correct, repassez en mode "create", ou même "none" (puisque toutes les tables ont déjà été créées) dans persistence.xml. N'oubliez pas le Ctrl-S après avoir modifié persistence.xml.

Git et GitHub...

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 <f:param> dans une balise <h:link> pour passer la valeur du paramètre (l'id du compte). Un exemple de l'utilisation de <param> est donné dans le cours dans la section "PRG".

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.

Lorsque l'utilisateur cliquera sur "Détails" d'une ligne, le navigateur affichera la page qui contient l'historique des opérations effectuées sur le compte de la ligne. L'URL de la requête GET générée par le clic sur la ligne du compte d'id 1 se terminera par "operations.xhtml?id=1" (en supposant que la page qui affiche les opérations bancaires sur un compte se nomme "operations.xhtml").

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

Il reste maintenant à écrire la page JSF operations.xhtml 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) :

Code du backing bean

Pour profiter des aides de NetBeans il vaut mieux commencer par écrire le backing bean de la page.

Comme vous l'avez déjà fait plusieurs fois, la page va contenir une section metadata qui va récupérer le paramètre de la requête qui contient l'id du compte et qui va ensuite récupérer le compte qui a cet id. Pour cela, le backing bean Operations.java doit contenir

  • Une propriété (getter et setter) pour l'id du compte dont on veut l'historique des opérations ;
  • Une méthode pour récupérer un compte à partir de son id.

Le backing bean doit aussi contenir

  • Une propriété (le getter suffit) qui contient le compte bancaire qui correspond à cet id.
  • Une propriété qui contient toutes les opérations associées au compte. A cause de l'attribut fetch de l'association entre comptes et opérations, toutes les opérations associées à un compte sont récupérées en même temps que le compte. Il est donc facile d'écrire dans le backing bean le getter pour cette propriété. Cette propriété sera utilisée par la datatable pour faire afficher les opérations effectuées sur le compte bancaire.

Code de la page

Ecrivez ensuite la page operations.xhtml.

N'oubliez pas que cette page est cliente du template utilisé dans l'application. Décochez la section "left" (qui affiche le menu) mais ne décochez pas la section "metadata" pour ce client du template.

La section metadata (n'oubliez pas d'y ajouter la balise <f:metadata>) contient

  • Un paramètre de vue (<f:viewParam>) pour récupérer l'id qui est passé en paramètre dans l'URL généré par le clic dans la page qui liste tous les comptes. La valeur de l'id est mise dans une propriété du backing bean de la page.
  • Une viewAction (<f:viewAction>) qui utilise, comme dans le TP 1, une méthode du backing bean de la page pour récupèrer le compte bancaire qui a l'id et met l'entité dans une propriété du bean.

Il reste à écrire le code de la datatable qui contient les opérations.

++ La facilité de NetBeans pour ajouter le code d'une table à partir d'une entité ne fonctionne pas pour le moment... Ecrivez donc vous-même le code de la datatable dans la section "content", en prenant exemple sur le code de la page qui affiche tous les comptes.

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.

++

Quand vous écrivez le code de la page, aidez-vous de la complétion de code offerte par NetBeans. Si, par exemple, vous tapez "#{operations.," NetBeans doit vous proposer les propriétés du backing bean. De même, en écrivant le code de la datatable NetBeans doit vous proposer les propriétés de l'entité. Si ça n'est pas le cas, c'est que vous n'avez pas écrit les codes de ces propriétés dans le backing bean ou l'entité et il vous faut les ajouter.

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

Optionnel : Écriture d'un 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. Vous n'aurez plus besoin de la balise <f:viewAction> que vous venez d'utiliser.

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).

Vous aller utiliser l'attribut managed (étudié dans le support de cours) qui permet d'injecter un bean CDI dans une classe convertisseur (interdit sinon) : la classe du convertisseur est 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".

N + 1 ordres SQL lancés

Dans les logs du serveur d'application, allez voir les ordres SQL lancés. Ils sont affichés dans les logs puisque vous avez mis le niveau de log à FINE dans persistence.xml.

Remarquez qu'au premier affichage de la liste, N + 1 selects sont lancés (N est le nombre de comptes) :

  • Un select pour avoir les comptes ;
  • Ensuite un select par compte, pour avoir les opérations.

Pourquoi ? Réponse.

Pour les affichages suivants de la liste, un seul select est lancé (vérifiez). Pourquoi ? Réponse.

Si N est grand, le nombre de select lancés sera donc grand et cela pourrait nuire aux performances. Ne pourriez-vous pas améliorer votre code pour l'éviter ? Réponse.

[ Exercice optionnel

En première lecture passez à l'exercice suivant. Revenez à cet exercice après avoir fini la fin du TP, s'il vous reste du temps.

Faites une copie du projet NetBeans pour garder 2 versions du projet. Si vous êtes un "expert" Git, créez plutôt une nouvelle branche (Team > Branch/Tag > Create Branch...) et travaillez dans cette branche.

Modifiez le code de l'application ainsi :

  • L'association entre un compte et les opérations bancaires faites sur ce compte n'est plus en mode eager. Si vous testez alors l'application, vous verrez dans les logs du serveur qu'une requête "select" est lancée quand l'utilisateur clique sur le lien pour afficher les opérations.
  • Modifiez une requête JPQL (laquelle ?) pour récupérer les opérations bancaires au moment de l'affichage de la table des comptes ; utilisez join fetch. Toutes les opérations étant déjà en mémoire, vérifiez dans les logs qu'une requête "select" n'est plus lancée quand l'utilisateur clique sur le lien pour afficher les opérations. Comparez aussi la requête "select" qui est lancée pour récupérer les comptes avec celle qui était lancée sans le join fetch.

Testez et faites un commit Git.

Juste pour voir, enlevez le join fetch. Que se passe-t-il si l'utilisateur clique sur le lien "Détails" ? Comment pourriez-vous alors faire afficher les opérations sur un compte quand l'utilisateur clique sur le lien "Détails" ?

Faites un checkout pour revenir sur la branche Git principale (Team > Checkout > Checkout Revision....). ]

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 sur JPA).

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 (faites un refresh de la table) 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 de NetBeans).

Git...

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 compte bancaire. Le nouveau cacul du solde est effectué 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 que le compte 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 qui s'affiche dans votre navigateur. Allez plutôt voir l'exception dans les logs du serveur car les explications sont plus détaillées. Expliquez (cherchez "OptimisticLockException"). Correction.

Traitement de l'exception

Évidemment, une application d'entreprise ne peut se permettre de faire afficher un tel message d'erreur dans le navigateur.

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.

Si vous avez créé avant une branche "options", 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 si les transactions sont gérées par le container, ni souhaitable. En effet, la récupération du compte se fait dans une méthode du bean CDI transactionnel 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 de faire gérer les transactions par le code (UserTransacion) 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.

Si on veut bloquer les données de façon pessimiste pour éviter des rollack trop fréquent en cas d'accès concurrent par d'autres processus, il faut essayer de s'arranger pour effectuer le traitement qui modifiera les données pas trop longtemps avant le commit. Les cas où ça n'est pas possible sont plus complexes à programmer car il ne faut pas bloquer des données trop longtemps et gêner les autres utilisateurs.

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. Vous pouvez, par exemple, lancer une exception qui ne provoquera pas un rollback, qui est attrapée par le backing bean pour afficher le message à l'utilisateur (un message de type "warning" sans doute).
  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. Le blocage pessismiste est alors supprimé automatiquement par le commit.

On a ainsi évité un rollback éventuel sans trop nuire aux performances de l'application (le compte n'a pas été bloqué trop longtemps).

Exemple de code

Retour TPs