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>&nbsp; <a href="<%= HttpUtils.getRequestURL(request) %>?action=next"> [5 suivants]</a>&nbsp; </center> </body> </html> 15