Aller au contenu

Exercice - Utilisation de JDBC⚓︎

1. En base de données⚓︎

Nous allons créer un schéma et une table sur celui-ci. Il suffit pour cela d'exécuter le script suivant sur la base de données :

SQL
create database gestion_notes;

CREATE TABLE `gestion_notes`.`STUDENT` (
  `ID_STUDENT` int NOT NULL auto_increment, /*(1)!*/
  `NAME` varchar(45) DEFAULT NULL,
  `FIRST_NAME` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`ID_STUDENT`)
);

CREATE TABLE `gestion_notes`.`NOTE` (
  `ID_NOTE` int NOT NULL auto_increment, /*(2)!*/
  `ID_STUDENT` int,
  `NOTE` decimal(4,2) DEFAULT NULL,
  PRIMARY KEY (`ID_NOTE`),
  FOREIGN KEY (ID_STUDENT)
      REFERENCES STUDENT(ID_STUDENT)
);
  1. Permet d'auto-incrémenter le champ à chaque nouvelle insertion en BDD.
  2. Permet d'auto-incrémenter le champ à chaque nouvelle insertion en BDD.
Avant toutes choses ...

On commencera par créer un projet Web Java, qu'on appelera gestionnotes.

2. Création de la data source⚓︎

Installation du pilote JDBC

