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 :
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)
);
- Permet d'auto-incrémenter le champ à chaque nouvelle insertion en BDD.
- 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é :
<!-- 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 .
- 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 tableNOTE
, et les accesseurs et mutateurs associés. - Il y a
fr.univtours.polytech.gestionnotes.model.StudentBean
. De même, avec la tableSTUDENT
. - Il y a
fr.univtours.polytech.gestionnotes.model.ResultBean
, qui permet de faire le lien entre les deux tables.
|
|
|
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 :
fr.univtours.polytech.gestionnotes.dao.NoteDAO
etfr.univtours.polytech.gestionnotes.dao.NoteDAOImpl
;fr.univtours.polytech.gestionnotes.dao.StudentDAO
etfr.univtours.polytech.gestionnotes.dao.StudentDAOImpl
.
Voici les accès qui seront proposés ici :
- Pour
NoteDAO
:- Récupération de la liste de toutes les notes (
SELECT ID_NOTE, ID_STUDENT, NOTE from note;
). - L'insertion d'une note (
INSERT INTO NOTE (ID_STUDENT, NOTE) values (?, ?);
). - La mise à jour d'une note (
UPDATE NOTE set ID_STUDENT=?, NOTE=?;
).
- Récupération de la liste de toutes les notes (
- Pour
StudentDAO
:- Récupération d'un étudiant à partir de son identifiant (
SELECT * from note where id = ?;
). - L'insertion d'un étudiant (
INSERT INTO STUDENT (NAME, FIRST_NAME) values (?, ?);
). - La mise à jour d'un étudiant (
UPDATE STUDENT set NAME=?, FIRST_NAME=?;
).
- Récupération d'un étudiant à partir de son identifiant (
Le code de l'interface
Cela ce traduit de la façon suivante dans les interfaces DAO :
public List<NoteBean> getNotesList();
public void insertNote(NoteBean note);
public void updateNote(NoteBean note);
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 Alt+Shift+O.
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
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 !
@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 !
@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 :
- L'affichage de la liste de toutes les notes saisies.
- L'ajout d'une note.
- L'ajout d'un étudiant.
- La mise à jour d'une note/d'un étudiant.
-
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 :
- Dans un premier temps, implémenter les services n°1 et n°5.
- Dans un second temps, implémenter les services n°2 et n°3.
- Enfin, implémenter le service n°4.
Le code de l'interface
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.
// 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 :
// 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 :
- La liste des résultats à afficher, avec la clef
RESULTS_LIST
. - 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 :
<%@ taglib prefix="c" uri="jakarta.tags.core" %><!--(1)!-->
- On utilise les balises
c:forEach
,c:if
,c:out
, ... car le préfixe indiqué à l'import de la bibliothèque estc
. 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 !)
<!-- 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 :
<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 :
<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 :
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 :
-
la V2 (nécessaire pour la suite) : ajout de l'IHM et des services permettant d'insérer une note.
requestDispatcher
vssendRedirect
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. -
la V3 (optionnelle) : ajout de l'IHM et des services permettant de modifier des données insérées en base.