Your Spring Boot API stores its data in an in-memory Map: everything disappears on restart. You need a relational database (PostgreSQL, MySQL…). Writing SQL by hand for every object is long and repetitive: an ORM (Object-Relational Mapping) bridges your Java classes and the tables.
- JPA (Jakarta Persistence, package
jakarta.persistence) is the standard specification: annotations and interfaces. - Hibernate is the most widely used implementation, and the one Spring Boot configures by default. This course uses Spring Boot 4.1 with Java 25: Hibernate 7 implements Jakarta Persistence 3.2 there.
- Spring Data JPA adds automatically generated repositories on top (lesson 2).
Configuration
Add spring-boot-starter-data-jpa, its test starter spring-boot-starter-data-jpa-test (test scope, for @DataJpaTest, lesson 5), and your database driver (org.postgresql:postgresql, or com.h2database:h2 for an in-memory test database). Spring Initializr adds these dependencies when you choose Spring Data JPA and H2 (-d dependencies=web,data-jpa,h2). Then, in application.properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/boutique
spring.datasource.username=boutique
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=update # create-drop, update, validate, none
spring.jpa.show-sql=true # shows the generated SQL (development)
spring.jpa.open-in-view=false # recommended, see lesson 4
ddl-auto controls schema generation: create-drop (recreated at every startup), update (adds what is missing), validate (checks without modifying), none. In production, use validate or none and versioned migrations (lesson 5).
A first entity
package fr.boutique.produit;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity // persistent class
@Table(name = "produit") // optional: explicit table name
public class Produit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // id generated by the database
private Long id;
@Column(nullable = false, length = 100)
private String nom;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal prix; // never use double for money
@Enumerated(EnumType.STRING) // stores "ACTIF", not 0
private StatutProduit statut = StatutProduit.ACTIF;
private LocalDateTime dateCreation; // column date_creation
protected Produit() { } // required by JPA
public Produit(String nom, BigDecimal prix) {
this.nom = nom;
this.prix = prix;
this.dateCreation = LocalDateTime.now();
}
public Long getId() { return id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
// other getters / setters...
}
enum StatutProduit { ACTIF, ARCHIVE }
Key points:
- JPA requires a no-argument constructor (at least
protected) and a non-finalclass: Hibernate instantiates the object and then fills in the fields, and creates proxies for lazy loading. That is why a record cannot be an entity; it remains ideal for DTOs. - The identifier is an object type (
Long):nullmeans “not yet saved”. @Enumerated(EnumType.STRING)is essential: with the defaultORDINAL, inserting a value in the middle of the enum would silently corrupt existing data.
Naming and types
Spring Boot configures a naming strategy that converts camelCase to snake_case: dateCreation becomes date_creation, the class LigneCommande the table ligne_commande. Java types map to SQL types: Long → bigint, String → varchar(255) by default, BigDecimal → numeric, LocalDate → date, LocalDateTime → timestamp, boolean → boolean. Primitive types (int, boolean) produce not null columns.
With show-sql=true, look at the generated create table at startup: it is the best way to check your mapping.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
The camelCase to snake_case naming strategy
Reproduce (in simplified form) Spring Boot’s default naming strategy. For each Java name in the input (class or attribute):
- replace
.with_; - insert a
_before an uppercase letter that is preceded by a lowercase letter and followed by a lowercase letter (the first letter is never affected); - convert everything to lowercase.
Print
nom -> nom_sql. Example:dateCreation -> date_creation,codeISO -> codeiso(uppercase acronyms are not split).import java.util.*; public class Main { static String versSnakeCase(String nom) { // TODO return nom; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String n = sc.nextLine().trim(); if (!n.isEmpty()) System.out.println(n + " -> " + versSnakeCase(n)); } } }- replace
Generate an entity's DDL
Write a mini
create tablegenerator (a simplified version of what Hibernate produces for PostgreSQL). The first line is the class name; each following line describes an attributenom:Type[:options], with options separated by commas:id,notnull,unique,length=N.Types:
Long/long→bigint,Integer/int→integer,String→varchar(255)(orvarchar(N)),BigDecimal→numeric(38,2),Boolean/boolean→boolean,LocalDate→date,LocalDateTime→timestamp(6). Primitives (long,int,boolean) arenot null. The id is writtenbigint generated by default as identity. Names go to snake_case (the rule from the previous exercise). Exact format:create table produit ( id bigint generated by default as identity, nom varchar(100) not null, primary key (id) )The order of suffixes is
not nullthenunique.import java.util.*; public class Main { static String versSnakeCase(String nom) { StringBuilder sb = new StringBuilder(nom.replace('.', '_')); for (int i = 1; i < sb.length() - 1; i++) { if (Character.isLowerCase(sb.charAt(i - 1)) && Character.isUpperCase(sb.charAt(i)) && Character.isLowerCase(sb.charAt(i + 1))) { sb.insert(i, '_'); i++; } } return sb.toString().toLowerCase(); } static String typeSql(String typeJava, Integer longueur) { // TODO return ""; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); String classe = sc.nextLine().trim(); // TODO } }Persist a first entity
In your Spring Boot project in the working environment, add
spring-boot-starter-data-jpa,spring-boot-starter-data-jpa-test(test scope), andcom.h2database:h2(runtime scope), or generate a new project withcurl https://start.spring.io/starter.zip -d type=maven-project -d dependencies=web,data-jpa,h2 -d javaVersion=25 -d groupId=fr.boutique -d artifactId=boutique -d packageName=fr.boutique -d baseDir=boutique -o boutique.zip. Configurespring.jpa.hibernate.ddl-auto=create-dropandspring.jpa.show-sql=true. Create theProduitentity (IDENTITY id, required name of at most 100 characters,BigDecimalprice, status as@Enumerated(EnumType.STRING), creation date). Start the application and find the generatedcreate tablein the logs.- The entity uses jakarta.persistence annotations and has a protected no-argument constructor
- The startup logs contain a create table produit with the columns nom (not null), prix, statut, and date_creation
- The statut column is a text type (varchar or enum), not an integer
- The application starts without errors
End-of-lesson quiz
What is the relationship between JPA and Hibernate?
- They are two names for the same product
- JPA is a specification, Hibernate is an implementation
- Hibernate is a specification, JPA is an implementation
Why prefer @Enumerated(EnumType.STRING)?
- It is faster
- The data stays correct if the order of the enum values changes
- It is the default value
Which value of spring.jpa.hibernate.ddl-auto is suitable for production?
- create-drop
- update
- validate (or none) with migrations
The rest of the course
- Spring Data JPA Repositories and Queries20 min
- Relationships Between Entities22 min
- Transactions, Persistence Context, and the N+1 Problem22 min
- Flyway Migrations, Optimistic Locking, and JPA Tests22 min