JDBC est une API intégrée à Java qui facilite l’accès aux Systèmes de Gestion de Bases de Données Relationnelles (SGBDR).
Elle vise à offrir une interface uniforme pour interagir avec différents SGBDR, principalement en utilisant SQL et en proposant des types de données adaptés à l’écosystème Java.
JDBC permet de se connecter à une base de données, d’exécuter des requêtes SQL, et de naviguer à travers les résultats obtenus.
Tout en étant flexible pour exploiter les fonctionnalités spécifiques d’un SGBDR particulier, JDBC peut nécessiter des ajustements pour maintenir la portabilité du code entre différents systèmes.
Système de gestion de base de données et Dataset
Dans le cadre de ce cours nous utiliserons les systèmes de gestion de base données PostgreSQL
Pour les travaux pratiques n’importe quel SGBDR pourra être utilisé par exemple h2.
H2 est un SGDB écrit en Java qui peut s’executer comme un serveur indépendant ou depuis un programme Java. Il peut aussi être utilisé purement en mémoire ou avec persistence sur disque.
Pour l’utiliser il suffit de télécharger le fichier .jar de h2 par exemple la version h2-2.2.224.jar depuis un entrepôt maven.
Le serveur peut être exécuté simplement avec la commande suivante pour autoriser les connexions tcp et la création automatique d’une base de données lors du premier accès.
Pour accéder à la base de données, il suffit de se connecter à l’URL jdbc:h2:tcp://localhost/Gtfs_RMTT;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE avec le nom d’utilisateur sa et un mot de passe vide.
java-cp h2*.jar org.h2.tools.RunScript \-url"jdbc:h2:tcp://localhost/Gtfs_RMTT;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"\-user sa \-script data.sql
Pour illustrer ce cours, nous allons utiliser une base de données qui représente des données de transport (des bus) au format GTFS (General Transit Feed Specification).
Format de données standardisé créé par Google
Permet le partage des données de transport public
Utilisé par Google Maps et autres applications
Nous utiliserons une extensions de postgresql pour les Systemes d’Information Géographique (SIG) PostGIS qui permet de stocker des données géographiques et de faire des requêtes spatiales.
Conversion des données pour l’intégré dans le système d’information géographique (SIG) PostGIS. PostGIS est une extension du SGBD PostgreSQL qui permet la manipulation d’informations géographiques (spatiales) sous forme de géométries (points, lignes, polygones), conformément aux standards établis par l’Open Geospatial Consortium. En d’autres termes, PostGIS permet le traitement d’objets spatiaux dans PostgreSQL, autorisant le stockage des objets graphiques en base de données pour les systèmes d’informations géographiques (SIG).
%%shelldocker run --rm --quiet --volume shared-data:/shared-data \--entrypoint sh \ ghcr.io/public-transport/gtfs-via-postgres:4-eu -o pipefail -c '/app/cli.js $@--require-dependencies --trips-without-shape-id --lower-case-lang-codes --/shared-data/gtfs/*.txt> /shared-data/gtfs/data.sql'
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
is_valid_lang_code
is_timezone
calendar
processed 382 rows
calendar_dates
processed 6894 rows
service_days
agency
processed 1 rows
routes
processed 97 rows
trips
processed 11787 rows
frequencies
feed_info
processed 1 rows
shapes
processed 195972 rows
stops
processed 1987 rows
stop_times
processed 387239 rows
%%shelldocker network ls|grep notebooknet >/dev/null|| docker network create notebooknetdocker container ls|grep db >/dev/null&& docker stop dbecho "--> Launch DBMS"docker run --quiet --rm --name db -d \--env POSTGRES_USER=${DB_USERNAME:-dba} \--env POSTGRES_PASSWORD=${DB_PASSWORD:-secretsecret} \--env POSTGRES_DB=${DB_NAME:-notebook-db} \--volume dbhost-data:/var/lib/postgresql \--volume shared-data:/shared-data \--network=notebooknet \-p 5432:5432 \ postgis/postgis:17-masterdocker container exec \--env PGPASSWORD=secretsecret \ db \ sh -c 'until pg_isready -U dba -d notebook-db;do echo "$(date) - waiting for database to start"; sleep 1; done'
db
--> Launch DBMS
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
e1aae55d9ea96f2a45d27f6046c70c99c1257698cf78e410ce57d295c9518bbc
/var/run/postgresql:5432 - no response
Tue 10 Dec 2024 07:00:07 AM UTC - waiting for database to start
/var/run/postgresql:5432 - no response
Tue 10 Dec 2024 07:00:08 AM UTC - waiting for database to start
/var/run/postgresql:5432 - no response
Tue 10 Dec 2024 07:00:10 AM UTC - waiting for database to start
/var/run/postgresql:5432 - accepting connections
%%shell# Requête SQL pour trouver les arrêts "Université" avec leurs coordonnées GPSread -r -d '' QUERY << EOMSELECT stop_id,-- Identifiant unique de l'arrêt stop_name,-- Nom de l'arrêtST_X(ST_Transform(stop_loc::geometry,4326)) AS stop_latitude,-- Conversion des coordonnées en latitude(EPSG:4326= standard GPS)ST_Y(ST_Transform(stop_loc::geometry,4326)) AS stop_longitude -- Conversion des coordonnées en longitude(EPSG:4326= standard GPS)FROM stopsWHERE stop_id ILIKE 'GACAM%'LIMIT 3;EOMdocker run --rm \--env PGPASSWORD=secretsecret \--network=notebooknet \ postgres \ psql -h db -U dba gtfs -c $QUERY;
%%shell# Requête SQL pour trouver les arrêts "Université" avec leurs coordonnées GPSread -r -d '' QUERY << EOMSELECT route_id,-- Identifiant unique de la ligne route_short_name,-- Nom court de la ligne route_long_name -- Nom long de la ligneFROM routesWHERE route_long_name ILIKE '%Tln%'LIMIT 3;EOMdocker run --rm \--env PGPASSWORD=secretsecret \--network=notebooknet \ postgres \ psql -h db -U dba gtfs -c $QUERY;
route_id | route_short_name | route_long_name
----------+------------------+------------------------------
U | U | Tech.Mer/Pôle d'Act. Tln Est
0102 | 102 | Gare Rte Tln-Aéroport Hyères
0103 | 103 | Gare Rout. Tln-Moulin 1er
(3 rows)
%%shell# Requête pour trouver les 3 arrêts les plus proches de l'Université de Toulonread -r -d '' QUERY << EOM-- Recherche des arrêts par proximité géographiqueSELECT stop_id,-- Identifiant de l'arrêt stop_name,-- Nom de l'arrêt-- Calcul de la distance entre chaque arrêt et l'UniversitéROUND(ST_Distance( stop_loc::geography,--Position de l'arrêtST_SetSRID(-- Définit le système de coordonnéesST_MakePoint(-- Crée un point géographique6.016499,-- Longitude Université43.135440-- Latitude Université),4326-- En WGS84))) AS distance,-- Conversion des coordonnées en WGS84(format GPS standard)ST_X(-- Extrait la latitudeST_Transform(-- Convertit les coordonnées stop_loc::geometry,--Position de l'arrêt4326-- Code EPSG pour WGS84(standard GPS))) AS LATITUDE,ST_Y(ST_Transform(stop_loc::geometry,4326)) AS LONGITUDE -- Idem pour longitudeFROM stopsORDER BY distance ASC -- Tri du plus proche au plus éloignéLIMIT 5-- Retourne les 5 plus prochesEOMdocker run --rm \--env PGPASSWORD=secretsecret \--network=notebooknet \ postgres \ psql -h db -U dba gtfs -c $QUERY;
%%shell# Store a query in the QUERY shell variableread -r -d '' QUERY << EOMSELECT s.stop_id AS arret,-- Identifiant de l'arrêt s.stop_name AS nom_arret,-- Nom de l'arrêt r.route_short_name AS ligne,-- Numéro de ligne r.route_long_name AS nom_ligne,-- Nom de la ligne t.trip_headsign AS destination,--Destination du voyage st.departure_time AS depart -- Heure de départ FROM stop_times st JOIN stops s ON st.stop_id= s.stop_id-- Liaison avec les arrêts JOIN trips t ON st.trip_id= t.trip_id-- Liaison avec les voyages JOIN routes r ON t.route_id= r.route_id-- Liaison avec les lignesWHERE st.stop_id='VACAMN' AND r.route_short_name='U' AND st.departure_time>(NOW()::time)-- Départ après l'heure actuelleORDER BY st.departure_timeLIMIT 3;EOMdocker run --rm \--env PGPASSWORD=secretsecret \--network=notebooknet \ postgres \ psql -h db -U dba gtfs -c $QUERY;
arret | nom_arret | ligne | nom_ligne | destination | depart
--------+-----------+-------+------------------------------+----------------------+----------
VACAMN | Campus | U | Tech.Mer/Pôle d'Act. Tln Est | Technopole de la Mer | 07:05:00
VACAMN | Campus | U | Tech.Mer/Pôle d'Act. Tln Est | Technopole de la Mer | 07:10:00
VACAMN | Campus | U | Tech.Mer/Pôle d'Act. Tln Est | Technopole de la Mer | 07:10:00
(3 rows)
%%shell# Store a query in the QUERY shell variableread -r -d '' QUERY << EOMSELECT r.route_short_name AS ligne,-- Numéro de ligne st.arrival_time AS arrivee,-- Heure d'arrivée st.departure_time AS depart -- Heure de départ FROM stop_times stJOIN stops s ON st.stop_id= s.stop_id-- Liaison avec les arrêtsJOIN trips t ON st.trip_id= t.trip_id-- Liaison avec les voyagesJOIN routes r ON t.route_id= r.route_id-- Liaison avec les lignesWHERE st.stop_id='GAUNSS'LIMIT 3EOMdocker run --rm \--env PGPASSWORD=secretsecret \--network=notebooknet \ postgres \ psql -h db -U dba gtfs -c $QUERY;
Définissons tout d’abord simplement une classe Java pour représenter l’une des entités de l’exemple, un arrêt de bus. Nous utiliserons un record Java (non mutable).
/*** Représente un arrêt de transport public*/publicrecordStop(String id,// Identifiant uniqueString name,// Nom de l'arrêtDouble latitude,// LatitudeDouble longitude,// LongitudeString locationType // arrêt, station){public Stop {if(id ==null|| name ==null|| latitude ==null|| longitude ==null){thrownewIllegalArgumentException("Required fields cannot be null");}}}
/*** Représente une ligne de transport*/publicrecordRoute(String id,// Identifiant uniqueString shortName,// Numéro/Code courtString longName // Nom complet){public Route {if(id ==null|| shortName ==null){thrownewIllegalArgumentException("Required fields cannot be null");}}}
/*** Représente un voyage sur une ligne*/publicrecordTrip(String id,// Identifiant unique Route route,// ID de la ligneString tripHeadsign,// Destination affichéeString tripShortName // Nom court du voyage){public Trip {if(id ==null|| route ==null|| tripHeadsign ==null){thrownewIllegalArgumentException("Required fields cannot be null");}}}
importjava.time.LocalTime;/*** Représente un horaire de passage à un arrêt*/publicrecordStopTime( Trip trip,// ID du voyage Stop stop,// ID de l'arrêt LocalTime arrivalTime,// Heure d'arrivée LocalTime departureTime,// Heure de départString stopHeadsign // Destination affichée à l'arrêt){public StopTime {// Validation des champs obligatoiresif(trip ==null|| stop ==null|| arrivalTime ==null|| departureTime ==null){thrownewIllegalArgumentException("trip, stop, times are required");}// Validation de la cohérence des heuresif(departureTime.isBefore(arrivalTime)){thrownewIllegalArgumentException("Departure time cannot be before arrival time");}}}
importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.SQLException;finalString DB_URL="jdbc:postgresql://docker/gtfs";// Nom d'utilisateur et mot de passe de la base de données récupérés depuis les variables d'environnementfinalString DB_USERNAME=Optional.of(System.getenv("DB_USERNAME")).orElseThrow(()->newIllegalArgumentException("DB_USERNAME is required"));finalString DB_PASSWORD=Optional.of(System.getenv("DB_PASSWORD")).orElseThrow(()->newIllegalArgumentException("DB_PASSWORD is required"));try(Connection connection =DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)){// La connexion est établie, vous pouvez interagir avec la base de données ici}catch(SQLException e){// Gestion des exceptions liées à la base de données e.printStackTrace();}
Exécution de requêtes SQL
Pour exécuter des requêtes SQL en Java, on utilise des instances de la classe Statement. Il existe trois types de Statement :
Statement : Pour requêtes SQL simples et dynamiques.
PreparedStatement : Version précompilée pour requêtes récurrentes avec paramètres variables, améliorant les performances.
CallableStatement : Pour exécuter des procédures stockées, permettant l’appel de fonctions complexes.
Tous sont créés à partir d’une Connection.
Les requêtes simples
SELECT (Interrogation de données)
Utilisez la méthode executeQuery()
Parcours des résultats avec un objet ResultSet
Mise à jour (UPDATE, INSERT, DELETE)
Privilégiez la méthode executeUpdate()
Renvoie le nombre de lignes affectées
Type indéterminé ou Résultats Multiples
Optez pour la méthode execute()
Gestion plus flexible des résultats
Parcours des résultats
Utilisation de l’interface ResultSet
Méthode executeQuery() pour exécuter une requête SELECT
Récupération des données via des méthodes :
getXXX(), où XXX représente le type Java correspondant à la valeur à récupérer.
getBoolean, getLong, etc.
Les colonnes sont numérotées à partir de 1
Utiliser les noms de colonnes ou les index
// Create a list to store Stop objectsList<Stop> stops =newArrayList<>();finalString query =""" SELECT stop_id, stop_name, ST_X(ST_Transform(stop_loc::geometry, 4326)) AS LATITUDE, ST_Y(ST_Transform(stop_loc::geometry, 4326)) AS LONGITUDE, location_type FROM stops ORDER BY ST_Distance(stop_loc::geometry, ST_SetSRID(ST_MakePoint(5.940207, 43.121145), 4326)) ASC LIMIT 3""";try(Connection connection =DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);Statement stmt = connection.createStatement();){ResultSet rs = stmt.executeQuery(query);long id=0;while(rs.next()) stops.add(newStop(rs.getString("stop_id"), rs.getString("stop_name"), rs.getDouble("latitude"), rs.getDouble("longitude"), rs.getString("location_type")));}catch(SQLException e){ e.printStackTrace();}stops.forEach(System.out::println);
Stop[id=TOCMAS, name=Champ de Mars, latitude=5.940372, longitude=43.121363, locationType=stop]
Stop[id=TOCMAN, name=Champ de Mars, latitude=5.940497, longitude=43.12208, locationType=stop]
Stop[id=TOPIXO, name=Pie X, latitude=5.941181, longitude=43.120398, locationType=stop]
try(Connection connection =DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);Statement statement = connection.createStatement()){// SQL query to insert data into the 'agency' tableString sql =""" INSERT INTO agency(agency_id, agency_name, agency_url, agency_timezone, agency_lang, agency_phone, agency_fare_url, agency_email) VALUES(3, 'Neverland', 'http://nowhere.fr', 'Europe/Paris', 'fr', '555-555-555', 'http://nowhere.fr/fare', 'neverland@nowhere.org')""";// Execute the query and get the number of changesint numberOfChanges = statement.executeUpdate(sql);// Print the resultSystem.out.println("Nombre de changements: "+ numberOfChanges);}catch(SQLException e){ e.printStackTrace();}
Nombre de changements: 1
Les exceptions
Erreur dans le code SQL : SQLException
Avertissement lors de l’exécution (SQLWarning)
Problèmes de conversion de données (DataTruncation - sous-classe de SQLWarning)
String wrongQuery =" SELECT * FROM Employee";try(Connection connection =DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery(wrongQuery)){}catch(SQLException e){//Erreur lors de la requête log.severe(e.getMessage());}
CompilationException:
try (Connection connection = DriverManager.getConnection(DB_URL,DB_USERNAME,DB_PASSWORD);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(wrongQuery)) {
} catch (SQLException e) {
//Erreur lors de la requête
log.severe(e.getMessage());
}
using incubating module(s): jdk.incubator.vector
log.severe(e.getMessage());
cannot find symbol
symbol: variable log
ResultSet resultSet = statement.executeQuery(wrongQuery)) {
auto-closeable resource resultSet is never referenced in body of corresponding try statement
Types Java/JDBC et SQL
JDBC masque les différences de types entre les SGBD en définissant ses propres types SQL (constantes de la classe Types).
Conversion :
SQL vers Java lors de la lecture.
Java vers SQL lors du passage de paramètres.
Utilisation explicite des méthodes getXXX() et setXXX().
Choix multiples pour certains types :
CHAR et VARCHAR : getString().
LONGVARCHAR : getAsciiStream() ou getCharacterStream().
BINARY et VARBINARY : getBytes().
REAL : getFloat(), DOUBLE et FLOAT : getDouble().
DECIMAL et NUMERIC : getBigDecimal().
Types de date et d’heure : DATE (utilisez getDate()), TIME (utilisez getTime()), TIMESTAMP (utilisez getTimestamp()).
Transaction
Auto-commit par défaut :
Après chaque requête SQL de mise à jour (INSERT, UPDATE, DELETE), un commit est automatiquement effectué.
Les modifications sont validées dans la base de données.
Contrôle manuel des transactions :
Désactivez l’auto-commit avec conn.setAutoCommit(false).
Utilisez conn.commit() pour valider la transaction.
Pour annuler, utilisez conn.rollback().
Précompilation des requêtes
Si les requêtes fabriquées à partir de String changent (paramètres) :
Elles sont compilées à chaque appel d’où une perte de performances
JDBC permet de ne compiler la requête qu’une fois (si le SGBD le supporte)
En indiquant les paramètres de façon générique
En fixant leur valeur (sans changer la requête) au moment de l’exécution
Deux Statement particuliers :
Les requêtes paramétrées (PreparedStatement)
// Déclaration de la requête SQLString sql ="""SELECT stop_id, stop_name, ST_X(ST_Transform(stop_loc::geometry, 4326)) AS LATITUDE, ST_Y(ST_Transform(stop_loc::geometry, 4326)) AS LONGITUDE, location_type FROM stops WHERE stop_id = ? ORDER BY ST_Distance(stop_loc::geometry, ST_SetSRID(ST_MakePoint(5.940207, 43.121145), 4326)) ASCLIMIT 3""";try(Connection connection =DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);PreparedStatement pstmt = connection.prepareStatement(sql)){// Tableau contenant les identifiants à rechercherString[] ids ={"TOCMAS","TOCMAN","TOPIXO"};for(String id : ids){// Remplacer le paramètre de la requête par l'identifiant actuel pstmt.setString(1, id);// Exécution de la requêtetry(ResultSet rs = pstmt.executeQuery()){while(rs.next()){// Création et affichage d'un objet Stop avec les données récupéréesSystem.out.println(newStop(rs.getString("stop_id"), rs.getString("stop_name"), rs.getDouble("latitude"), rs.getDouble("longitude"), rs.getString("location_type")));}}}}catch(SQLException e){// Gestion des exceptions liées à la connexion ou à la requête e.printStackTrace();}
Stop[id=TOCMAS, name=Champ de Mars, latitude=5.940372, longitude=43.121363, locationType=stop]
Stop[id=TOCMAN, name=Champ de Mars, latitude=5.940497, longitude=43.12208, locationType=stop]
Stop[id=TOPIXO, name=Pie X, latitude=5.941181, longitude=43.120398, locationType=stop]
Les transactions
try(Connection connection =DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)){// Création de la table accountString createAccountTableSql ="CREATE TABLE account ("+"id SERIAL PRIMARY KEY,"+"name VARCHAR(100) NOT NULL,"+"balance DEC(15,2) NOT NULL)";try(Statement statement = connection.createStatement()){ statement.executeUpdate(createAccountTableSql);}// Création des comptes pour Alice et Bob avec une requête préparéeString createAccountSql ="INSERT INTO account(name, balance) VALUES(?, ?);";try(PreparedStatement pstmt = connection.prepareStatement(createAccountSql)){ pstmt.setString(1,"Bob"); pstmt.setInt(2,1000); pstmt.executeUpdate(); pstmt.setString(1,"Alice"); pstmt.setInt(2,1000); pstmt.executeUpdate();}// Préparation de la requête pour mettre à jour le solde des comptesString updateAccountSql ="UPDATE account SET balance = balance + ? WHERE id = ?";try(PreparedStatement pstmtIncreaseAccount = connection.prepareStatement(updateAccountSql)){// Sauvegarde de l'état auto-commitboolean autoCommit = connection.getAutoCommit();try{ connection.setAutoCommit(false);// Désactivation de l'auto-commit pour la transaction// Retirer 500€ du compte de Bob pstmtIncreaseAccount.setInt(1,-500); pstmtIncreaseAccount.setInt(2,1);// ID de Bob pstmtIncreaseAccount.executeUpdate();// Ajouter 500€ au compte d'Alice pstmtIncreaseAccount.setInt(1,500); pstmtIncreaseAccount.setInt(2,2);// ID d'Alice pstmtIncreaseAccount.executeUpdate(); connection.commit();// Valider la transaction}catch(SQLException exc){ connection.rollback();// Annuler la transaction en cas de problèmethrow exc;// Relancer l'exception pour gestion ultérieure}finally{ connection.setAutoCommit(autoCommit);// Rétablir l'état auto-commit}}// Suppression de la table accounttry(Statement statement = connection.createStatement()){ statement.executeUpdate("DROP TABLE account");}}catch(SQLException e){ e.printStackTrace();// Gestion des exceptions SQL}
Procédures stockées
Les procédures stockées sont des sous-programmes stockés dans la base de données qui peuvent être exécutés par un appel direct. Elles permettent d’encapsuler des opérations complexes et de les réutiliser facilement.
Avantages des procédures stockées :
Réutilisabilité : Les procédures stockées peuvent être appelées à partir de différentes applications.
Performance : Les procédures stockées sont précompilées et stockées dans la base de données, ce qui améliore les performances.
Inconvénients potentiels :
Portabilité : Les procédures stockées peuvent être spécifiques à un SGBD, ce qui peut limiter la portabilité du code.
Complexité : Les procédures stockées peuvent être difficiles à déboguer et à maintenir.
Avec JDBC CallableStatement permet d’appeler une procédure stockée directement sur le SGBD.
-- Création d'une procédure stockée pour transférer de l'argent entre comptesCREATEORREPLACEPROCEDURE transfer_funds( from_account INT, to_account INT, amount DECIMAL) AS $$BEGINUPDATEaccountSET balance = balance - amount WHEREid= from_account;UPDATEaccountSET balance = balance + amount WHEREid= to_account;END;$$ LANGUAGE plpgsql;
// Connexion à la base de donnéestry(Connection connection =DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)){// Préparation de l'appel à la procédure stockéeString callProcedureSql ="{CALL transfer_funds(?, ?, ?)}";try(CallableStatement callableStatement = connection.prepareCall(callProcedureSql)){// Définition des paramètres de la procédure callableStatement.setInt(1,1);// ID du compte source callableStatement.setInt(2,2);// ID du compte destination callableStatement.setBigDecimal(3,newBigDecimal("500.00"));// Montant à transférer// Exécution de la procédure stockée callableStatement.execute();}}catch(SQLException e){ e.printStackTrace();// Gestion des exceptions SQL}