JDBC et JPA
Ton programme Java manipule des objets ; ta base de données range des lignes dans des tables. Deux outils font le pont : JDBC, l'API standard de Java qui envoie du SQL, et Jakarta Persistence (JPA), qui fait correspondre objets et tables et écrit le SQL à ta place.
Après cette fiche, tu sais
- ouvrir une connexion JDBC, exécuter une requête préparée et lire un ResultSet, en fermant tout avec try-with-resources ;
- expliquer pourquoi une requête préparée bloque l'injection SQL, et où elle ne suffit pas ;
- regrouper des requêtes dans une transaction, avec commit et rollback ;
- mapper une entité JPA et suivre son cycle de vie : nouvelle, gérée, détachée, supprimée ;
- reconnaître le problème N+1 et l'éviter avec un JOIN FETCH.
Envoyer du SQL depuis Java
JDBC est un ensemble d'interfaces du module java.sql : Connection, PreparedStatement, ResultSet… Chaque système de base de données fournit un pilote qui les implémente. Ton code ne parle qu'à JDBC ; changer de base revient surtout à changer de pilote et d'adresse.
L'adresse est une URL JDBC de la forme jdbc:sous-protocole:nom. Pour PostgreSQL : jdbc:postgresql://localhost/ecole, le port par défaut étant 5432.3 DriverManager.getConnection(url, user, mdp) ouvre une connexion ; la Javadoc indique qu'un DataSource est le moyen préféré de se connecter.1
String url =
"jdbc:postgresql://localhost/ecole";
String sql = "SELECT id, nom"
+ " FROM etudiants WHERE classe = ?";
try (Connection conn = DriverManager
.getConnection(url, user, mdp);
PreparedStatement ps =
conn.prepareStatement(sql)) {
ps.setString(1, "3M1");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String nom = rs.getString("nom");
System.out.println(id + " " + nom);
}
}
}
Pour un SELECT : renvoie un ResultSet, les lignes du résultat.
Pour INSERT, UPDATE, DELETE : renvoie un int, le nombre de lignes touchées.
Le try (…) est un try-with-resources : chaque ressource déclarée entre ses parenthèses est fermée à la fin du bloc, même si une exception survient, dans l'ordre inverse de sa création.2 Sans fermeture, une connexion garde ses ressources de base de données au lieu de les libérer tout de suite.1 Fais défiler pour suivre ce code pas à pas.
Ne jamais coller une saisie dans du SQL
Construire une requête en collant une saisie de l'utilisateur, c'est le laisser écrire une partie de ton SQL : c'est l'injection SQL. Une requête préparée envoie le code SQL, avec ses ?, séparément des valeurs : la base distingue toujours le code des données, quoi qu'ait tapé l'utilisateur.4 Fais défiler.
x' OR '1'='1 a renvoyé 3 lignes ; la requête préparée avec la même saisie, 0 ligne (Java 21, base H2).Tout ou rien
Par défaut, une nouvelle connexion est en auto-commit : chaque requête est exécutée et validée comme une transaction à elle seule. Avec setAutoCommit(false), les requêtes sont regroupées dans une transaction qui se termine par commit(), qui rend tous les changements définitifs, ou par rollback(), qui les annule tous.1 Fais défiler : un virement de 50 CHF qui échoue, d'abord sans transaction, puis avec.
conn.setAutoCommit(false);
try (PreparedStatement credit =
conn.prepareStatement(CREDIT);
PreparedStatement debit =
conn.prepareStatement(DEBIT)) {
credit.setInt(1, 50);
credit.setInt(2, idLea);
credit.executeUpdate();
debit.setInt(1, 50);
debit.setInt(2, idNoe);
debit.executeUpdate();
conn.commit(); // les deux, ou…
} catch (SQLException e) {
conn.rollback(); // … aucun
throw e;
}
Des objets qui deviennent des lignes
Avec JDBC, tu écris le SQL et tu recopies chaque colonne dans un objet. Un ORM automatise ce travail. En Java, la norme s'appelle Jakarta Persistence (anciennement Java Persistence API, d'où le sigle JPA). Sa version 3.2, publiée le 20 mai 2024 et intégrée à Jakarta EE 11, demande Java 17 au moins ; ses implémentations les plus connues sont Hibernate ORM et EclipseLink.5 Hibernate 7 implémente cette version 3.2.6
Une classe annotée @Entity devient une entité : chaque instance correspond à une ligne. La spécification exige un constructeur sans paramètre, public ou protégé, que l'implémentation appelle pour créer les objets.5 Fais défiler pour voir ce que devient chaque annotation.
etudiants (classe_id bigint, id bigint not null, nom varchar(50) not null, primary key (id)), puis la contrainte de clé étrangère vers Classe.Nouvelle, gérée, détachée, supprimée
On manipule les entités par un EntityManager, qui tient un contexte de persistance : l'ensemble des entités qu'il gère. La spécification distingue quatre états.5
Créée par new : pas d'identité persistante, aucun contexte ne la connaît.
Elle a une identité et appartient au contexte de persistance en cours.
Elle a une identité, mais plus aucun contexte ne la gère (par exemple après close()).
Gérée, mais promise à la suppression au commit de la transaction.
Point clé : l'état des entités gérées est synchronisé avec la base au commit de la transaction.5 Hibernate appelle cela la vérification automatique des modifications (automatic dirty checking) : après avoir modifié une entité gérée, aucune opération explicite n'est nécessaire.6 Fais défiler.
Une entité détachée ne suit plus rien : la modifier ne change pas la base. Pour enregistrer ses modifications, on appelle em.merge(e), qui recopie son état sur une instance gérée de même identité, et c'est cette instance gérée que renvoie merge.5 Enfin, em.find(Etudiant.class, id) renvoie null si la ligne n'existe pas.
Le piège des requêtes en cascade
Une relation bidirectionnelle a un côté propriétaire : celui dont la table porte la clé étrangère. Pour un couple @ManyToOne / @OneToMany, le côté « plusieurs » doit être propriétaire, et le côté inverse le désigne avec mappedBy.5
@Entity
public class Classe {
@Id @GeneratedValue
private Long id;
private String nom;
@OneToMany(mappedBy = "classe")
private List<Etudiant> etudiants =
new ArrayList<>();
}
Par défaut, @ManyToOne se charge tout de suite (EAGER) et @OneToMany à la demande (LAZY, chargement paresseux).5 Le guide de Hibernate qualifie le chargement immédiat des @ManyToOne de « défaut très malheureux » de JPA et recommande de rendre presque toutes les associations paresseuses (fetch = FetchType.LAZY).6 Mais le paresseux a son propre piège, le problème N+1. Fais défiler.
// N+1 : 1 requête, puis 1 par classe
List<Classe> classes = em
.createQuery("SELECT c FROM Classe c",
Classe.class)
.getResultList();
for (Classe c : classes)
c.getEtudiants().size(); // SELECT !
// 1 seule requête, avec une jointure
List<Classe> toutes = em
.createQuery("SELECT DISTINCT c"
+ " FROM Classe c"
+ " LEFT JOIN FETCH c.etudiants",
Classe.class)
.getResultList();
Interroger des entités, pas des tables
JPQL (Jakarta Persistence Query Language) ressemble à SQL, mais il parle d'entités et d'attributs : Etudiant et e.classe.nom, pas etudiants et classe_id. Les paramètres nommés commencent par deux points (:nom) et, comme les ? de JDBC, ils protègent de l'injection.5
List<Etudiant> r = em
.createQuery("SELECT e FROM Etudiant e"
+ " WHERE e.classe.nom = :nom"
+ " ORDER BY e.nom", Etudiant.class)
.setParameter("nom", "3M3")
.getResultList();
select e1_0.id, e1_0.classe_id, e1_0.nom from etudiants e1_0 join Classe c1_0 on c1_0.id = e1_0.classe_id where c1_0.nom = ? order by e1_0.nom
La sortie ci-dessus est le SQL réellement produit par Hibernate 7.4, mis en forme : la navigation e.classe.nom est devenue une jointure, et le paramètre un ?.
Les pièges classiques
Connexions, requêtes et ResultSet occupent des ressources de la base de données : déclare-les dans un try-with-resources.
Une requête JPQL construite en collant une saisie est tout aussi vulnérable. Paramètres ? ou :nom, toujours.
Les colonnes d'un ResultSet sont numérotées à partir de 1. Lire la colonne 0 lève une SQLException.
Si la deuxième échoue, la première est déjà validée : les données deviennent incohérentes.
Après close(), plus rien n'est suivi : sans merge, la modification reste dans la mémoire du programme.
Une requête par tour : c'est le problème N+1. Charge ce dont tu as besoin d'avance, avec un JOIN FETCH.
Exercices corrigés
Cherche d'abord, puis ouvre le corrigé.
1Écris une méthode JDBC qui renvoie le nombre d'étudiants d'une classe donnée.Voir le corrigé
long compter(Connection conn,
String classe)
throws SQLException {
String sql = "SELECT COUNT(*)"
+ " FROM etudiants WHERE classe = ?";
try (PreparedStatement ps =
conn.prepareStatement(sql)) {
ps.setString(1, classe);
try (ResultSet rs =
ps.executeQuery()) {
rs.next();
return rs.getLong(1);
}
}
}
COUNT(*) renvoie toujours une seule ligne : un seul rs.next(), puis la colonne 1. La connexion est fournie par l'appelant, qui la fermera.
2Ce code est vulnérable : st.executeQuery("SELECT * FROM etudiants WHERE classe = '" + c + "'"). Corrige-le.Voir le corrigé
String sql = "SELECT * FROM etudiants"
+ " WHERE classe = ?";
try (PreparedStatement ps =
conn.prepareStatement(sql)) {
ps.setString(1, c);
try (ResultSet rs = ps.executeQuery()) {
// …
}
}
La saisie c devient une valeur transmise à part : elle ne peut plus modifier la structure de la requête.
3Quelles requêtes SQL ce code envoie-t-il ? tx.begin(); Etudiant e = em.find(Etudiant.class, 1L); e.setNom("Léa B."); tx.commit();Voir le corrigé
Un SELECT, puis un UPDATE. find lit la ligne ; l'entité est gérée, donc sa modification est détectée et écrite au commit. Avec Hibernate 7.4, la console a affiché select … from etudiants e1_0 where e1_0.id=? puis update etudiants set classe_id=?, nom=? where id=?.
4Une page affiche 30 classes et leurs étudiants, et envoie 31 requêtes. Pourquoi, et comment n'en envoyer qu'une ?Voir le corrigé
C'est le problème N+1 : une requête pour les 30 classes, puis une par classe quand le code parcourt la liste paresseuse getEtudiants(). Solution : SELECT DISTINCT c FROM Classe c LEFT JOIN FETCH c.etudiants, qui charge tout en une jointure (le LEFT garde aussi les classes sans étudiant).
La fiche en huit lignes
Teste-toi
Sources
- Oracle, Java SE 25 & JDK 25 API, module java.sql, pages Connection (auto-commit par défaut, commit, rollback, close), DriverManager (DataSource, moyen préféré de se connecter) et ResultSet (curseur avant la première ligne, colonnes numérotées dès 1). docs.oracle.com
- Oracle, The Java Tutorials : The try-with-resources Statement (ressources fermées à la fin de l'instruction, dans l'ordre inverse de leur création ; exemple JDBC). docs.oracle.com
- PostgreSQL JDBC Driver, Connecting to the Database (formats d'URL jdbc:postgresql, hôte localhost et port 5432 par défaut). jdbc.postgresql.org
- OWASP, SQL Injection Prevention Cheat Sheet (requêtes préparées : la base distingue le code des données ; exemple Java ; noms de tables, de colonnes et ASC/DESC impossibles en paramètres, liste blanche). owasp.org
- Eclipse Foundation, Jakarta Persistence 3.2, 20 mai 2024, et sa spécification (constructeur sans paramètre, quatre états d'une entité, persist ignoré sur une entité gérée, synchronisation au commit, merge, côté propriétaire et mappedBy, EAGER et LAZY par défaut, paramètres nommés, JOIN FETCH). jakarta.ee
- Hibernate, An Introduction to Hibernate 7, version 7.4 (prise en charge de JPA 3.2, vérification automatique des modifications, chargement des @ManyToOne à rendre paresseux, stratégies contre le N+1). docs.hibernate.org
Fiche écrite et sourcée en octobre 2026. Le code JDBC et JPA a été exécuté avec Java 21, Hibernate ORM 7.4.11 et la base H2 ; les comptes de requêtes et le SQL affiché viennent de ces exécutions.