ORM, Entities, and Basic Mapping

First lesson of the Persistence with JPA and Hibernate course, free to read: about 20 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Explain the role of an ORM, JPA, and Hibernate
  • Write a JPA entity with @Entity, @Id, @GeneratedValue, @Column, and @Enumerated
  • Configure the data source and understand schema generation (ddl-auto)

JPA (Jakarta Persistence)HibernateEntities and mapping

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-final class: 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): null means “not yet saved”.
  • @Enumerated(EnumType.STRING) is essential: with the default ORDINAL, 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.

  1. CodeDifficulty 2Server sandbox2 hints

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

    1. replace . with _;
    2. 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);
    3. 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));
            }
        }
    }
  2. CodeDifficulty 3Server sandbox2 hints

    Generate an entity's DDL

    Write a mini create table generator (a simplified version of what Hibernate produces for PostgreSQL). The first line is the class name; each following line describes an attribute nom:Type[:options], with options separated by commas: id, notnull, unique, length=N.

    Types: Long/long → bigint, Integer/int → integer, String → varchar(255) (or varchar(N)), BigDecimal → numeric(38,2), Boolean/boolean → boolean, LocalDate → date, LocalDateTime → timestamp(6). Primitives (long, int, boolean) are not null. The id is written bigint 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 null then unique.

    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
        }
    }
  3. TaskDifficulty 22 hints

    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), and com.h2database:h2 (runtime scope), or generate a new project with curl 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. Configure spring.jpa.hibernate.ddl-auto=create-drop and spring.jpa.show-sql=true. Create the Produit entity (IDENTITY id, required name of at most 100 characters, BigDecimal price, status as @Enumerated(EnumType.STRING), creation date). Start the application and find the generated create table in 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

  1. 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
  2. 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
  3. 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

  1. Spring Data JPA Repositories and Queries20 min
  2. Relationships Between Entities22 min
  3. Transactions, Persistence Context, and the N+1 Problem22 min
  4. Flyway Migrations, Optimistic Locking, and JPA Tests22 min

Persistence with JPA and Hibernate · Advanced · 2 h 46 min