Tomcat est un serveur très léger, qui par défaut, contient uniquement un conteneur de Servlet. Pour nous connecter à une base de données, il faut donc ajouter un JAR prévu à cet effet (c'est le pilote qui permet de se connecter à une base de données MySQL - il en faut par exemple un autre pour se connecter à une base de données Oracle).

Nous allons ajouter cette dépendance via Maven, c'est-à-dire en modifiant le fichier 📄pom.xml.

Pour cela, le plus simple est d'aller sur l'entrepôt Maven (en cherchant Maven Repository dans un moteur de recherche).

Dans la zone de recherche, saisir jdbc connector, puis sélectionner le premier résultat (MySQL Connector/J), puis la dernière version.

Il suffit ensuite de cliquer sur le code XML, celui-ci est automatiquement copié :

Le code XML à ajouter dans le fichier 📄pom.xml
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.2.0</version>
</dependency>

Ce code est à ajouter dans la balise <dependencies>.

3. Couche "Modèle"⚓︎

La couche modèle

La couche modèle est composée de 3 beans .

  1. Il y a fr.univtours.polytech.gestionnotes.model.NoteBean. Ce bean est très simple, puisqu'il ne comporte que les propriétés correspondant aux champs de la table NOTE, et les accesseurs et mutateurs associés.
  2. Il y a fr.univtours.polytech.gestionnotes.model.StudentBean. De même, avec la table STUDENT.
  3. Il y a fr.univtours.polytech.gestionnotes.model.ResultBean, qui permet de faire le lien entre les deux tables.

classDiagram
    StudentBean
    class StudentBean{
        -idStudent: Integer
        -name: String
        -fisrtName: String
        +getIdStudent() Integer
        +setIdStudent(idStudent: Integer)
        +getName() String
        +setName(name: String)
        +getFirstName() String
        +setFirstName(firstName: String)
    }
classDiagram
    NoteBean
    class NoteBean{
        -idNote: Integer
        -idStudent: Integer
        -note: Float
        +getIdNote() Integer
        +setIdNote(idNote: Integer)
        +getIdStudent() Integer
        +setIdStudent(idStudent: Integer)
        +getNote() Float
        +setNote(note: Float)
    }
classDiagram
    ResultBean
    class ResultBean{
        -studentBean: StudentBean
        -noteBean: NoteBean
        +getStudentBean() StudentBean
        +setStudentBean(studentBean: StudentBean)
        +getNoteBean() NoteBean
        +setNoteBean(noteBean: NoteBean)
    }

Un bean implémente Serializable

Attention ! Il ne faut pas oublier qu'un bean est une classe qui implémente l'interface Serializable.

4. Couche "Accès aux données"⚓︎

Accès aux données

Nous allons maintenant créer la partie DAO - pour Data Access Object. Cette couche contient des couples :

  • interface - qui définit les accès possibles aux données* (la plupart du temps, ce sont les CRUD),
  • classe - qui implémente cette interface.

Il y a deux tables dans notre base de données, donc nous allons créer deux couples :

  1. fr.univtours.polytech.gestionnotes.dao.NoteDAO et fr.univtours.polytech.gestionnotes.dao.NoteDAOImpl;
  2. fr.univtours.polytech.gestionnotes.dao.StudentDAO et fr.univtours.polytech.gestionnotes.dao.StudentDAOImpl.

Voici les accès qui seront proposés ici :

  1. Pour NoteDAO :
    1. Récupération de la liste de toutes les notes (SELECT ID_NOTE, ID_STUDENT, NOTE from note;).
    2. L'insertion d'une note (INSERT INTO NOTE (ID_STUDENT, NOTE) values (?, ?);).
    3. La mise à jour d'une note (UPDATE NOTE set ID_STUDENT=?, NOTE=?;).
  2. Pour StudentDAO :
    1. Récupération d'un étudiant à partir de son identifiant (SELECT * from note where id = ?;).
    2. L'insertion d'un étudiant (INSERT INTO STUDENT (NAME, FIRST_NAME) values (?, ?);).
    3. La mise à jour d'un étudiant (UPDATE STUDENT set NAME=?, FIRST_NAME=?;).
Le code de l'interface

Cela ce traduit de la façon suivante dans les interfaces DAO :

☕ Code Java - Interface fr.univtours.polytech.gestionnotes.dao.NoteDAO
public List<NoteBean> getNotesList();

public void insertNote(NoteBean note);

public void updateNote(NoteBean note);
☕ Code Java - Interface fr.univtours.polytech.gestionnotes.dao.StudentDAO
public StudentBean getStudent(Integer id);

public void insertStudent(StudentBean student);

public void updateStudent(StudentBean student);
Le début du code de l'implémentation, qu'il faut compléter
Paramètres de connexion

Attention à bien adapter les identifiants de connexion à la BDD en fonction de ce que vous avez.

Dans le code ci-dessous, les lignes surlignées doivent être adaptées en fonction de ta configuration.

Ce sont ici les valeurs par défaut si tu utilises Wamp, UwAmp, ou Xampp. Ce ne sont pas celles par défaut pour MySql Workbench !

Imports

En recopiant le code ci-dessous, un certain nombre d'objets Java est à importer.

Pour rappel, il suffit pour cela d'utiliser le raccourci AltShiftO.

Lors de l'import, il existe plusieurs objets Java venant de package différent. Ici, il faut choisir ceux provenant du package java.sql.

Code de la classe établissant la connexion à la BDD
☕ Code Java - Classe fr.univtours.polytech.gestionnotes.dao.ConnectionDB
package fr.univtours.polytech.gestionnotes.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import javax.naming.NamingException;

/**
 * Cette classe gère la connexion à la base de données.
 * 
 * Elle propose une méthode statique createConnection() qui établit la connexion avec la BDD.
 */
public class ConnectionDB {

    /**
     * À adapter en fonction de vos paramètres !!
     */
    private static final String DB_URL = "jdbc:mysql://localhost:3306/gestion_notes";
    private static final String DB_USER = "root";
    private static final String DB_PWD = "";

    /**
     * Permet d'ouvrir la connexion.
     * 
     * @return L'object {@link ConnectionDB} correspondant.
     * @throws SQLException
     * @throws NamingException
     */
    public static Connection createConnection() throws SQLException, NamingException {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            return DriverManager.getConnection(DB_URL, DB_USER, DB_PWD);
        } catch (SQLException sqle) {
            sqle.printStackTrace();
        } catch (ClassNotFoundException ce) {
            ce.printStackTrace();
        }

        return null;
    }
}
Code de l'implémentation des DAO - NoteDAOImpl

Les lignes surlignées sont à compléter, dans un second temps !

☕ Code Java - Classe fr.univtours.polytech.gestionnotes.dao.NoteDAOImpl
@Override
public List<NoteBean> getNotesList() {
    Statement st = null;
    ResultSet rs = null;
    Connection connection = null;
    List<NoteBean> notesList = null;

    try {
        // Lecture de la table NOTE :
        final String sql = "SELECT ID_NOTE, ID_STUDENT, NOTE from gestion_notes.NOTE;";
        connection = ConnectionDB.createConnection();
        st = connection.createStatement();
        rs = st.executeQuery(sql);
        notesList = mapResultSetToList(rs);
    } catch (Exception e) {
        // S'il y a eu un problème, on le fait remonter.
        throw new RuntimeException(e);
    } finally {
        try {
            // Dans tous les cas, on ferme tout ce qui doit l'être.
            st.close();
            connection.close();
        } catch (Exception e) {
            // S'il y a eu un problème, on le fait remonter.
            throw new RuntimeException(e);
        }
    }
    return notesList;
}

