JSP JavaServer Pages

publicité
JSP
JavaServer
Pages
Duane Fields
Éditions Eyrolles
ISBN : 2-212-09228-8
2000
7
Intégration aux bases
de données
Dans ce chapitre :
• Lien entre l’API JDBC et JSP
• Stockage et extraction des composants JSP avec un SGBD relationnel
• Affichage des résultats d’une requête dans des pages JSP
• Établissement des connexions persistantes
Autrefois réservées aux entreprises les plus nanties, les bases de données sont aujourd’hui
intégrées à la plupart des sites web, qu’elles alimentent dynamiquement en données. On peut
citer comme exemple d’utilisation de bases de données sur le web la gestion des bandeaux
publicitaires, des informations utilisateur, des listes de contacts, etc.
Pour de tels besoins, les bases de données et les pages JSP forment une bonne combinaison :
les bases de données relationnelles prennent en charge l’organisation à moindre coût de
grands ensembles de données dynamiques, tandis que les pages JSP offrent un moyen simple
de présenter ces données. On obtient ainsi des applications web dynamiques bénéficiant à la
fois de la puissance des bases de données relationnelles et de la souplesse de présentation des
JSP.
1. JSP et JDBC
À la différence d’autres langages de script tels que ColdFusion, JavaScript côté serveur ou
encore PHP, JSP ne propose pas de balises prédéfinies pour la connectivité à des bases de
données. Les concepteurs des JSP ont en effet préféré tirer avantage de JDBC, une norme
d’interface Java puissante et populaire pour les bases de données.
2
JSP – Java Server Pages
La communication avec une base de données depuis une application JSP nécessite la présence
d’un pilote (driver) propriétaire écrit pour l’API JDBC. Dès lors il s’agit donc du même
moyen d’accès éprouvé et efficace mis au point par Sun. Nous verrons au Chapitre 8 qu’en
pratique, on masque l’accès à la base de données dans une servlet ou un composant Java pour
l’isoler des détails de présentation de la page JSP. Deux approches différentes sont donc possibles, qui sont illustrées Figure 7-1.
Figure 7-1
Requête
Types d’accès possibles
à des bases de données
avec JSP
Servlet
JSP
Accès direct
à partir
d’une page JSP
JSP
Pilote JDBC
API JDBC
Accès à la base
gérée par une servlet ;
les résultats sont passés
à une page JSP
Base de données
L’étude de l’API JDBC n’entre pas dans le cadre de ce livre, sans compter que bien des
ouvrages traitent du sujet qui compte par ailleurs une documentation fournie. Vous trouverez
en particulier des didacticiels en ligne sur le site de Sun à l’adresse http://java.sun.com/products/
jdbc. Nous ne décrivons ici que l’utilisation de JDBC dans des applications JSP.
REMARQUE
Les classes JDBC font partie du paquetage java.sql. Celui-ci doit être importé dans toute classe Java
censée utiliser JDBC, en l’occurrence vos pages JSP. Des extensions supplémentaires peuvent être ajoutées à la version 2.0 de l’API JDBC à partir du paquetage javax.sql, si celui-ci est installé sur votre
système. Si votre pilote JDBC ne se trouve pas dans le dossier de votre moteur de JSP, il faut soit l’importer
dans la page, soit y faire référence par son nom de classe complet.
1.1. JNDI et les autres sources de données
Dans certains systèmes de script utilisant des modèles, tel ColdFusion, l’accès à une base de
données se fait à l’aide d’un identifiant unique représentant une connexion prédéfinie ou un
pool de connexions fourni par l’administrateur système. On fait ainsi l’économie des détails
de connectivité au niveau du code, puisque l’on fait référence aux sources des données par un
identifiant logique tel que DBEmployes ou DBVentes. De cette manière, si un nouveau pilote doit
être utilisé, ou si le serveur de la base est déplacé, ou que les informations de session changent,
il suffit de mettre à jour la description de la ressource ; les composants et les instructions qui
font référence à la ressource nommée ne nécessitent aucun changement.
JSP ne définit pas de système de gestion de ressources particulier. Il faut recourir à l’interface
Datasource de JDBC 2.0 et à l’API JNDI (Java’s Naming and Directory Interface) pour
Intégration aux bases de données
CHAPITRE 7
fournir les services de nommage et de localisation. JNDI permet de séparer le code de l’application des détails concernant la base de données, tels que le pilote, le nom d’utilisateur, le mot
de passe et l’URI de la connexion. Pour créer une connexion à une base de données en utilisant l’interface JNDI, on spécifie un nom de ressource qui correspond à une entrée dans une
base de données ou un service de nommage, pour recevoir les informations nécessaires à
l’établissement de la connexion. Cela protège le code JSP et ses composants des modifications
pouvant être apportées à la configuration de la base. Pour en savoir plus sur l’utilisation de
JNDI, reportez-vous au site de Sun à l’adresse http://java.sun.com/products.jndi. Voici un exemple
de création d’une connexion à partir d’une source de données définie dans le registre JNDI :
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup("jdbc/DBVentes");
Connection con = ds.getConnection("username", "password");
Pour dissocier davantage le code JSP de l’accès à la base et rendre l’accès encore plus simple,
il est possible d’utiliser des balises personnalisées, qui utilisent JNDI pour l’accès à des
ressources nommées – comme ce qui se fait dans ColdFusion et d’autres langages à balises.
1.2. Les instructions préparées (PreparedStatement)
Les instructions préparées, très similaires à des procédures stockées, sont des modèles de
requêtes SQL réutilisables avec des paramètres différents à chaque exécution. La procédure
consiste pour l’essentiel à créer la requête, qui peut être tout type d’instruction SQL, sans
définir les valeurs de certaines variables. Les valeurs manquantes sont spécifiées avant
l’exécution de la requête, qui peut être réexécutée autant de fois que nécessaire. Les instructions préparées sont créées à partir d’un objet Connection, comme des objets Statement
normaux. En SQL, les valeurs des variables à spécifier lors de l’exécution sont représentées
par un point d’interrogation (?) :
String query = "SELECT * FROM GAME_RECORDS WHERE SCORE > ? AND TEAM = ?";
PreparedStatement statement = connection.prepareStatement(query);
L’objet PreparedStatement prend en charge des méthodes d’affectation de valeur de paramètre, correspondant chacune à un type de données particulier, int, long, String, etc. Elles
prennent deux paramètres, d’une part l’indice du paramètre à spécifier et, d’autre part, la
valeur à lui affecter. Les valeurs d’indice commencent à 1 et non à 0. Dans notre cas, pour
obtenir tous les scores supérieurs à 10 000 dans l’équipe « Gold », on initialisera les valeurs
de paramètres de la requête avec les instructions suivantes, avant de l’exécuter :
statement.setInt(1, 10000); // Score
statement.setString(2, "Gold"); // Nom de l’équipe
ResultSet results = statement.executeQuery();
Une fois créée, l’instruction préparée peut être exécutée plusieurs fois avec différentes valeurs
de paramètres. Il n’est pas nécessaire de créer d’instance nouvelle de la procédure stockée tant
que la requête reste analogue. Plusieurs requêtes peuvent ainsi être exécutées sans créer
d’objet Statement. Une instruction préparée peut même être utilisée par différents composants
d’une application ou par différents utilisateurs d’une servlet. En outre, le moteur de la base de
données n’analyse l’instruction SQL qu’une seule fois.
En outre, les instructions préparées permettent le passage de paramètres via un composant
Java plutôt que par un codage littéral. Dans l’exemple suivant, le composant Java userBean,
3
4
JSP – Java Server Pages
initialisé à partir d’un formulaire de saisie, passe des paramètres de requête, d’où une simplification du code et une meilleure lisibilité :
statement.setInt(1, userBean.getScore()); // Score
statement.setString(2, userBean.getTeam()); // Nom de l’équipe
ResultSet results = statement.execute();
Pour obtenir le même résultat sans recourir au passage de paramètres, il faudrait jongler avec
des chaînes SQL, chose difficile dans le cas de requêtes complexes, comme le montre
l’exemple suivant, qui reprend le précédent mais sans faire appel à une instruction préparée :
Statement statement = connection.getStatement();
String query = "SELECT * FROM GAME_RECORDS WHERE SCORE > " +
userBean.getScore() + " AND TEAM = ‘" + user.getTeam() +
userBean.getTeam() + "’";
ResultSet results = Statement.executeQuery(query);
Ce dernier exemple met en évidence un autre avantage décisif de l’utilisation des instructions
préparées, à savoir l’économie des détails de mise en forme. En effet, lorsqu’une valeur de
paramètre est spécifiée dans une instruction préparée à l’aide d’une méthode d’affectation, nul
besoin de se soucier des guillemets nécessaires à l’identification des chaînes de caractères, ni
des caractères d’échappement, des conversions de dates et autres exigences de formats particuliers à la base de données. C’est un avantage stratégique dans le cas de pages JSP destinées
à récolter des paramètres de requête directement à partir de formulaires remplis par les utilisateurs, et de ce fait exposées à d’imprévisibles erreurs de saisie et toutes sortes de caractères
spéciaux. Chaque base de données étant généralement soumise à des contraintes de format
particulières, notamment de date, l’utilisation d’instructions préparées contribue à affranchir
encore votre code des spécificités propres à chaque base.
2. Les pages JSP orientées base de données
Bien des moyens permettent de développer des applications bases de données avec les JSP.
Dans ce chapitre, nous n’allons étudier que l’interaction entre JSP et une base de données,
sans nous attarder sur l’architecture du programme. La conception d’applications JSP sera
traitée au Chapitre 8 et illustrée au Chapitre 9 par un exemple de projet web utilisant une base
de données.
2.1. Intégration des données d’une table dans une page JSP
L’intégration d’une base de données à une application JSP repose principalement sur l’utilisation de composants JavaBeans, servant à faire le lien entre la base et les pages JSP. Chaque
composant permet de récupérer ligne par ligne des données contenues dans une table, grâce à
l’analogie qui peut être établie entre une table de base de données et un composant : la structure d’une table, telle la classe d’un composant, spécifie le type et le nom des données qu’elle
contient ; les colonnes d’une table, telles les propriétés d’un composant, permettent de stocker
les valeurs de chaque instance du type contenu. Enfin, les tables et les classes sont des
modèles de stockage d’informations et, à ce titre, n’ont d’utilité pratique que par les données
qu’elles permettent de stocker (bons d’achat, détails d’inventaire, etc.) ; une table vide n’a pas
d’utilité pratique, tout comme une classe qui n’a pas été instanciée.
Intégration aux bases de données
CHAPITRE 7
En bref, les tables et les classes Java sont des modèles de données conçus pour la gestion
d’informations, concernant des objets ou des événements réels. Retenez ce principe, qui est à
la base du développement de nombreuses applications de base de données JSP.
Les bases de données ont généralement pour rôle d’initialiser dynamiquement un composant
Java à partir des informations d’une table. La configuration de composants JSP à partir
d’une base de données est très simple si la structure de la table (ou les résultats d’une opération de jointure entre plusieurs tables) correspondent une à une aux propriétés du composant. Dans ce cas, il suffit d’invoquer les méthodes d’accès aux enregistrements de la classe
ResultSet et passer au composant les valeurs des colonnes de table correspondantes.
Lorsqu’il y a plusieurs lignes dans les résultats, il est nécessaire de créer autant de composants que de lignes.
Initialisation d’un composant à l’aide de scriptlets
Il est possible d’utiliser des scriptlets JSP pour initialiser les propriétés d’un composant lors
de sa création, après l’établissement de la connexion, avec les données contenues dans
ResultSet. N’oubliez pas d’importer le paquetage java.sql dans la page avec la directive
<%@page import="java.sql.*" %>.
Dans l’exemple suivant, nous utiliserons la classe ItemBean pour représenter un item particulier de l’inventaire, en prenant le numéro d’item dans l’objet de requête :
<%@ page import="java.sql.*" %>
<jsp:useBean id="item" class="ItemBean">
<%
Connection connection = null;
Statement statement = null;
ResultSet results = null;
ItemBean item = new ItemBean();
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
String url = "jdbc:oracle:oci8@dbserver";
String id = request.getParameter(id);
String query = "SELECT * FROM PRODUCTS_TABLE WHERE ITEM_ID = " + id;
connection = DriverManager.getConnection(url, "scott", "tiger");
statement = connection.createStatement();
results = statement.executeQuery(query);
if (results.next()) {
item.setId(results.getInt("ITEM_ID"));
item.setDesc(results.getString("DESCRIPTION"));
item.setPrice(results.getDouble("PRIX"));
item.setStock(results.getInt("QTE_DISPONIBLE"));
}
connection.close();
}
catch (ClassNotFoundException e) {
System.err.println("Impossible de charger le pilote de la base de données.");
}
catch (SQLException e) {
System.err.println("Echec de la connexion à la base de données.");
}
5
6
JSP – Java Server Pages
finally {
try { if (connection != null) connection.close(); }
catch (SQLException e) {
}
}
%>
</jsp:useBean>
<html>
<body>
<table>
<tr><td>Numéro d’article</td><td>
<jsp:getProperty name="item" property="id"/></td></tr>
<tr><td>Description</td><td>
<jsp:getProperty name="item" property="desc"/></td></tr>
<tr><td>Prix</td><td>
<jsp:getProperty name="item" property="prix"/></td></tr>
<tr><td>Disponible</td><td>
<jsp:getProperty name="item" property="stock"/></td></tr>
</table>
</body>
</html>
À la fin de l’exécution de ce programme, soit l’objet ItemBean est vide, si l’instruction SELECT
n’a fourni aucun résultat, soit il contient des données de la table PRODUCTS_TABLE. Après la
création du composant et son initialisation avec des valeurs de la base de données, on affiche
la valeur de ses propriétés. Dans cet exemple, une grande quantité de code Java est nécessaire
pour prendre en charge une mise en forme HTML minimale. Pour écrire plusieurs pages
répondant aux mêmes contraintes de présentation, il faudrait récrire à chaque fois (ou copier,
coller puis adapter) ces instructions. Nous verrons au Chapitre 8 certaines architectures
permettant d’éviter ce type de problème. Pour l’heure, nous pouvons placer ce code à l’intérieur d’un composant capable de récupérer seul des données, comme nous allons le montrer.
Récupération dynamique des données par un composant
Une technique similaire à celle qui a été décrite dans l’exemple précédent peut être utilisée
pour créer des composants susceptibles de récupérer des données dynamiquement. Pour cela,
il suffit d’établir la connexion à la base dans le constructeur du composant, exécuter la
requête, affecter les valeurs obtenues aux propriétés correspondantes puis fermer la
connexion. Il est encore possible de définir certaines propriétés de sorte qu’elles déclenchent
la récupération des données, en incluant le code d’accès à la base dans la méthode. Par
exemple, vous pouvez faire en sorte que la modification de la propriété ID du composant
ItemBean lance la récupération de la ligne correspondante pour initialiser les autres propriétés.
Influence externe
Comme nous le verrons au Chapitre 8, il est souvent préférable de limiter la quantité de code
Java qui se trouve dans une page JSP. Pour ce faire, il est possible d’encapsuler les données
récupérées dans une servlet avant de les intégrer aux composants auxquels la page JSP fait
appel. L’accès à la base de données se fait toujours de la même façon mais l’utilisation de la
servlet permet de partager et de réutiliser la connexion à la base de données. Ainsi, l’on
confiera à une servlet la gestion des connexions et de la récupération des données.
Intégration aux bases de données
CHAPITRE 7
2.2. Types de données JSP et JDBC
Chaque base de données prend en charge des types de données prédéfinis, qui varient sensiblement d’un éditeur à un autre. Heureusement, JDBC sert d’interface entre les types Java et
ceux de la base de données et libère ainsi le développeur Java du souci de gérer le formatage
des données et les différences subtiles qui peuvent exister entre les types équivalents. JDBC
définit un ensemble de types SQL correspondant aux types natifs de la base et établit une
correspondance entre ces types SQL et les types Java, et inversement.
En effet, pour manipuler directement la base de données, par exemple pour établir le schéma
d’une table, on utilise les types de données natifs de SQL. En revanche, pour l’extraction ou
le stockage via JDBC, s’applique le système de typage Java : la manière dont on convertit les
données vers le type SQL approprié dépend des méthodes JDBC invoquées. Pour créer des
composants JSP destinés à interagir avec la base, il faut bien comprendre comment fonctionne
la conversion de ces données. Nous présentons ci-après les principaux types SQL et explicitons
la manière dont ils sont gérés par JDBC.
Nombres entiers
JDBC définit quatre types SQL pour gérer les nombres entiers, mais la plupart des éditeurs de
bases de données ne prennent en charge que deux d’entre eux. Le type SMALLINT représente des
entiers signés sur 16 bits et correspond au type Java short. Le type INTEGER est affecté au type Java
int, et représente une valeur entière signée sur 32 bits. Les deux autres types, TINYINT et BIGINT,
représentent respectivement des entiers sur 8 et 64 bits ; il est rare qu’ils soient pris en charge.
Nombres à virgule flottante
JDBC spécifie deux types de données à virgule flottante, DOUBLE et FLOAT. Ce dernier a été
ajouté pour améliorer la cohérence avec ODBC. Sun recommande en général de s’en tenir au
type DOUBLE, homologue du type Java double.
Chaînes de caractères
On trouve deux types primaires SQL de manipulation de texte, CHAR et VARCHAR. Ils sont traités
en tant qu’objets String par JDBC. Le type CHAR est pris en charge par la plupart des bases de
données. Il contient une chaîne de longueur fixe à la différence de VARCHAR qui contient une
chaîne de longueur variable, pour laquelle est néanmoins spécifiée une longueur maximale.
Le type CHAR peut contenir des chaînes plus courtes que la longueur fixée, auquel cas JDBC
comble les caractères manquants par des espaces. Les navigateurs HTML ignorent les espaces
redondants dans les données JSP renvoyées. Il est en outre possible d’appeler la méthode
trim() de la classe String pour ôter les espaces en fin de chaînes avant de traiter ces dernières.
Enfin, un troisième type est défini par JDBC, à savoir LONGVARCHAR qui peut contenir de très
grands blocs de texte. Malheureusement, la prise en charge du type LONGVARCHAR diffère grandement d’un éditeur à un autre, ce qui rend son utilisation difficile.
Dates et heures
Pour gérer les informations de date et d’heure, JDBC définit trois types de données distincts :
DATE, TIME et TIMESTAMP. Comme leur nom l’indique, ces types de données contiennent des
informations de date (jour, mois et année), des informations horaires (heures, minutes et
secondes), le type TIMESTAMP combinant les informations des deux autres en y ajoutant les
7
8
JSP – Java Server Pages
nano-secondes. Hélas, aucun de ces types ne correspond exactement à la classe Date du
paquetage java.util, qui ne contient pas de champ de nano-secondes.
Ces types SQL sont gérés par trois sous-classes de java.util.Date, à savoir java.sql.Date,
java.sql.Time, et java.sql.Timestamp. En tant que sous-classes de java.util.Date, ces
classes peuvent être utilisées partout où un type java.util.Date est attendu. Cela permet
notamment de les traiter comme des valeurs de date et d’heure normales, tout en préservant la
compatibilité avec la base de données. Il est important de comprendre comment chacune de
ces sous-classes a été dérivée de sa classe de base. La classe java.sql.Date, par exemple,
occulte les champs horaires, tandis que la classe java.sql.Time fait la même chose pour les
champs de date. Ces détails sont à prendre en compte lorsque vous échangez des données
entre la base de données et vos composants JSP. S’il vous faut convertir un objet de la classe
java.sql.Timestamp en son équivalent java.util.Date le plus proche, vous pouvez recourir au
code suivant :
Timestamp t = results.getTimestamp("MODIFIED");
java.util.Date d;
d = new java.util.Date(t.getTime() + (t.getNanos()/1000000));
Le tableau 7-1 fournit un résumé des correspondances de types le plus fréquentes, ainsi que
les méthodes d’accès ResultSet adaptées à chaque type.
Tableau 7-1. Conversions de types Java/JDBC
Type Java
Type JDBC
Méthode d’accès JDBC conseillée
short
SMALLINT
getShort()
int
INTEGER
getInt()
double
DOUBLE
getDouble()
java.lang.String
CHAR
getString()
java.lang.String
VARCHAR
getString()
java.util.Date
DATE
getDate()
java.sql.Time
TIME
getTime()
java.sql.Timestamp
TIMESTAMP
getTimestamp()
Gestion des champs vides
Lorsqu’une colonne de table ne contient pas de données, la valeur null lui est affectée. Or, la
représentation d’absence de valeur (null) par des types de données Java, tels que int et
double, pose problème. En effet, ceux-ci ne sont pas des objets et ne peuvent donc être vides.
Certes, la méthode getInt() pourrait renvoyer 0 ou –1 pour indiquer une valeur vide, mais il
s’agit de valeurs valides. Le même problème se pose pour les types String. Certains pilotes de
base renvoient une chaîne vide ("") tandis que d’autres ne renvoient rien (null) ou une chaîne
null. L’une des solutions, peu élégante mais qui a le mérite d’être efficace, est la méthode
wasNull() de la classe ResultSet, qui renvoie vrai ou faux selon que la dernière méthode
d’accès appelée aurait dû renvoyer une valeur valide ou un vrai null.
Ce problème se pose lors de la création de composants JSP à partir de composants Java.
L’interprétation d’une absence de valeur par la balise <jsp:getProperty> n’est pas homogène
Intégration aux bases de données
CHAPITRE 7
d’un produit à l’autre. Cela rend impossible l’utilisation d’une valeur littérale, et l’approche à
adopter est la même que pour une interface JDBC. Il est cependant possible de définir une
propriété booléenne indiquant la validité de la valeur de propriété en question. Lorsqu’une
valeur nulle est rencontrée dans la base, cette propriété peut prendre une valeur valide à condition qu’un contrôle de validité renvoie bien la valeur false. Dans le programme suivant, on
affecte une valeur à la propriété indiquant la quantité disponible d’un article à partir de la
colonne QTE_DISPONIBLE de la classe ResultSet. Un indicateur signale aussi si la valeur était
une valeur valide.
init() {
...
myQuantity = results.getInt("QTE_DISPONIBLE");
if (results.wasNull()) {
myQuantity = 0;
validQuantity = false;
}
else {
validQuantity = true;
}
...
}
isValidQuantity() {
return validQuantity;
}
Bien entendu, cela implique qu’une vérification préalable de la validité de la valeur ait été
effectuée plus haut dans le code JSP, avant l’utilisation de celle-ci. Pour cela, on peut appeler
la méthode de vérification booléenne :
Quantité disponible :
<% if (item.isValidQuantity()) %>
<jsp:getProperty name="item" property="quantity"/> unités
<% else %>
Inconnue
Si la valeur utilisée dans le programme JSP est simplement destinée à être affichée, on peut
aussi définir une propriété String qui renvoie la valeur appropriée, quel que soit l’état de la
propriété. Une telle approche aurait des conséquences sur la flexibilité du composant mais elle
aurait le mérite de simplifier votre code JSP.
getQuantityString() {
if (validQuantity)
return new Integer(quantity).toString();
else
return "Inconnu";
}
La meilleure façon d’éviter ce problème est encore d’interdire les valeurs non valides dans la
base de données… La plupart des bases de données offrent même la possibilité d’interdire
l’entrée de valeurs non valides au niveau du schéma.
9
10
JSP – Java Server Pages
2.3. Connexions persistantes
On souhaiterait parfois utiliser une connexion pour plusieurs requêtes d’un même client. Il
faut cependant tenir compte de ce que le nombre de connexions qu’un serveur peut prendre en
charge est limité. Les connexions persistantes sont possibles tant que le nombre d’utilisateurs
simultanés reste modeste. Mais en cas de fort trafic réseau, il n’est pas souhaitable d’établir
une connexion à la base à chaque requête car c’est sans doute l’un des processus les plus lents
de l’application ; il faut l’éviter dans la mesure du possible.
Un certain nombre de solutions s’offrent à vous. La première est celle de la mise en cache de
connexions (connection pooling) qui est implémentée soit par le pilote de la base de données,
soit par des classes dédiées. Cette technique permet d’entretenir un nombre fixe de
connexions et de les « prêter » à mesure qu’elles sont demandées par des pages JSP ou des
composants Java. La mise en cache de connexions est un bon compromis entre un trop grand
nombre de connexions simultanées ouvertes et une trop grande fréquence de connexions et de
déconnexions qui ont un coût en termes de performances.
Le listing 7-1 crée un composant qui encapsule une connexion à une base de données. L’utilisation de cette classe ConnectionBean permet d’isoler la page JSP des détails de connexion, tout
en rendant la connexion persistante sur plusieurs pages en l’attachant à la session. De cette
manière, il n’est pas utile de redemander une connexion à la base à chaque fois. Des méthodes
supplémentaires ont été ajoutées pour appeler les méthodes correspondantes de l’objet
connexion encapsulé. Notez que les paramètres d’accès à la base sont codés littéralement pour
simplifier le programme. Il va sans dire qu’il est souvent préférable de les rendre configurables.
Listing 7-1. Code source de ConnectionBean.java
package com.taglib.wdjsp.databases;
import java.sql.*;
import javax.servlet.http.*;
public class ConnectionBean implements HttpSessionBindingListener {
private Connection connection;
private Statement statement;
private static final String driver="postgresql.Driver";
private static final String dbURL="jdbc:postgresql://slide/test";
private static final String login="guest";
private static final String password="guest";
public ConnectionBean() {
try {
Class.forName(driver);
connection=DriverManager.getConnection(dbURL,login,password);
statement=connection.createStatement();
}
catch (ClassNotFoundException e) {
System.err.println("ConnectionBean : pilote non disponible");
connection = null;
}
catch (SQLException e) {
System.err.println("ConnectionBean : pilote non chargé");
connection = null;
Intégration aux bases de données
CHAPITRE 7
}
}
public Connection getConnection() {
return connection;
}
public void commit() throws SQLException {
connection.commit();
}
public void rollback() throws SQLException {
connection.rollback();
}
public void setAutoCommit(boolean autoCommit)
throws SQLException {
connection.setAutoCommit(autoCommit );
}
public ResultSet executeQuery(String sql) throws SQLException {
return statement.executeQuery(sql);
}
public int executeUpdate(String sql) throws SQLException {
return statement.executeUpdate(sql);
}
public void valueBound(HttpSessionBindingEvent event) {
System.err.println("ConnectionBean: dans la méthode valueBound");
try {
if (connection == null || connection.isClosed()) {
connection =
DriverManager.getConnection(dbURL,login,password);
statement = connection.createStatement();
}
}
catch (SQLException e) { connection = null; }
}
public void valueUnbound(HttpSessionBindingEvent event) {
try {
connection.close();
}
catch (SQLException e) { }
finally {
connection = null;
}
}
protected void finalize() {
11
12
JSP – Java Server Pages
try {
connection.close();
}
catch (SQLException e) { }
}
}
La classe ConnectionBean implémente HttpSessionBindingListener, et se déconnecte automatiquement de la base si le composant est retiré de la session. La durée de vie de la connexion
est ainsi limitée lorsqu’elle n’est plus utilisée, avant même sa suppression par le ramassemiettes.
Ce composant n’a pour but que d’isoler l’application des détails de connexion. Nous pourrions aussi créer un composant d’usage plus général, contenant les valeurs de configuration
nécessaires (url, username, password et driver) que la page JSP devrait initialiser pour activer
la connexion.
2.4. Gestion de grands ensembles de résultats
Si la requête envoyée à la base renvoie un grand nombre de lignes, il n’est pas recommandé
de les afficher toutes. Une table de 15 000 lignes, par exemple, est non seulement difficile à
lire mais excessivement longue à télécharger sous format HTML. Si la manière dont est
conçue l’application le permet, il est bon d’imposer une limite au nombre de lignes qu’une
requête peut renvoyer – le moyen le plus rapide étant encore de demander à l’utilisateur de
préciser sa recherche.
Une solution encore meilleure consiste à n’afficher qu’une page à la fois. Il y a bien des façons
de le faire avec JSP. L’interface RowSet a été ajoutée dans la version 2.0 de JDBC pour normaliser l’accès à des données mises en cache à travers un composant Java ou via des systèmes
distribués.
Création d’un objet ResultSet persistant
Lorsqu’une requête renvoie un objet ResultSet, tous les résultats ne sont pas en mémoire. La
base de données, en effet, entretient une connexion vers la base et renvoie les lignes au fur et
à mesure qu’elles sont demandées. Un tel comportement a l’avantage de nécessiter peu de
bande passante et peu de mémoire, mais suppose un temps de connexion long, ce qui peut
poser des problèmes dans des environnements réseau très denses où le recyclage des
connexions doit être fréquent. Le pilote de la base de données détermine le nombre optimal de
lignes à extraire à la fois, nombre qu’il est désormais possible de configurer vous-même dans
la version 2.0 de JDBC. Le renvoi des lignes suivantes s’effectue automatiquement à mesure
que vous progressez dans les résultats du ResultSet ; il est inutile de faire manuellement le
suivi de la position du curseur.
Ensuite, on peut parcourir les ResultSet page par page, à raison de 20 lignes par page, par
exemple. Il suffit de définir une boucle à travers 20 lignes, maintenir l’objet ResultSet dans la
session et recommencer une autre boucle sur les 20 lignes suivantes. La position interne du
curseur ne change pas d’une requête à une autre, ce qui permet de lancer la requête exactement là où s’était arrêtée la précédente. Il n’est pas nécessaire de conserver explicitement une
référence à l’objet Connection car l’objet ResultSet le fait de lui-même. Lorsque celui-ci sort
du champ de visibilité et est supprimé par le ramasse-miettes, l’objet Connection est lui aussi
supprimé.
Intégration aux bases de données
CHAPITRE 7
Vous pouvez aussi encapsuler l’objet ResultSet dans un composant et implémenter HttpSessionBindingListener pour fermer automatiquement les connexions à la base dès qu’elles ne
sont plus utilisées, voire mettre à disposition une méthode de nettoyage et l’invoquer en fin
d’exécution de la page JSP. Mais cela pose quand même un problème, à savoir de devoir
laisser ouverte la connexion à la base aussi longtemps. Pour remédier à cela, nous allons maintenant étudier d’autres solutions, qui évitent de maintenir la connexion ouverte lorsque l’utilisateur navigue de page en page.
Réexécution des requêtes plusieurs fois
Dans cette technique, la requête de recherche est réexécutée pour chaque page de résultats
affichée, la seule condition étant de stocker la position du curseur dans la session utilisateur.
À chaque étape, la requête originale est réémise, puis la méthode next()de la classe ResultSet
est appliquée (voire la méthode absolute()de la version 2.0 de JDBC) pour faire avancer le
curseur jusqu’à la position appropriée. On affiche ensuite les 20 lignes. Au deuxième chargement de la page, on fait avancer le curseur de 20 lignes, au troisième chargement, on augmente
le déplacement à 40 lignes, et ainsi de suite. Pour faire savoir à l’utilisateur où il se trouve dans
l’ensemble des résultats, il suffit de noter la taille de l’objet ResultSet. En connaissant le
nombre de lignes affichées à chaque fois, vous pourrez afficher une information du type
« Page 1 sur 5 ». Cette technique présente un inconvénient, celui de réinitialiser l’affichage de
la base de données à chaque fois, de sorte qu’une modification des données entre deux
requêtes peut modifier l’apparence des résultats pour l’utilisateur.
Préciser la requête automatiquement
Une autre solution consiste à faire en sorte qu’à chaque nouvelle requête, celle-ci contienne
des paramètres pour l’affichage des résultats qui n’ont pas encore été affichés. Cette méthode
ne peut être utilisée que dans certains cas. Il s’agit ici d’afficher une page de données, puis de
stocker la clé primaire du dernier enregistrement affiché. Pour chaque page, il faut émettre une
nouvelle requête, mais avec une clause WHERE limitant la recherche aux éléments non encore
affichés.
Ce procédé est remarquable lorsque les données de la table sont classées d’une manière
ordonnée, par numéro de produit par exemple. Si le dernier enregistrement affiché correspondait au numéro de produit 8375, il suffit de stocker ce nombre dans la session et d’ajouter une
clause WHERE dans la requête suivante pour en tenir compte, comme ceci :
SELECT * FROM PRODUCTS WHERE ID > 8357
Utilisation du composant CachedRowSet
Lorsque les résultats d’une requête sont plus maniables, c’est-à-dire que, sans tenir dans une
seule page, ils n’accaparent pas pour autant toute la mémoire, vous pouvez utiliser le composant CachedRowSet. L’interface RowSet de JDBC 2.0 encapsule dans un composant JavaBeans,
appelé CachedRowSet, une connexion à la base de données et les résultats de la requête correspondante. Ce composant fournit un conteneur navigable déconnecté pour accéder à des résultats de requêtes SQL depuis une page JSP ou depuis un conteneur JavaBeans. Il s’agit là d’un
outil fort utile permettant de travailler sur des données de la base depuis l’intérieur de la page
JSP. Cette classe fait partie des extensions facultatives de JDBC 2.0. Pour en savoir davantage
sur ce composant, rendez-vous sur le site web de Sun consacré à JDBC, http://java.sun.com/
products/jdbc. À la différence de la classe ResultSet, le composant CachedRowSet offre une
13
14
JSP – Java Server Pages
« connexion » hors ligne, en mettant en cache tous les résultats de la requête. Aucune
connexion active n’est requise car l’ensemble des données demandées a déjà été collecté dans
la base. Cette technique est très commode mais peut poser des problèmes de sur-utilisation de
la mémoire dans le cas de très grands ensembles de résultats – auquel cas la meilleure solution
est probablement celle d’une connexion persistante.
CachedRowSet est très simple d’utilisation. Il suffit de configurer les propriétés nécessaires,
telles que le nom d’utilisateur (username), le mot de passe (password) et l’URL de la base de
données, puis de définir la propriété command de votre requête SQL. Il est également possible
d’utiliser un objet RowSet, créé à partir d’une autre requête, pour remplir le composant
CachedRowSet.
Exemple : parcourir les résultats avec un composant CachedRowSet
Dans cet exemple, nous allons afficher page après page les résultats d’une requête en utilisant
le composant Java CachedRowSet et JSP. Nous commencerons par collecter les données dans la
base, pour les afficher ensuite dans des pages de cinq lignes. L’utilisateur aura la possibilité de
revenir au premier résultat s’il le souhaite. Nous aurions pu faire la même chose avec un objet
ResultSet persistant, mais il aurait alors fallu recourir à des scriptlets JSP ou encapsuler
l’objet ResultSet dans notre propre composant (voir figure 7-2). Le code de notre programme
est donné dans le listing 7.2.
Figure 7-2
Navigation
dans les résultats
à partir
d’un objet
CachedRowSet
Listing 7-2. Code source de la page CachedResults.jsp
<%@ page import="java.sql.*,javax.sql.*,sun.jdbc.rowset.*" %>
<jsp:useBean id="crs" class="CachedRowSet" scope="session">
<%
try { Class.forName("postgresql.Driver"); }
Intégration aux bases de données
CHAPITRE 7
catch (ClassNotFoundException e) {
System.err.println("Erreur" + e);
}
%>
<jsp:setProperty name="crs" property="url"
value="jdbc:postgresql://slide/test" />
<jsp:setProperty name="crs" property="username"
<jsp:setProperty name="crs" property="password"
<jsp:setProperty name="crs" property="command"
value="select * from navettes order by id"
<%
try { crs.execute(); }
catch (SQLException e) { out.println("Erreur
%>
</jsp:useBean>
value="invité" />
value="pomme" />
/>
SQL : " + e); }
<html>
<body>
<center>
<h2>Résultats mis en cache</h2>
<p>
<table border="2">
<tr bgcolor="tan">
<th>id</th><th>Aéroport</th><th>Départ</th><th>Sièges</th></tr>
<%
try {
if ("first".equals(request.getParameter("action")))
crs.beforeFirst();
for (int i=0; (i < 5) && crs.next(); i++) {
%>
<tr>
<td><%= crs.getString("id") %></td>
<td><%= crs.getString("aéroport") %></td>
<td><%= crs.getString("heure") %></td>
<td><%= crs.getString("sièges") %></td>
</tr>
<% } %>
</table>
</p>
<%
if (crs.isAfterLast()) {
crs.beforeFirst(); %>
<br>Fin des résultats<br>
<% } }
catch (SQLException e) { out.println("Erreur SQL " + e); }
%>
<a href="<%= HttpUtils.getRequestURL(request) %>?action=first">
[5 premiers]</a> 
<a href="<%= HttpUtils.getRequestURL(request) %>?action=next">
[5 suivants]</a> 
</center>
</body>
</html>
15
Téléchargement