Support de cours adapté à PostgreSQL avec : une version Java pur + JDBC + Maven + DAO + Singleton une version Hibernate sans Spring Boot + Maven avec les patterns utiles Exemple métier utilisé : Pilote
Support de cours adapté à PostgreSQL avec :
Exemple métier utilisé : Pilote
À la fin de ce support, vous saurez :
pilote-jdbc-postgresql/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── fr/bouget/pilote/ │ │ │ ├── App.java │ │ │ ├── dao/ │ │ │ │ ├── PiloteDao.java │ │ │ │ └── PiloteDaoJdbc.java │ │ │ ├── model/ │ │ │ │ └── Pilote.java │ │ │ └── util/ │ │ │ └── ConnectionSingleton.java │ │ └── resources/ │ │ └── schema.sql │ └── test/ │ └── java/ └── README.md
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>fr.bouget</groupId> <artifactId>pilote-jdbc-postgresql</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Driver JDBC PostgreSQL --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.8</version> </dependency> </dependencies> </project>
Le driver PostgreSQL permet à Java de dialoguer avec PostgreSQL via JDBC.
PostgreSQL n’utilise pas AUTO_INCREMENT comme MySQL. En PostgreSQL moderne, on utilise généralement GENERATED ALWAYS AS IDENTITY.
AUTO_INCREMENT
CREATE TABLE IF NOT EXISTS pilote ( id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, nom VARCHAR(50) NOT NULL, site VARCHAR(50) NOT NULL ); INSERT INTO pilote (nom, site) VALUES ('SERGE', 'NICE'), ('JEAN', 'PARIS'), ('CLAUDINE', 'GRENOBLE'), ('ROBERT', 'NANTES'), ('MICHEL', 'PARIS'), ('LUCIENNE', 'TOULOUSE'), ('BERTRAND', 'LYON'), ('HERVE', 'BASTIA'), ('LUC', 'PARIS'), ('GASPARD', 'PARIS'), ('ELODIE', 'BREST');
Pilote
package fr.bouget.pilote.model; public class Pilote { private Integer id; private String nom; private String site; public Pilote() { } public Pilote(Integer id, String nom, String site) { this.id = id; this.nom = nom != null ? nom.toUpperCase() : null; this.site = site != null ? site.toUpperCase() : null; } public Pilote(String nom, String site) { this(null, nom, site); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom != null ? nom.toUpperCase() : null; } public String getSite() { return site; } public void setSite(String site) { this.site = site != null ? site.toUpperCase() : null; } @Override public String toString() { return "Pilote{id=" + id + ", nom='" + nom + "', site='" + site + "'}"; } }
L’idée est de centraliser la configuration de connexion dans une seule classe.
Attention : dans une vraie application multi-utilisateur ou complexe, on évite souvent de conserver une seule connexion partagée trop longtemps. Le plus propre consiste généralement à centraliser la configuration, puis ouvrir/fermer proprement les connexions selon le besoin.
Ici, pour un support pédagogique simple, le Singleton est pratique.
package fr.bouget.pilote.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionSingleton { private static final String URL = "jdbc:postgresql://localhost:5432/bd_avion"; private static final String USER = "test"; private static final String PASSWORD = "test"; private static ConnectionSingleton instance; private Connection connection; private ConnectionSingleton() { try { this.connection = DriverManager.getConnection(URL, USER, PASSWORD); } catch (SQLException e) { throw new RuntimeException("Impossible de créer la connexion PostgreSQL", e); } } public static synchronized ConnectionSingleton getInstance() { if (instance == null) { instance = new ConnectionSingleton(); } return instance; } public Connection getConnection() { try { if (connection == null || connection.isClosed()) { connection = DriverManager.getConnection(URL, USER, PASSWORD); } } catch (SQLException e) { throw new RuntimeException("Impossible de récupérer la connexion PostgreSQL", e); } return connection; } }
Le pattern DAO (Data Access Object) permet d’isoler les accès à la base de données du reste du programme.
package fr.bouget.pilote.dao; import fr.bouget.pilote.model.Pilote; import java.util.List; import java.util.Optional; public interface PiloteDao { Optional<Pilote> findById(int id); Optional<Pilote> findByNom(String nom); List<Pilote> findAll(); int addPilote(Pilote pilote); int updatePilote(Pilote pilote); int removePilote(int id); long count(); }
Optional<Pilote>
Parce qu’un pilote recherché peut ne pas exister. Cela évite les null qui se promènent partout comme des valises perdues à Roissy.
null
package fr.bouget.pilote.dao; import fr.bouget.pilote.model.Pilote; import fr.bouget.pilote.util.ConnectionSingleton; import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class PiloteDaoJdbc implements PiloteDao { private static final String SELECT_BY_ID = "SELECT id, nom, site FROM pilote WHERE id = ?"; private static final String SELECT_BY_NOM = "SELECT id, nom, site FROM pilote WHERE nom = ?"; private static final String SELECT_ALL = "SELECT id, nom, site FROM pilote ORDER BY id"; private static final String INSERT = "INSERT INTO pilote(nom, site) VALUES (?, ?)"; private static final String UPDATE = "UPDATE pilote SET nom = ?, site = ? WHERE id = ?"; private static final String DELETE = "DELETE FROM pilote WHERE id = ?"; private static final String COUNT = "SELECT COUNT(*) FROM pilote"; private final Connection connection; public PiloteDaoJdbc() { this.connection = ConnectionSingleton.getInstance().getConnection(); } @Override public Optional<Pilote> findById(int id) { try (PreparedStatement ps = connection.prepareStatement(SELECT_BY_ID)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Pilote pilote = mapRow(rs); return Optional.of(pilote); } } } catch (SQLException e) { throw new RuntimeException("Erreur lors de findById()", e); } return Optional.empty(); } @Override public Optional<Pilote> findByNom(String nom) { try (PreparedStatement ps = connection.prepareStatement(SELECT_BY_NOM)) { ps.setString(1, nom.toUpperCase()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Pilote pilote = mapRow(rs); return Optional.of(pilote); } } } catch (SQLException e) { throw new RuntimeException("Erreur lors de findByNom()", e); } return Optional.empty(); } @Override public List<Pilote> findAll() { List<Pilote> pilotes = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(SELECT_ALL); ResultSet rs = ps.executeQuery()) { while (rs.next()) { pilotes.add(mapRow(rs)); } } catch (SQLException e) { throw new RuntimeException("Erreur lors de findAll()", e); } return pilotes; } @Override public int addPilote(Pilote pilote) { try (PreparedStatement ps = connection.prepareStatement(INSERT, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, pilote.getNom()); ps.setString(2, pilote.getSite()); int lignesModifiees = ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { pilote.setId(keys.getInt(1)); } } return lignesModifiees; } catch (SQLException e) { throw new RuntimeException("Erreur lors de addPilote()", e); } } @Override public int updatePilote(Pilote pilote) { try (PreparedStatement ps = connection.prepareStatement(UPDATE)) { ps.setString(1, pilote.getNom()); ps.setString(2, pilote.getSite()); ps.setInt(3, pilote.getId()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("Erreur lors de updatePilote()", e); } } @Override public int removePilote(int id) { try (PreparedStatement ps = connection.prepareStatement(DELETE)) { ps.setInt(1, id); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("Erreur lors de removePilote()", e); } } @Override public long count() { try (PreparedStatement ps = connection.prepareStatement(COUNT); ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getLong(1); } } catch (SQLException e) { throw new RuntimeException("Erreur lors de count()", e); } return 0; } private Pilote mapRow(ResultSet rs) throws SQLException { return new Pilote( rs.getInt("id"), rs.getString("nom"), rs.getString("site") ); } }
App.java
package fr.bouget.pilote; import fr.bouget.pilote.dao.PiloteDao; import fr.bouget.pilote.dao.PiloteDaoJdbc; import fr.bouget.pilote.model.Pilote; public class App { public static void main(String[] args) { PiloteDao piloteDao = new PiloteDaoJdbc(); System.out.println("=== Liste des pilotes ==="); piloteDao.findAll().forEach(System.out::println); System.out.println("\nNombre de pilotes : " + piloteDao.count()); Pilote nouveau = new Pilote("Philippe", "Paris"); piloteDao.addPilote(nouveau); System.out.println("\nPilote ajouté : " + nouveau); piloteDao.findByNom("PHILIPPE") .ifPresent(p -> System.out.println("\nTrouvé par nom : " + p)); nouveau.setSite("Lyon"); piloteDao.updatePilote(nouveau); System.out.println("\nAprès mise à jour : " + piloteDao.findById(nouveau.getId()).orElse(null)); piloteDao.removePilote(nouveau.getId()); System.out.println("\nAprès suppression, nombre de pilotes : " + piloteDao.count()); } }
Elle permet de voir clairement :
PreparedStatement
ResultSet -> objet Java
Le code JDBC devient vite répétitif :
C’est justement ce qui motive l’usage d’un ORM comme Hibernate.
Avec Hibernate, les patterns les plus utiles dans un projet simple sont :
SessionFactory
Session
Autrement dit, Hibernate n’est pas juste un outil : c’est aussi un festival de patterns bien habillés.
pilote-hibernate-postgresql/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── fr/bouget/pilote/ │ │ │ ├── App.java │ │ │ ├── dao/ │ │ │ │ ├── PiloteDao.java │ │ │ │ └── PiloteDaoHibernate.java │ │ │ ├── model/ │ │ │ │ └── Pilote.java │ │ │ └── util/ │ │ │ └── HibernateUtil.java │ │ └── resources/ │ │ └── hibernate.cfg.xml │ └── test/ │ └── java/ └── README.md
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>fr.bouget</groupId> <artifactId>pilote-hibernate-postgresql</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Hibernate ORM --> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>6.6.14.Final</version> </dependency> <!-- Driver PostgreSQL --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.8</version> </dependency> <!-- API Jakarta Persistence --> <dependency> <groupId>jakarta.persistence</groupId> <artifactId>jakarta.persistence-api</artifactId> <version>3.2.0</version> </dependency> <!-- Logs simples pour voir ce qui se passe --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.17</version> </dependency> </dependencies> </project>
hibernate.cfg.xml
Placez ce fichier dans src/main/resources et non dans src/main/java.
src/main/java
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "https://hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Connexion PostgreSQL --> <property name="hibernate.connection.driver_class">org.postgresql.Driver</property> <property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/bd_avion</property> <property name="hibernate.connection.username">test</property> <property name="hibernate.connection.password">test</property> <!-- Dialecte PostgreSQL --> <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property> <!-- Affichage SQL --> <property name="hibernate.show_sql">true</property> <property name="hibernate.format_sql">true</property> <property name="hibernate.highlight_sql">true</property> <!-- Contexte de session --> <property name="hibernate.current_session_context_class">thread</property> <!-- Stratégie de génération de schéma --> <property name="hibernate.hbm2ddl.auto">update</property> <!-- Mapping --> <mapping class="fr.bouget.pilote.model.Pilote"/> </session-factory> </hibernate-configuration>
jakarta.persistence.*
javax.persistence.*
src/main/resources
hbm2ddl.auto=update
package fr.bouget.pilote.model; import jakarta.persistence.*; @Entity @Table(name = "pilote") public class Pilote { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(name = "nom", nullable = false, length = 50) private String nom; @Column(name = "site", nullable = false, length = 50) private String site; public Pilote() { } public Pilote(Integer id, String nom, String site) { this.id = id; this.nom = nom != null ? nom.toUpperCase() : null; this.site = site != null ? site.toUpperCase() : null; } public Pilote(String nom, String site) { this(null, nom, site); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom != null ? nom.toUpperCase() : null; } public String getSite() { return site; } public void setSite(String site) { this.site = site != null ? site.toUpperCase() : null; } @Override public String toString() { return "Pilote{id=" + id + ", nom='" + nom + "', site='" + site + "'}"; } }
HibernateUtil
package fr.bouget.pilote.util; import org.hibernate.SessionFactory; import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; public class HibernateUtil { private static final SessionFactory sessionFactory = buildSessionFactory(); private HibernateUtil() { } private static SessionFactory buildSessionFactory() { try { StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .configure("hibernate.cfg.xml") .build(); Metadata metadata = new MetadataSources(registry) .getMetadataBuilder() .build(); return metadata.getSessionFactoryBuilder().build(); } catch (Exception e) { throw new ExceptionInInitializerError("Erreur d'initialisation de la SessionFactory : " + e.getMessage()); } } public static SessionFactory getSessionFactory() { return sessionFactory; } public static void shutdown() { getSessionFactory().close(); } }
Une SessionFactory coûte cher à construire. On l’instancie donc une fois, pas toutes les trente secondes !
On peut garder exactement la même interface que dans la version JDBC.
package fr.bouget.pilote.dao; import fr.bouget.pilote.model.Pilote; import fr.bouget.pilote.util.HibernateUtil; import org.hibernate.Session; import org.hibernate.Transaction; import java.util.List; import java.util.Optional; public class PiloteDaoHibernate implements PiloteDao { @Override public Optional<Pilote> findById(int id) { try (Session session = HibernateUtil.getSessionFactory().openSession()) { Pilote pilote = session.get(Pilote.class, id); return Optional.ofNullable(pilote); } } @Override public Optional<Pilote> findByNom(String nom) { try (Session session = HibernateUtil.getSessionFactory().openSession()) { String hql = "from Pilote p where p.nom = :nom"; Pilote pilote = session.createQuery(hql, Pilote.class) .setParameter("nom", nom.toUpperCase()) .uniqueResult(); return Optional.ofNullable(pilote); } } @Override public List<Pilote> findAll() { try (Session session = HibernateUtil.getSessionFactory().openSession()) { String hql = "from Pilote p order by p.id"; return session.createQuery(hql, Pilote.class).list(); } } @Override public int addPilote(Pilote pilote) { Transaction tx = null; try (Session session = HibernateUtil.getSessionFactory().openSession()) { tx = session.beginTransaction(); session.persist(pilote); tx.commit(); return 1; } catch (Exception e) { if (tx != null) tx.rollback(); throw new RuntimeException("Erreur lors de addPilote()", e); } } @Override public int updatePilote(Pilote pilote) { Transaction tx = null; try (Session session = HibernateUtil.getSessionFactory().openSession()) { tx = session.beginTransaction(); session.merge(pilote); tx.commit(); return 1; } catch (Exception e) { if (tx != null) tx.rollback(); throw new RuntimeException("Erreur lors de updatePilote()", e); } } @Override public int removePilote(int id) { Transaction tx = null; try (Session session = HibernateUtil.getSessionFactory().openSession()) { tx = session.beginTransaction(); Pilote pilote = session.get(Pilote.class, id); if (pilote != null) { session.remove(pilote); tx.commit(); return 1; } tx.commit(); return 0; } catch (Exception e) { if (tx != null) tx.rollback(); throw new RuntimeException("Erreur lors de removePilote()", e); } } @Override public long count() { try (Session session = HibernateUtil.getSessionFactory().openSession()) { String hql = "select count(p) from Pilote p"; return session.createQuery(hql, Long.class).uniqueResult(); } } }
package fr.bouget.pilote; import fr.bouget.pilote.dao.PiloteDao; import fr.bouget.pilote.dao.PiloteDaoHibernate; import fr.bouget.pilote.model.Pilote; import fr.bouget.pilote.util.HibernateUtil; public class App { public static void main(String[] args) { PiloteDao piloteDao = new PiloteDaoHibernate(); System.out.println("=== Liste des pilotes ==="); piloteDao.findAll().forEach(System.out::println); System.out.println("\nNombre de pilotes : " + piloteDao.count()); Pilote nouveau = new Pilote("Philippe", "Paris"); piloteDao.addPilote(nouveau); System.out.println("\nAjout : " + nouveau); piloteDao.findByNom("PHILIPPE") .ifPresent(p -> System.out.println("\nTrouvé : " + p)); nouveau.setSite("Lyon"); piloteDao.updatePilote(nouveau); piloteDao.findById(nouveau.getId()) .ifPresent(p -> System.out.println("\nAprès mise à jour : " + p)); piloteDao.removePilote(nouveau.getId()); System.out.println("\nAprès suppression : " + piloteDao.count()); HibernateUtil.shutdown(); } }
SELECT p FROM Pilote p WHERE PI_NOM = :nom
PI_NOM
factory.getCurrentSession()
String hql = "from Pilote p where p.nom = :nom";
et non :
String hql = "SELECT p FROM Pilote p WHERE PI_NOM = :nom";
Dans un petit projet, DAO + main peuvent suffire.
Dans un projet un peu plus propre, on ajoute une couche Service :
IHM / Main ↓ Service ↓ DAO ↓ Base de données
package fr.bouget.pilote.service; import fr.bouget.pilote.dao.PiloteDao; import fr.bouget.pilote.model.Pilote; import java.util.List; import java.util.Optional; public class PiloteService { private final PiloteDao piloteDao; public PiloteService(PiloteDao piloteDao) { this.piloteDao = piloteDao; } public List<Pilote> listerTous() { return piloteDao.findAll(); } public Optional<Pilote> rechercherParNom(String nom) { return piloteDao.findByNom(nom); } public void ajouter(String nom, String site) { piloteDao.addPilote(new Pilote(nom, site)); } }
org.postgresql
jakarta.persistence
javax.persistence
Ajouter la méthode :
List<Pilote> findBySite(String site);
à la version JDBC puis à la version Hibernate.
Créer un menu console :
Ajouter une couche PiloteService et déplacer les contrôles métier dedans :
PiloteService