@Override
public void insertNote(NoteBean note) {
    // On utilisera ici des PreparedStatement plutôt que des Statement pour des
    // raisons de sécurité.
    PreparedStatement st = null;
    Connection connection = null;

    try {
        connection = ConnectionDB.createConnection();
        final String sqlInsert = "INSERT INTO NOTE (ID_STUDENT, NOTE) values (?, ?);";
        st = connection.prepareStatement(sqlInsert);
        st.setInt(1, note.getIdStudent());
        st.setDouble(2, note.getNote());
        st.executeUpdate();
    } catch (Exception e) {
        // S'il y a eu un problème, on le fait remonter.
        throw new RuntimeException(e);
    } finally {
        try {
            // Dans tous les cas, on ferme tout ce qui doit l'être.
            st.close();
            connection.close();
        } catch (Exception e) {
            // S'il y a eu un problème, on le fait remonter.
            throw new RuntimeException(e);
        }
    }
}

@Override
public void updateNote(NoteBean note) {
    // On utilisera ici des PreparedStatement plutôt que des Statement pour des
    // raisons de sécurité.
    // Cette méthode n'est à implémenter que dans un second temps, lorsque ce TD est
    // terminé.
}

/**
 * Permet d'effectuer le mapping entre le {@link ResultSet} renvoyé par la
 * requête, et la liste d'objets {@link NoteBean}.
 * 
 * @param rs Le {@link ResultSet} à transformer.
 * @return La liste de {@link NoteBean} qui correspond.
 * @throws SQLException
 */
private final List<NoteBean> mapResultSetToList(final ResultSet rs) throws SQLException {
    List<NoteBean> notesList = new ArrayList<NoteBean>();
    while (rs.next()) {
        // Pour chaque ligne de la table,
        // on instancie un nouveau NoteBean.
        final NoteBean noteBean = new NoteBean();
        noteBean.setIdNote(rs.getInt("ID_NOTE")); // Il faut indiquer le nom du champ en BDD, ici, 'ID_NOTE'.
        noteBean.setIdStudent(rs.getInt("ID_STUDENT"));
        noteBean.setNote(rs.getFloat("NOTE"));
        // On ajoute ce bean à la liste des résultats.
        notesList.add(noteBean);
    }
    return notesList;
}
Code de l'implémentation des DAO - StudentDAOImpl

Les lignes surlignées sont à compléter, dans un second temps !

☕ Code Java - Classe fr.univtours.polytech.gestionnotes.dao.StudentDAOImpl
@Override
public StudentBean getStudent(Integer id) {
    PreparedStatement st = null;
    ResultSet rs = null;
    Connection connection = null;
    List<StudentBean> studentsList = null;

    try {
        // Lecture de la table student :
        final String sql = "SELECT * from gestion_notes.STUDENT where ID_STUDENT = ?;";
        connection = ConnectionDB.createConnection();
        st = connection.prepareStatement(sql);
        st.setInt(1, id);
        rs = st.executeQuery();
        studentsList = mapResultSetToList(rs);
    } catch (Exception e) {
        // S'il y a eu un problème, on le fait remonter.
        throw new RuntimeException(e);
    } finally {
        try {
            // Dans tous les cas, on ferme tout ce qui doit l'être.
            st.close();
            connection.close();
        } catch (Exception e) {
            // S'il y a eu un problème, on le fait remonter.
            throw new RuntimeException(e);
        }
    }
    return studentsList.get(0);
}

@Override
public void insertStudent(StudentBean student) {
    // TODO Auto-generated method stub
}

@Override
public void updateStudent(StudentBean student) {
    // TODO Auto-generated method stub
}

/**
* Permet d'effectuer le mapping entre le {@link ResultSet} renvoyé par la
* requête, et la liste d'objets {@link StudentBean}.
* 
* @param rs Le {@link ResultSet} à transformer.
* @return La liste de {@link StudentBean} qui correspond.
* @throws SQLException
*/
private final List<StudentBean> mapResultSetToList(final ResultSet rs) throws SQLException {
    List<StudentBean> studentsList = new ArrayList<StudentBean>();
    while (rs.next()) {
        // Pour chaque ligne de la table,
        // on instancie un nouveau studentsBean.
        final StudentBean studentBean = new StudentBean();
        studentBean.setIdStudent(rs.getInt("ID_STUDENT")); // Il faut indiquer le nom du champ en BDD, ici,
                                                        // 'ID_STUDENT'.
        studentBean.setName(rs.getString("NAME"));
        studentBean.setFirstName(rs.getString("FIRST_NAME"));
        // On ajoute ce bean à la liste des résultats.
        studentsList.add(studentBean);
    }
    return studentsList;
}
La méthode mapResultSetToList

