Informatique · Java et bases de données

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.

OBJET JAVA Etudiant id = 1 nom = "Léa" JPA JDBC TABLE etudiants id │ nom 1 │ Léa
Une question pour plus tard : ton appli affiche 50 classes, avec le nombre d'élèves de chacune. Combien de requêtes SQL partent vers la base ? Garde ta réponse : on la vérifie dans la notion 6.

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.
Notion 1 · JDBC

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

LectureEtudiants.javaJava
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);
    }
  }
}
executeQuery()

Pour un SELECT : renvoie un ResultSet, les lignes du résultat.

executeUpdate()

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.

Retiens le curseur : il démarre avant la première ligne, et c'est rs.next() qui l'avance. Quand next() renvoie false, il n'y a plus rien à lire.
Exercice · à toi

Remets la lecture dans l'ordre

Cette méthode lit les noms des étudiants d'une classe. Ses lignes ont été mélangées : touche une ligne, puis celle avec laquelle l'échanger.

Notion 2 · Requêtes préparées

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.

SAISIE REQUÊTE ENVOYÉE LIGNES RENVOYÉES Léa3M1 Noé3M1 Zoé3M2
Étape 1 sur 5 · le code fautif

Le programme colle la saisie dans la requête : "… WHERE nom = '" + saisie + "'".

Étape 2 sur 5 · une saisie normale

Avec Léa, la requête est correcte et renvoie une ligne.

Étape 3 sur 5 · une saisie piégée

Avec x' OR '1'='1, l'apostrophe ferme la chaîne et la suite devient du SQL : la condition est toujours vraie, toutes les lignes sortent.

Étape 4 sur 5 · la requête préparée

Le SQL part avec un ?, la saisie part à part, comme une simple valeur. Aucun étudiant ne s'appelle x' OR '1'='1 : zéro ligne.

Étape 5 sur 5 · la limite

Un ? ne remplace qu'une valeur : pas un nom de table, de colonne, ni ASC ou DESC. Pour un tri choisi par l'utilisateur, on compare sa saisie à une liste de colonnes autorisées.

Testé pour de vraiSur une table de trois étudiants, la requête concaténée avec 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).
Jeu · sûr ou vulnérable ?

Quatre requêtes à juger

Pour chaque extrait, dis si un utilisateur malveillant peut modifier le SQL exécuté.

Notion 3 · Transactions

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.

Sans transaction, 50 CHF sont apparus de nulle part : le crédit était déjà validé quand le débit a échoué. Avec la transaction, le rollback efface tout.
Virement.javaJava
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;
}
Exercice · à toi

Commit ou rollback ?

Trois questions sur l'auto-commit et les transactions.

Notion 4 · Jakarta Persistence

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.

@Entity @Table(name = "etudiants") public class Etudiant { @Id @GeneratedValue private Long id; @Column(nullable = false, length = 50) private String nom; @ManyToOneprivate Classe classe; TABLE GÉNÉRÉE etudiants idbigint, clé primaire nomvarchar(50) not null classe_idclé étrangère → Classe
Étape 1 sur 4 · la classe

@Entity fait de la classe une entité ; @Table choisit le nom de sa table, ici etudiants.

Étape 2 sur 4 · l'identifiant

@Id désigne la clé primaire ; @GeneratedValue laisse l'implémentation la générer.

Étape 3 sur 4 · une colonne

@Column règle la colonne : obligatoire (not null) et de 50 caractères au plus.

Étape 4 sur 4 · une relation

@ManyToOne : plusieurs étudiants par classe. Dans la table, cela devient une colonne classe_id, clé étrangère vers la table des classes.

Vérifié avec Hibernate 7.4À partir de cette classe, Hibernate a créé 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.
Notion 5 · EntityManager et cycle de vie

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

Nouvelle

Créée par new : pas d'identité persistante, aucun contexte ne la connaît.

Gérée

Elle a une identité et appartient au contexte de persistance en cours.

Détachée

Elle a une identité, mais plus aucun contexte ne la gère (par exemple après close()).

Supprimée

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.

Le setNom suivi d'un commit a suffi à produire un UPDATE : pas besoin d'appeler persist sur une entité déjà gérée. La spécification dit d'ailleurs que persist l'ignore.

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.

Exercice · à toi

Dans quel état est l'entité ?

Touche une situation, puis l'état dans lequel elle laisse l'entité.

Notion 6 · Relations et chargement

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

Classe.javaJava
@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.

Réponse à ma question du début : 51 requêtes pour 50 classes, une pour la liste, puis une par classe. Avec un JOIN FETCH, une seule.
N+1 et JOIN FETCHJava
// 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();
Mesuré avec Hibernate 7.4Avec quatre classes en base, la boucle naïve a préparé 5 requêtes (1 + 4), la version JOIN FETCH une seule. Le guide de Hibernate conseille de charger d'emblée les données utiles, presque toujours par une jointure (JOIN FETCH ou graphe d'entités).6
Exercice · à toi

Combien de requêtes ?

Trois questions sur le chargement des relations.

Notion 7 · JPQL

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

Recherche.javaJava
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 ?.

Exercice · à toi

Quelle requête JPQL ?

Choisis la requête correcte pour chaque besoin.

Attention

Les pièges classiques

Oublier de fermer

Connexions, requêtes et ResultSet occupent des ressources de la base de données : déclare-les dans un try-with-resources.

Concaténer du SQL… ou du JPQL

Une requête JPQL construite en collant une saisie est tout aussi vulnérable. Paramètres ? ou :nom, toujours.

rs.getString(0)

Les colonnes d'un ResultSet sont numérotées à partir de 1. Lire la colonne 0 lève une SQLException.

Rester en auto-commit pour des requêtes liées

Si la deuxième échoue, la première est déjà validée : les données deviennent incohérentes.

Modifier une entité détachée

Après close(), plus rien n'est suivi : sans merge, la modification reste dans la mémoire du programme.

Charger les relations dans une boucle

Une requête par tour : c'est le problème N+1. Charge ce dont tu as besoin d'avance, avec un JOIN FETCH.

Entraîne-toi

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).

À retenir

La fiche en huit lignes

JDBCConnection, PreparedStatement, ResultSet ; un pilote par base ; un DataSource de préférence.
ResultSetcurseur avant la première ligne, rs.next() pour avancer, colonnes numérotées dès 1.
Ressourcestry-with-resources : fermeture garantie, dans l'ordre inverse de création.
Injectionjamais de concaténation ; ? ou :nom pour les valeurs, liste blanche pour les noms de colonnes.
Transactionsauto-commit par défaut ; setAutoCommit(false), puis commit ou rollback.
Entités@Entity, @Id, @Column ; constructeur sans paramètre ; Jakarta Persistence 3.2, Hibernate 7.
Cycle de vienouvelle, gérée, détachée, supprimée ; une entité gérée modifiée est écrite au commit.
Relationsle côté @ManyToOne porte la clé étrangère ; LAZY partout ; JOIN FETCH contre le N+1.
Dernière étape : le quiz. Si tu hésites, repense aux scènes : le curseur, le virement, le jeton de Léa.
Quiz final

Teste-toi

Sources

  1. 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
  2. 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
  3. PostgreSQL JDBC Driver, Connecting to the Database (formats d'URL jdbc:postgresql, hôte localhost et port 5432 par défaut). jdbc.postgresql.org
  4. 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
  5. 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
  6. 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.

Glisser pour continuer vers Design patterns
Bachelor Informatique