PostgreSQL et les migrations
Cours · Spring Boot : écrire le serveur (école d'ingénieur), chapitre 10 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Depuis le chapitre 5, la Librairie range ses livres et ses commandes dans H2, une base écrite en Java qui vit dans la mémoire de l'application. C'était idéal pour apprendre : rien à installer, une base neuve à chaque démarrage. C'est aussi son défaut : arrêtez le serveur, et toutes les commandes de la journée disparaissent. Une vraie boutique a besoin d'une vraie base de données, qui garde ses données d'un démarrage à l'autre et que plusieurs serveurs puissent partager.
Nous passons à PostgreSQL, la base libre la plus répandue pour ce genre d'application, lancée dans un conteneur Docker. Ce changement en entraîne un second, plus important qu'il n'y paraît : le schéma de la base (ses tables, ses colonnes, ses contraintes) ne peut plus être recréé à chaque démarrage à partir des entités. Il devient un objet à part entière, qui évolue par étapes numérotées, les migrations, appliquées par l'outil Flyway. Enfin, nous testerons sur PostgreSQL lui-même, et non plus sur H2, grâce à Testcontainers : nous verrons pourquoi ce n'est pas un luxe.
Objectifs du chapitre
- Lancer PostgreSQL dans un conteneur, démarré par Spring Boot avec Docker Compose.
- Décrire le schéma par des migrations Flyway versionnées, et laisser Hibernate le vérifier (
ddl-auto: validate). - Lire la table
flyway_schema_historyet comprendre les refus de Flyway et d'Hibernate. - Repérer une différence entre H2 et PostgreSQL, et la corriger.
- Tester sur un vrai PostgreSQL avec Testcontainers et
@ServiceConnection.
Le projet code/ch10/librairie part de celui du chapitre 9. Tout ce qui est montré a été obtenu avec PostgreSQL 17.11, Flyway 12.4.0 et Testcontainers 2.0.5. Docker doit être installé et lancé. Dans les journaux, on a retiré la date, le niveau et le numéro de processus en tête de ligne.
10.1 PostgreSQL dans un conteneur
Installer PostgreSQL sur chaque poste, à la bonne version, avec la bonne base et le bon mot de passe, est fastidieux. Un conteneur Docker emballe le serveur PostgreSQL tout configuré ; un fichier compose.yaml, à la racine du projet, le décrit :
# PostgreSQL pour le développement, démarré par Spring Boot (spring-boot-docker-compose).
# Port 5433 sur la machine, pour ne pas gêner un PostgreSQL déjà installé sur 5432.
services:
postgres:
image: postgres:17
container_name: livre-spring-librairie-db
environment:
POSTGRES_DB: librairie
POSTGRES_USER: librairie
POSTGRES_PASSWORD: librairie
ports:
- "5433:5432"
L'image postgres:17 crée au premier lancement une base librairie, avec l'utilisateur et le mot de passe donnés. Le serveur écoute sur le port 5432 dans le conteneur, publié sur le port 5433 de la machine.
Dans le pom.xml, H2 et sa console s'en vont ; arrivent le pilote JDBC de PostgreSQL, Flyway, et le module qui fait démarrer Docker Compose par Spring Boot :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
Avec spring-boot-docker-compose, Spring Boot lance docker compose up au démarrage de l'application, attend que les services soient prêts, puis déduit des conteneurs les réglages de connexion : il reconnaît l'image postgres, lit le port publié, la base, l'utilisateur et le mot de passe. C'est une connexion de service : aucune propriété spring.datasource.url à écrire.
Dans application.yml, l'adresse de H2 et la console disparaissent ; on ajoute seulement :
docker:
compose:
lifecycle-management: start-only # la base reste lancée entre deux démarrages
Par défaut, Spring Boot arrête les conteneurs quand l'application s'arrête ; avec start-only, la base reste lancée, et le redémarrage de l'application ne la recrée pas.
Premier démarrage (./mvnw spring-boot:run) ; on a omis la ligne qui donne le chemin du fichier et celles qui créent le réseau Docker :
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Creating
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Created
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Starting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Started
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Waiting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container livre-spring-librairie-db Healthy
[main] org.flywaydb.core.FlywayExecutor : Database: jdbc:postgresql://127.0.0.1:5433/librairie?ApplicationName=librairie (PostgreSQL 17.11)
La dernière ligne montre l'adresse que Spring Boot a construite. Le conteneur tourne :
$ docker ps --format "{{.Names}} {{.Image}} {{.Ports}}"
livre-spring-librairie-db postgres:17 0.0.0.0:5433->5432/tcp
10.2 Le schéma décrit par des migrations
Jusqu'ici, Hibernate créait les tables à partir des entités à chaque démarrage (le réglage par défaut pour une base en mémoire). Avec une base qui garde ses données, c'est impossible : recréer les tables, c'est tout effacer. Et laisser Hibernate « mettre à jour » le schéma (ddl-auto: update) est dangereux : il ajoute des colonnes mais n'en renomme ni n'en supprime jamais, ne sait pas déplacer des données, et chaque serveur y va de ses modifications sans trace.
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.