Une requête en BDD de type SELECT renvoie un objet ResultSet. Cette objet doit être parcouru, et chaque ligne qu'il contient doit être transformée en NoteBean. C'est l'utilité de la méthode mapResultSetToList.

5. Couche "Services métiers"⚓︎

Couche métier

De manière semblable à la couche DAO, il faut créer des couples interface/classe dans cette couche.

La couche DAO est une couche technique, liée aux tables de la BDD.

La couche métier est la couche centrale de l'application. C'est-à-partir de celle-ci que toute l'application se construit.

Le découpage se fait ici par rapport au métier de l'application, c'est-à-dire les services qu'elle propose concrètement. Ici, il n'y a qu'un seul service : c'est l'affichage (et la modification/création) des notes des étudiants.

Il y a donc un seul couple interface/classe :

  • une interface - fr.univtours.polytech.gestionnotes.business.NoteBusiness - qui définit les services métiers disponibles,
  • et une classe - fr.univtours.polytech.gestionnotes.business.NoteBusinessImpl - qui implémente cette interface.

Voici les services qui seront proposés ici :

  1. L'affichage de la liste de toutes les notes saisies.
  2. L'ajout d'une note.
  3. L'ajout d'un étudiant.
  4. La mise à jour d'une note/d'un étudiant.
  5. Le calcul de la moyenne de toutes les notes.

    La moyenne ne doit être affichée que s'il y a des notes en base de données.

    Pour cela, la règle de gestion est la suivante :

    • S'il y a des notes, on calcule la moyenne.
    • S'il n'y a aucune note, la méthode correspondante renvoie null.

Pour rappel :

  1. Dans un premier temps, implémenter les services n°1 et n°5.
  2. Dans un second temps, implémenter les services n°2 et n°3.
  3. Enfin, implémenter le service n°4.
Le code de l'interface
☕ Code Java - Interface fr.univtours.polytech.gestionnotes.business.NoteBusiness
public List<ResultBean> getResultsList();

public void insertNote(NoteBean note);

public void insertStudent(StudentBean studentBean);

public void updateResult(ResultBean result);

public Float computeMean(List<ResultBean> notesList);
Le code de la classe, qu'il faut compléter

Les lignes surlignées sont à compléter.

☕ Code Java - Classe fr.univtours.polytech.gestionnotes.business.NoteBusinessImpl
// Dépendances vers les différents DAO utiles pour ce service métier.
private NoteDAO noteDao;
private StudentDAO studentDAO;

public NoteBusinessImpl() {
    // Instanciation des différentes dépendances.
    this.noteDao = new NoteDAOImpl();
    this.studentDAO = new StudentDAOImpl();
}

@Override
public List<ResultBean> getResultsList() {
    List<ResultBean> results = new ArrayList<ResultBean>();
    List<NoteBean> notes = noteDao.getNotesList();
    System.out.println("Nb results : " + notes.size());
    for (NoteBean noteBean : notes) {
        ResultBean resultBean = new ResultBean();
        resultBean.setNoteBean(noteBean);
        System.out.println(noteBean.getIdStudent() + " - " + noteBean.getNote());
        resultBean.setStudentBean(this.studentDAO.getStudent(noteBean.getIdStudent()));
        System.out.println(resultBean.getStudentBean().getFirstName() + " - " + resultBean.getNoteBean().getNote());
        results.add(resultBean);
    }
    return results;
}

@Override
public void insertNote(NoteBean noteBean) {
    this.noteDao.insertNote(noteBean);
}

@Override
public void insertStudent(StudentBean studentBean) {
    this.studentDAO.insertStudent(studentBean);
}

@Override
public void updateResult(ResultBean resultBean) {
    this.noteDao.updateNote(resultBean.getNoteBean());
    this.studentDAO.updateStudent(resultBean.getStudentBean());
}

@Override
public Float computeMean(List<ResultBean> resultsList) {
    ...
}

6. Couche présentation - implémentation de la vue et du contrôleur⚓︎

Couche présentation

Nous avons implémenté le modèle, il reste à implémenter le contrôleur (ce sont les différentes Servlets) et la vue (ce sont les 3 JSPs).

Depuis le contrôleur, on peut appeler les services métier. Par exemple, pour récupèrer la liste des notes présentes en BDD. Pour cela, comme nous l'avons fait dans un TD précédent, nous allons créer le lien avec la couche service dans la méthode init de chaque servlet :

☕ Code Java - Dans les Servlets
// Dépendances vers les différents services métiers utiles.
// Il n'y en a qu'un seul ici.
private NoteBusiness business;

@Override
public void init() throws ServletException {
    // Instanciation de ces (cette ici) dépendances.
    this.business = new NoteBusinessImpl();
}

Dans les Servlets, on placera deux choses en attribut de la requête :

  1. La liste des résultats à afficher, avec la clef RESULTS_LIST.
  2. La moyenne, si il y a des notes dans la BDD, avec la clef RESULTS_MEAN.

Depuis la vue, il faudra utiliser la JSTL (pour Java Standard Tag Library) pour utiliser des boucles "pour" et des instructions conditionnelles "if".

Configuration de la JSTL

Pour cela, il faut ajouter la directive suivante dans chaque JSP utilisant la JSTL :

Java Server Page
<%@ taglib prefix="c" uri="jakarta.tags.core" %><!--(1)!-->
  1. On utilise les balises c:forEach, c:if, c:out, ... car le préfixe indiqué à l'import de la bibliothèque est c. C'est la valeur "classique" de ce préfixe.

Enfin, lorsqu'on utilise un serveur Tomcat (ce ne sera pas le cas par la suite avec un serveur WildFly), il faut ajouter les deux dépendances suivantes dans le fichier 📄pom.xml :

  • jakarta.servlet.jsp.jstl
  • jakarta.servlet.jsp.jstl-api

Au moment où ceci est écrit, cela revient à ajouter le code suivant dans la balise <dependencies> du 📄pom.xml (mais vérifié dans le référentiel maven qu'il n'y a pas de versions plus récentes !)

Dans le 📄pom.xml
<!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl -->
<dependency>
    <groupId>org.glassfish.web</groupId>
    <artifactId>jakarta.servlet.jsp.jstl</artifactId>
    <version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api -->
<dependency>
    <groupId>jakarta.servlet.jsp.jstl</groupId>
    <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
    <version>3.0.0</version>
</dependency>
Aide pour la mise en place d'une boucle pour

Si la liste des objets NoteBean a été placé dans la requête avec la clef LISTE_NOTES, on pourra utiliser le code suivant, à compléter :

Java Server Page
<table>
    <tr>
        <th>Prénom</th>
        <th>Nom</th>
        <th>Note</th>
    </tr>
    <c:forEach items="${requestScope.LISTE_NOTES}" var="note">
        <tr>
            <td>${note.prenom}</td>
            <td>${note.nom}</td>
            <td>${note.note}
        </tr>
    </c:forEach>
</table>
Aide pour la mise en place d'une instruction conditionnelle

Pour l'affichage de la moyenne, on peut imaginer n'afficher le bloc correspondant que lorsque il y a au moins une note.

Par exemple, si la moyenne, lorsqu'il y a au moins une note donc, est stockée dans la requête avec la clef RESULTS_MEAN, on peut utiliser le code suivant :

Java Server Page
 <c:if test="${not empty requestScope.RESULTS_MEAN}">
    La moyenne est de ${requestScope.RESULTS_MEAN}
</c:if>
Vérification

Vérifier maintenant que l'application est bien fonctionnelle.

Pour vérifier que les notes sont bien affichées, et comme l'IHM pour l'ajout de notes n'est pas encore créée dans la première version, on peut insérer des notes directement en base de données.

Tu peux par exemple exécuter le script suivant :

SQL
INSERT INTO STUDENT (NAME, FIRST_NAME) values ('A', 'Alice');
INSERT INTO STUDENT (NAME, FIRST_NAME) values ('B', 'Bob');

INSERT INTO NOTE (ID_STUDENT, NOTE) values (1, 19.5);
INSERT INTO NOTE (ID_STUDENT, NOTE) values (1, 18);
INSERT INTO NOTE (ID_STUDENT, NOTE) values (2, 14.5);

7. Implémentation de la V2 et de la V3⚓︎

Tu peux maintenant implémenter :

  1. la V2 (nécessaire pour la suite) : ajout de l'IHM et des services permettant d'insérer une note.

    requestDispatcher vs sendRedirect

    Pour cela, il faut soumettre un formulaire (dans lequel on aura indiqué le numéro de l'étudiant et sa note). On utilisera donc une requête HTTP POST.

    Mais il faut ensuite afficher la liste des notes, c'est-à-dire utiliser une requête HTTP GET.

    On ne peut donc pas utiliser request.getRequestDispatcher(location), car dans ce cas, on fait suivre la requête HTTP, et donc le verbe utilisé reste le même.

    On utilisera ici response.sendRedirect(location), qui permet de créer une nouvelle requête HTTP, avec un verbe GET.

  2. la V3 (optionnelle) : ajout de l'IHM et des services permettant de modifier des données insérées en base.