Beginner

Java SE Deep Dive Hibernate Fundamentals

ORM (Object-Relational Mapping) is a technique that converts data between an object-oriented programming language and a relational database management system (RDBMS). These two type syste...

Table of Contents

  1. Course presentation
  2. Introduction to Object Relational Mapping
  1. Working with Entities
  1. Modeling relationships between Entities
  1. Modeling inheritance between Entities
  1. Transaction management
  1. Working with JPQL
  1. The Criteria API

1. Course presentation

This course examines the principles and practice of developing Java applications accessing relational databases using Hibernate. He teaches:

  • How to write efficient code that accesses databases from Java applications.
  • How to manage object-relational mapping (ORM).
  • How to work with transactions.
  • How to interact with databases using JPQL and the Criteria API.

Main topics covered:

  • Explore ORM, Hibernate and their concepts.
  • Discover entities and relationships between entities.
  • Learn how to transpose the OOP concept of inheritance into a database.
  • Manage concurrent transactions and updates.
  • Interact with databases using JPQL and the Criteria API.

Prerequisites:

  • Mastery of Java Core code.
  • Basic knowledge of Apache Maven build tool.
  • Basic knowledge of database principles.

2. Introduction to Object Relational Mapping

2.1 ORM, JPA and Hibernate — Fundamental Concepts

ORM (Object-Relational Mapping) is a technique that converts data between an object-oriented programming language and a relational database management system (RDBMS). These two type systems are incompatible: one works with objects and their capabilities, the other with relationships and their capabilities.

The fundamental problem: storing the representation of objects so that they can be saved in a database, while retaining the information they contain, and then querying the relational database to reconstruct the object after retrieving the information. An object that is successfully saved and retrieved is called a persistent object.

JPA (Jakarta Persistence API) is the Java specification for object-relational mapping and persistence. Hibernate is the most used framework implementing the JPA specification. Hibernate can use either XMLs or annotations to specify mapping logic from the object-oriented world to the relational database world. In essence, you have to map an object’s classes and fields to tables and columns.

2.2 Advantages of JPA and Hibernate

  • Less code to write. We work mainly at the program object level, with much less direct interaction with the database from the Java code.
  • Productivity boost. Less code written means faster development.
  • Focus on object-oriented programming. Less attention to pay to SQL.
  • Consistent model of interaction with the database. We focus on business logic, the persistence load being on JPA/Hibernate.
  • Database vendor independence. The code is portable, and Hibernate translates it for each vendor. Seller-specific features also remain accessible if necessary.

2.3 Potential disadvantages

  • New learning layer. A good mastery of the framework is necessary before effective use.
  • More difficult debugging. Problems may occur in an additional layer.
  • Potential performance impact. Additional layer may introduce delay.
  • For simple applications, JDBC may sometimes be preferable to stay closer to the database, or to use vendor-specific features.

2.4 Object-Relational Impedance Mismatch

object-relational impedance mismatch means that the object and relational models do not work well together. OOP keeps data as interconnected objects with fields and methods, while an RDBMS keeps data as linked tables. This mismatch is divided into five sub-problems.

2.4.1 The granularity problem (Granularity)

For a flight management application, we have a Passenger class and a Ticket class. If we extend the model to include an address (with street, number, postal code, city, country), most database vendors do not allow you to create a new SQL data type to represent the Address class. We therefore end up with additional columns in the PASSENGERS table: ADDRESS_STREET, ADDRESS_NUMBER, ADDRESS_ZIP_CODE, ADDRESS_CITY, ADDRESS_COUNTRY. The sizes of the types we work with are different — this is the granularity problem.

2.4.2 The inheritance problem (Inheritance)

Inheritance is a feature of OOP that is not well represented in the relational model. Suppose there are several types of tickets: OneWayTicket, ReturnTicket, RoundTripTicket. This hierarchy can be represented in OOP, but we cannot create a OneWayTicket table which “extends” the Ticket table. The Passenger class has an association to the Ticket superclass — it is a polymorphic association. Relational databases do not have a standard way to represent such polymorphic associations. A foreign key references exactly one target table.

2.4.3 The identity problem (Identity)

Java uses two notions of equality:

  • Object identity: two references point to the same object (operator ==).
  • Logical equality: two objects can be different references in memory but logically equal (equals() method).

On the relational database side, the identity of two rows is expressed by their primary keys. Neither Java way of checking for equality is connected to how it happens in the database. surrogate keys (primary key columns with no meaning to business logic, used only to identify data) are a best practice to resolve this issue.

2.4.4 The problem of associations (Associations)

In the OOP model, associations represent connections between classes. In the relational model, a foreign key constraint on a column represents an association. In OOP, object references are directional (has-a relationship). Most relationships are one-way. For a bidirectional relationship, you must define the association twice, once on each side.

Java associations can be:

  • One-to-one
  • One-to-many
  • Many-to-many

In the relational model, the foreign key declaration on the TICKETS table is always a many-to-one association. Representing a many-to-many association in the relational model requires the introduction of a new link table (link table).

2.4.5 The Data Navigation problem

Data navigation involves traversing objects in the OOP model and joining tables in the relational model. In Java, iterating through a passenger’s tickets can be written naturally, but it is not efficient in the relational model.

The solution: minimize the number of queries executed against the database and use conditions and joins between tables. Running a Cartesian product (JOIN) on two tables of 1000 rows each will return 1 million rows.

If you wish to retrieve a record from the PASSENGERS table without the tickets:

SELECT * FROM PASSENGERS WHERE ID = ?

If you want to retrieve a record with the associated tickets:

SELECT p.*, t.* FROM PASSENGERS p LEFT JOIN TICKETS t ON t.PASSENGER_ID = p.ID WHERE p.ID = ?

2.5 Demo: Create a project, Entity Classes and persistent objects

This demonstration creates a Jakarta EE 10 application with Hibernate, creates the entity classes and shows how objects are persisted in the database.

Maven project structure:

airport/
├── pom.xml
├── setup.sql
└── src/
    └── main/
        ├── java/
        │   └── com/pluralsight/hibernatefundamentals/
        │       ├── Main.java
        │       └── airport/
        │           ├── Airport.java
        │           ├── Passenger.java
        │           └── Ticket.java
        └── resources/
            └── META-INF/
                └── persistence.xml

pom.xml — Maven configuration:

<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.pluralsight.hibernatefundamentals</groupId>
    <artifactId>airport</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.2.7.Final</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

Note: The hibernate-core dependency also adds Jakarta Persistence API as a transitive dependency. Lombok automatically generates constructors, getters and setters via annotations.

setup.sql — Creating the database:

CREATE DATABASE M02_EX01;

persistence.xml — JPA/Hibernate configuration:

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

    <persistence-unit name="hibernatefundamentals.m02.ex01">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/M02_EX01"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value=""/>

            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>

            <property name="hibernate.hbm2ddl.auto" value="create"/>
        </properties>

    </persistence-unit>

</persistence>

Parameter hibernate.hbm2ddl.auto: the value create recreates the database from scratch on each execution. For an incremental update, use update.

Airport.java — Entity Airport:

package com.pluralsight.hibernatefundamentals.airport;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Entity
@Table(name = "AIRPORTS")
@Access(AccessType.FIELD)
@NoArgsConstructor
public class Airport {

    @Id
    @Column(name = "ID")
    @Getter
    private int id;

    @Column(name = "NAME")
    @Getter
    @Setter
    private String name;

    @OneToMany(mappedBy = "airport")
    private List<Passenger> passengers = new ArrayList<>();

    public Airport(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public List<Passenger> getPassengers() {
        return Collections.unmodifiableList(passengers);
    }

    public void addPassenger(Passenger passenger) {
        passengers.add(passenger);
    }
}

Key points:

  • @Entity marks the class as a JPA entity with a corresponding table.
  • @Table(name = "AIRPORTS") specifies the table name.
  • @Access(AccessType.FIELD) indicates that Hibernate accesses persistent state via instance fields.
  • @Id marks the field as primary key.
  • @Column(name = "ID") specifies the name of the column in the database.
  • @OneToMany(mappedBy = "airport") defines a one-to-many relationship to Passenger, mapped by the airport field of Passenger.
  • getPassengers() returns an unmodifiable list to protect the integrity of the list.
  • @NoArgsConstructor (Lombok) generates a constructor without arguments, required by JPA.

Passenger.java — Entity Passenger:

package com.pluralsight.hibernatefundamentals.airport;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Entity
@Table(name = "PASSENGERS")
@NoArgsConstructor
public class Passenger {

    @Id
    @Column(name = "ID")
    private int id;

    @Column(name = "NAME")
    @Getter
    @Setter
    private String name;

    @ManyToOne
    @JoinColumn(name = "AIRPORT_ID")
    @Getter
    @Setter
    private Airport airport;

    @OneToMany(mappedBy = "passenger")
    private List<Ticket> tickets = new ArrayList<>();

    public Passenger(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public List<Ticket> getTickets() {
        return Collections.unmodifiableList(tickets);
    }

    public void addTicket(Ticket ticket) {
        tickets.add(ticket);
    }
}

Key points:

  • @ManyToOne: several passengers can use the same airport.
  • @JoinColumn(name = "AIRPORT_ID"): join column in the PASSENGERS table.
  • @OneToMany(mappedBy = "passenger"): a passenger can have several tickets, mapped by the passenger field of Ticket.

Ticket.java — Entity Ticket:

package com.pluralsight.hibernatefundamentals.airport;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @Id
    @Column(name = "ID")
    private int id;

    @Column(name = "NUMBER")
    @Getter
    @Setter
    private String number;

    @ManyToOne
    @JoinColumn(name = "PASSENGER_ID")
    @Getter
    @Setter
    private Passenger passenger;

    public Ticket(int id, String number) {
        this.id = id;
        this.number = number;
    }
}

Main.java — Main class:

package com.pluralsight.hibernatefundamentals;

import com.pluralsight.hibernatefundamentals.airport.Airport;
import com.pluralsight.hibernatefundamentals.airport.Passenger;
import com.pluralsight.hibernatefundamentals.airport.Ticket;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class Main {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hibernatefundamentals.m02.ex01");
        EntityManager em = emf.createEntityManager();

        em.getTransaction().begin();

        Airport airport = new Airport(1, "Henri Coanda");
        Passenger john = new Passenger(1, "John Smith");
        john.setAirport(airport);
        Passenger mike = new Passenger(2, "Michael Johnson");
        mike.setAirport(airport);
        airport.addPassenger(john);
        airport.addPassenger(mike);

        Ticket ticket1 = new Ticket(1, "AA1234");
        ticket1.setPassenger(john);

        Ticket ticket2 = new Ticket(2, "BB5678");
        ticket2.setPassenger(john);

        john.addTicket(ticket1);
        john.addTicket(ticket2);

        Ticket ticket3 = new Ticket(3, "CC0987");
        ticket3.setPassenger(mike);
        mike.addTicket(ticket3);

        em.persist(airport);
        em.persist(john);
        em.persist(mike);

        em.persist(ticket1);
        em.persist(ticket2);
        em.persist(ticket3);

        em.getTransaction().commit();
        emf.close();
    }
}

Key points:

  • Persistence.createEntityManagerFactory("...") creates the EntityManagerFactory using the persistence-unit name defined in persistence.xml.
  • emf.createEntityManager() creates the EntityManager, the main interface for interacting with the persistence context.
  • em.getTransaction().begin() starts the transaction.
  • em.persist(object) inserts a matching record into the database.
  • em.getTransaction().commit() commits the transaction, writing changes not yet flushed to the database.
  • emf.close() closes the EntityManagerFactory.

Checking in MySQL database:

USE M02_EX01;

SELECT * FROM AIRPORTS;
-- Résultat : ID=1, NAME='Henri Coanda'

SELECT * FROM PASSENGERS;
-- Résultat : John Smith (ID 1, AIRPORT_ID 1), Michael Johnson (ID 2, AIRPORT_ID 1)

SELECT * FROM TICKETS;
-- Résultat : AA1234 (PASSENGER_ID 1), BB5678 (PASSENGER_ID 1), CC0987 (PASSENGER_ID 2)

2.6 Module 2 Summary

  • Introduction to ORM, JPA and Hibernate.
  • Review of the pros and cons of Hibernate.
  • Detailed analysis of object-relational impedance mismatch issues.
  • Creation of a first Hibernate application interacting with a MySQL database.

3. Working with Entities

3.1 What is an Entity?

An entity is a domain object that can be persisted. An entity class must respect the following rules:

  • Be annotated with @Entity or defined as entity via XML configuration.
  • Be a top-level class (top-level class); non-standard interfaces cannot be entities.
  • Have a public or protected constructor with no arguments (can define other constructors).
  • The JPA specification requires that an entity class not be final. Hibernate is less strict, but declaring final classes as entities prevents Hibernate from using the proxy pattern for performance improvement — bad practice.
  • Supports inheritance, polymorphic associations, polymorphic queries.

The persistent state of an entity can be of the following types:

  • Primitive types: byte, short, int, long, float, double, boolean, char.
  • Other entity types, embeddable types.
  • Serializable types, including primitive wrappers (Integer, Character) and user-defined types implementing Serializable.
  • Enums.
  • Collections of primitive types, embeddable types, or entity types.

3.2 Table mapping annotations

Annotations that specify table mapping:

AnnotationDescription
@TableSpecifies the primary table for the annotated entity. If absent, the table name corresponds to the name of the entity class.
@SecondaryTableSpecifies a secondary table for the entity. If absent, all persistent fields are mapped to the primary table.
@SecondaryTablesSpecifies multiple secondary tables for an entity class.

Parameters of @Table:

  • name — the name of the table (default: entity name).
  • catalog — default: the default catalog.
  • schema — default: the default schema.
  • uniqueConstraints — unique constraints to place on the table.
  • indexes — indexes (used if table generation is in effect).

Parameters of @SecondaryTable: same as @Table, plus:

  • pkJoinColumns — columns used to join with the primary table.
  • foreignKey — foreign key constraints for columns in pkJoinColumns.

3.3 Demo: Secondary Table with a single field

A passenger has an address represented by a single field. The persistence-unit is hibernatefundamentals.m03.ex01, database M03_EX01.

@Entity
@Table(name = "PASSENGERS")
@SecondaryTable(
    name = "ADDRESSES",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "PASSENGER_ID", referencedColumnName = "PASSENGER_ID")
)
@NoArgsConstructor
public class Passenger {

    @Id
    @Column(name = "PASSENGER_ID")
    private int id;

    @Column(name = "PASSENGER_NAME", table = "PASSENGERS")
    @Getter @Setter
    private String name;

    @Column(name = "PASSENGER_ADDRESS", table = "ADDRESSES",
            columnDefinition = "varchar(25) not null")
    @Getter @Setter
    private String address;

    public Passenger(int id, String name, String address) {
        this.id = id;
        this.name = name;
        this.address = address;
    }
}

Database result:

-- Table PASSENGERS : PASSENGER_ID, PASSENGER_NAME
-- Table ADDRESSES : PASSENGER_ID (FK), PASSENGER_ADDRESS

USE M03_EX01;
SELECT * FROM PASSENGERS;
-- John Smith (PASSENGER_ID 1)
SELECT * FROM ADDRESSES;
-- '3, Flowers Street, Boston' appartenant au passager ID 1

The @SecondaryTable annotation on ADDRESSES specifies that PASSENGER_ID in ADDRESSES references PASSENGER_ID in the primary table PASSENGERS. A single entity generates two tables in the database.

3.4 Demo: Secondary Table with multiple fields

If the address requires multiple fields (street, number, zip code, city), the persistence-unit is hibernatefundamentals.m03.ex02.

@Entity
@Table(name = "PASSENGERS")
@SecondaryTable(
    name = "ADDRESSES",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "PASSENGER_ID", referencedColumnName = "PASSENGER_ID")
)
@NoArgsConstructor
public class Passenger {

    @Id
    @Column(name = "PASSENGER_ID")
    private int id;

    @Column(name = "PASSENGER_NAME")
    @Getter @Setter
    private String name;

    @Column(name = "STREET", table = "ADDRESSES", columnDefinition = "varchar(25) not null")
    @Getter @Setter
    private String street;

    @Column(name = "NUMBER", table = "ADDRESSES", columnDefinition = "varchar(10) not null")
    @Getter @Setter
    private String number;

    @Column(name = "ZIP_CODE", table = "ADDRESSES", columnDefinition = "varchar(10) not null")
    @Getter @Setter
    private String zipCode;

    @Column(name = "CITY", table = "ADDRESSES", columnDefinition = "varchar(25) not null")
    @Getter @Setter
    private String city;
}

Checking:

USE M03_EX02;
SELECT * FROM PASSENGERS;
-- John Smith (PASSENGER_ID 1)
SELECT * FROM ADDRESSES;
-- CITY, NUMBER, STREET, ZIP_CODE pour le passager ID 1

3.5 Demo: Multiple Secondary Tables

A passenger has an address and a telephone number. The persistence-unit is hibernatefundamentals.m03.ex03.

@Entity
@Table(name = "PASSENGERS")
@SecondaryTables({
    @SecondaryTable(
        name = "ADDRESSES",
        pkJoinColumns = @PrimaryKeyJoinColumn(name = "PASSENGER_ID", referencedColumnName = "PASSENGER_ID")
    ),
    @SecondaryTable(
        name = "PHONES",
        pkJoinColumns = @PrimaryKeyJoinColumn(name = "PASSENGER_ID", referencedColumnName = "PASSENGER_ID")
    )
})
public class Passenger {

    @Id
    @Column(name = "PASSENGER_ID")
    private int id;

    // Champs address (dans ADDRESSES)...
    @Column(name = "STREET", table = "ADDRESSES", columnDefinition = "varchar(25) not null")
    @Getter @Setter private String street;

    // Champs phone (dans PHONES)
    @Column(name = "AREA_CODE", table = "PHONES", columnDefinition = "varchar(5) not null")
    @Getter @Setter private String areaCode;

    @Column(name = "PREFIX", table = "PHONES", columnDefinition = "varchar(5) not null")
    @Getter @Setter private String prefix;

    @Column(name = "LINE_NUMBER", table = "PHONES", columnDefinition = "varchar(10) not null")
    @Getter @Setter private String lineNumber;
}

Checking:

USE M03_EX03;
SELECT * FROM PASSENGERS;   -- John Smith
SELECT * FROM ADDRESSES;    -- L'adresse de John
SELECT * FROM PHONES;       -- Le téléphone de John (area_code, prefix, line_number)

An entity here generates three different tables: the primary table and two secondary tables.

3.6 Entity Access Type

There are two ways in which the persistence provider runtime accesses the persistent state of an entity:

  • Field access: via instance variables.
  • Property access: via the getter and setter methods.

Rules for determining access type:

  • If the mapping annotations are on persistent fields → field-based access.
  • If the annotations are on getters → property-based access.
  • If no mapping annotations, it is the position of the @Id annotation which determines the access type.

Use @Access to explicitly define access type:

// Field-based access pour toute la classe
@Entity
@Access(AccessType.FIELD)
public class Passenger {

    @Id
    private int id;

    // Par défaut field-based, mais on peut surcharger pour un attribut spécifique
    @Access(AccessType.PROPERTY)
    @Column(name = "NAME")
    public String getName() { return name; }

    private String name;
}

Field-based vs property-based comparison:

Field-based accessProperty-based access
Allows you to omit getters for unexposed fieldsAccessor methods can perform additional logic
Fields declared on a single line, better readabilityUseful if you want additional logic to run during persistence
If persistence should avoid additional actions → prefer field-based

3.7 Entity Primary Keys and Entity Identity

Each entity must have a primary key. This can correspond to one or more fields or properties of the entity class.

  • Simple primary key: corresponds to a single field/property. Use @Id.
  • Composite primary key: can correspond to a single persistent field or a set of fields. Use @EmbeddedId or @IdClass.

Rules for composite primary keys:

  1. Must be public with a public constructor with no arguments.
  2. Must be serializable.
  3. Must define the equals() and hashCode() methods.
  4. Logical equality semantics must be consistent with database equality.
  5. Can be represented as an embedded class or an id class.

@GeneratedValue specifies a generation strategy for the primary key value, used with @Id. Settings:

  • strategy — the build strategy to use.
  • generator — the name of the generator to use.

Available policies:

  • GenerationType.AUTO — the provider automatically determines the strategy for the particular database (default).
  • GenerationType.IDENTITY — uses an auto-incremented column from the database.
  • GenerationType.SEQUENCE — uses a database sequence.
  • GenerationType.TABLE — uses a dedicated table to simulate a sequence.

3.8 Demo: Primary Keys with @GeneratedValue

Without @GeneratedValue, two objects with the same ID (default 0 for int) cause an EntityExistsException: a different object with the same identifier value was already associated with the session.

@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    @Getter
    private int id;

    @Column(name = "SERIES")
    @Getter @Setter
    private String series;

    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;

    @Column(name = "ORIGIN")
    @Getter @Setter
    private String origin;

    @Column(name = "DESTINATION")
    @Getter @Setter
    private String destination;
}

With GenerationType.AUTO, Hibernate creates a TICKETS_SEQ table in addition to TICKETS. TICKETS_SEQ provides the first value (1) to insert into TICKETS. Each insertion into TICKETS retrieves the next value from TICKETS_SEQ.

USE M03_EX04;
SELECT * FROM TICKETS;
-- ID=1 (premier ticket), ID=2 (deuxième ticket)

3.9 Demo: Embeddable Primary Key and Embedded ID

// Classe servant d'ID composite
@Embeddable
public class TicketKey implements Serializable {

    @Column(name = "SERIES")
    @Getter @Setter
    private String series;

    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;

    // equals() et hashCode() obligatoires
    @Override
    public boolean equals(Object o) { ... }

    @Override
    public int hashCode() { ... }
}

// Entity utilisant l'ID composite
@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @EmbeddedId
    private TicketKey id;

    @Column(name = "ORIGIN")
    @Getter @Setter
    private String origin;

    @Column(name = "DESTINATION")
    @Getter @Setter
    private String destination;
}
// Dans Main.java
TicketKey key = new TicketKey();
key.setSeries("AA");
key.setNumber("1234");
Ticket ticket = new Ticket();
ticket.setId(key);
ticket.setOrigin("NYC");
ticket.setDestination("LAX");
em.persist(ticket);

Checking:

USE M03_EX05;
SELECT * FROM TICKETS;
-- ORIGIN, DESTINATION (de Ticket), SERIES, NUMBER (de TicketKey)

@Embeddable indicates that the class will be embedded in another entity. It must implement Serializable because the ID can be used as a key in the second level cache.

3.10 Demo: Embeddable Primary Key and ID Class

An alternative to @EmbeddedId: use @IdClass. In this case, the fields of the ID class must be duplicated in the entity, annotated with @Id.

// La même classe TicketKey, annotée @Embeddable
@Embeddable
public class TicketKey implements Serializable {
    private String series;
    private String number;
    // equals() et hashCode()...
}

// Entity utilisant @IdClass
@Entity
@Table(name = "TICKETS")
@IdClass(TicketKey.class)
@NoArgsConstructor
public class Ticket {

    @Id
    @Column(name = "SERIES")
    @Getter @Setter
    private String series;

    @Id
    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;

    @Column(name = "ORIGIN")
    @Getter @Setter
    private String origin;

    @Column(name = "DESTINATION")
    @Getter @Setter
    private String destination;
}
// Dans Main.java : pas de TicketKey instanciée séparément
Ticket ticket = new Ticket();
ticket.setSeries("AA");
ticket.setNumber("1234");
ticket.setOrigin("NYC");
ticket.setDestination("LAX");
em.persist(ticket);

The TICKETS table contains the columns ORIGIN, DESTINATION (from Ticket) and SERIES, NUMBER (from IdClass). The composite key is made up of SERIES and NUMBER.

3.11 Module 3 Summary

  • Definition of an entity and analysis of its particularities.
  • Mapping objects to tables (primary, secondary, multiple secondary).
  • Examining entity access types (field-based vs property-based).
  • Definition of entity primary keys:
  • Via @GeneratedValue.
  • Via an embedded primary key and @EmbeddedId.
  • Via an embedded primary key and @IdClass.

4. Model relationships between Entities

4.1 Types and directions of relationships

JPA supports the following types of relationships between entities:

TypeDescription
@OneToManyAn entity is associated with several others
@ManyToOneMultiple entities are associated with a single
@ManyToManySeveral entities are associated with several others
@OneToOneAn entity is associated with exactly one other

Relationship directions:

  • Unidirectional: relationship with only one owning side. Navigation possible only from the owner side to the other.
  • Bidirectional: relationship with an owner side and an inverse side (inverse side). Navigation possible in both directions. Relationships in Java code are reflected as foreign keys in the database (two-way navigation possible).

The owning side:

  • Every relationship, no matter its direction, has a possessive side.
  • This owner side controls relationship updates in the database.
  • In a one-to-many / many-to-one relationship, the many side is always the owning side — it contains the physical reference (foreign key).
  • The reverse side of a bidirectional relationship must reference its owning side via the mappedBy attribute.

Example:

In a one-to-many relationship between Passenger and Ticket: passenger John Smith (ID 1) has several tickets. ID 1 is kept on the many side (in TICKETS) as a foreign key. If we want to change the relationship so that the ticket belongs to Mike Johnson (ID 2), we change the foreign key on the owner side (TICKETS).

4.2 Demo: One-to-many and Many-to-one relationships

In the demo project, Passenger and Ticket have generated IDs (@GeneratedValue).

Passenger side (one):

@Entity
@Table(name = "PASSENGERS")
@NoArgsConstructor
public class Passenger {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NAME")
    @Getter @Setter
    private String name;

    // Relation One-to-many : un passager peut avoir plusieurs tickets
    @OneToMany(mappedBy = "passenger")
    private List<Ticket> tickets = new ArrayList<>();

    public Passenger(String name) { this.name = name; }

    public List<Ticket> getTickets() {
        return Collections.unmodifiableList(tickets);
    }

    public void addTicket(Ticket ticket) {
        tickets.add(ticket);
    }
}

Ticket side (many, owning side):

@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;

    // Relation Many-to-one : plusieurs tickets appartiennent à un passager
    @ManyToOne
    @JoinColumn(name = "PASSENGER_ID")
    @Getter @Setter
    private Passenger passenger;

    public Ticket(String number) { this.number = number; }
}

Main.java:

Passenger john = new Passenger("John Smith");
Ticket ticket1 = new Ticket("AA1234");
ticket1.setPassenger(john);
Ticket ticket2 = new Ticket("BB5678");
ticket2.setPassenger(john);
john.addTicket(ticket1);
john.addTicket(ticket2);

em.persist(john);
em.persist(ticket1);
em.persist(ticket2);

Checking:

USE M04_EX01;
SELECT * FROM PASSENGERS;   -- John Smith
SELECT * FROM TICKETS;      -- AA1234 (PASSENGER_ID 1), BB5678 (PASSENGER_ID 1)

4.3 Demo: Many-to-many relationships

For a many-to-many relationship, we modify the previous project. A ticket can now belong to several passengers.

Passenger side:

// @OneToMany remplacée par @ManyToMany
@ManyToMany(mappedBy = "passengers")
private List<Ticket> tickets = new ArrayList<>();

Ticket side:

// Supprimer le champ passenger (one), ajouter une liste passengers
@ManyToMany
private List<Passenger> passengers = new ArrayList<>();

public List<Passenger> getPassengers() {
    return Collections.unmodifiableList(passengers);
}

public void addPassenger(Passenger passenger) {
    passengers.add(passenger);
}

Main.java:

Passenger john = new Passenger("John Smith");
Passenger mike = new Passenger("Michael Johnson");

Ticket ticket1 = new Ticket("AA1234");
Ticket ticket2 = new Ticket("BB5678");

// ticket1 appartient à John ET Mike
john.addTicket(ticket1);
mike.addTicket(ticket1);
ticket1.addPassenger(john);
ticket1.addPassenger(mike);

// ticket2 appartient à John ET Mike
john.addTicket(ticket2);
mike.addTicket(ticket2);
ticket1.addPassenger(john);
ticket1.addPassenger(mike);

em.persist(john);
em.persist(mike);
em.persist(ticket1);
em.persist(ticket2);

Result: Hibernate automatically creates a link table TICKETS_PASSENGERS with 4 rows (2 passengers × 2 tickets).

USE M04_EX02;
SELECT * FROM PASSENGERS;           -- John Smith, Mike Johnson
SELECT * FROM TICKETS;              -- AA1234, BB5678
SELECT * FROM TICKETS_PASSENGERS;   -- 4 lignes (ID_ticket, ID_passenger)

4.4 Annotations for defining relationships

AnnotationDescription
@JoinTableSpecifies the cross-reference table for mapping a relationship. Must be specified on the owner side.
@JoinColumnSpecifies the column for joining an entity association.
@JoinColumnsDefines the mapping for composite foreign keys. Group of @JoinColumn annotations.

Parameters of @JoinTable:

  • name — name of the cross-reference table.
  • joinColumns — FK columns in the cross-reference table referencing the owning entity’s table.
  • inverseJoinColumns — FK columns in cross-reference table referencing the non-owning entity’s table.

Parameters of @JoinColumn:

  • name — foreign key name.
  • referencedColumnName — name of the column referenced by this FK column.

4.5 Demo: Join Tables on one column

One-to-one relationship between Manager and Department via a link table MANAGER_TO_DEPARTMENT.

@Entity
@Table(name = "MANAGERS")
@NoArgsConstructor
public class Manager {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NAME")
    @Getter @Setter
    private String name;

    @OneToOne
    @JoinTable(
        name = "MANAGER_TO_DEPARTMENT",
        joinColumns = @JoinColumn(
            name = "MANAGER_ID",
            referencedColumnName = "ID"
        ),
        inverseJoinColumns = @JoinColumn(
            name = "DEPARTMENT_ID",
            referencedColumnName = "ID",
            nullable = false
        )
    )
    @Getter @Setter
    private Department department;

    public Manager(String name) { this.name = name; }
}

Result:

USE M04_EX03;
SELECT * FROM MANAGERS;                -- John Smith (ID 1)
SELECT * FROM DEPARTMENTS;             -- Accounting (ID 1)
SELECT * FROM MANAGER_TO_DEPARTMENT;   -- MANAGER_ID=1, DEPARTMENT_ID=1

The MANAGER_ID column references the ID column of MANAGERS, and DEPARTMENT_ID references the ID column of DEPARTMENTS.

4.6 Demo: Join Tables on Multiple Columns

many-to-one relationship between Payment and Ticket, with join on two columns.

@Entity
@Table(name = "PAYMENTS")
@NoArgsConstructor
public class Payment {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "AMOUNT")
    @Getter @Setter
    private double amount;

    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "TICKET_ID", referencedColumnName = "ID"),
        @JoinColumn(name = "TICKET_NUMBER", referencedColumnName = "NUMBER")
    })
    @Getter @Setter
    private Ticket ticket;
}

Result:

USE M04_EX04;
SELECT * FROM TICKETS;
-- ID=1, NUMBER='AA1234'
SELECT * FROM PAYMENTS;
-- TICKET_ID=1, TICKET_NUMBER='AA1234'

The join is done on two columns as specified by @JoinColumns.

4.7 Embeddable Classes

embeddable classes are fine-grained classes representing the state of an entity. Features :

  • Do not have their own persistent identity.
  • Cannot be shared between multiple persistent entities.
  • Exist only as part of the state of the entity to which they belong.
  • Must be annotated with @Embeddable.

Useful annotations:

  • @AttributeOverride — overrides the mapping for a particular field or property of an embeddable class.
  • @AttributeOverrides — overrides the mapping of multiple properties or fields.
  • @ElementCollection — defines a collection of instances of a basic or embeddable type.
  • @CollectionTable — specifies the table used for mapping collections of basic or embeddable types.
  • @MapKeyColumn — specifies the column name for the map key if the key is a basic type.

4.8 Demo: Embedding classes in Entities

@Embeddable
public class Address {
    @Getter @Setter private String street;
    @Getter @Setter private String number;
    @Getter @Setter private String zipCode;
    @Getter @Setter private String city;
}

@Entity
@Table(name = "PASSENGERS")
@NoArgsConstructor
public class Passenger {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NAME")
    @Getter @Setter
    private String name;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street",  column = @Column(name = "PASSENGER_STREET")),
        @AttributeOverride(name = "number",  column = @Column(name = "PASSENGER_NUMBER")),
        @AttributeOverride(name = "zipCode", column = @Column(name = "PASSENGER_ZIP_CODE")),
        @AttributeOverride(name = "city",    column = @Column(name = "PASSENGER_CITY"))
    })
    @Getter @Setter
    private Address address;
}

Result: a single PASSENGERS table with columns ID, NAME, PASSENGER_STREET, PASSENGER_NUMBER, PASSENGER_ZIP_CODE, PASSENGER_CITY.

USE M04_EX05;
SELECT * FROM PASSENGERS;
-- ID, NAME, PASSENGER_STREET, PASSENGER_NUMBER, PASSENGER_ZIP_CODE, PASSENGER_CITY

4.9 Demo: Embedding class collections in Entities

@Embeddable
public class Ticket {
    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;
}

@Entity
@Table(name = "PASSENGERS")
@NoArgsConstructor
public class Passenger {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NAME")
    @Getter @Setter
    private String name;

    @ElementCollection
    @CollectionTable(
        name = "PASSENGER_TICKETS",
        joinColumns = @JoinColumn(name = "PASSENGER_ID", referencedColumnName = "ID")
    )
    private List<Ticket> tickets = new ArrayList<>();

    public void addTicket(Ticket ticket) { tickets.add(ticket); }
}

Result:

USE M04_EX06;
SELECT * FROM PASSENGERS;
-- John Smith (ID 1)
SELECT * FROM PASSENGER_TICKETS;
-- PASSENGER_ID=1, NUMBER='AA1234'
-- PASSENGER_ID=1, NUMBER='BB5678'

4.10 Demo: Embedding class maps in Entities

@Entity
@Table(name = "PASSENGERS")
@NoArgsConstructor
public class Passenger {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NAME")
    @Getter @Setter
    private String name;

    // Collection d'embeddable Tickets
    @ElementCollection
    @CollectionTable(
        name = "PASSENGER_TICKETS",
        joinColumns = @JoinColumn(name = "PASSENGER_ID", referencedColumnName = "ID")
    )
    private List<Ticket> tickets = new ArrayList<>();

    // Map d'attributs particuliers au passager
    @ElementCollection
    @MapKeyColumn(name = "ATTRIBUTE_NAME")
    @Column(name = "ATTRIBUTE_VALUE")
    @CollectionTable(
        name = "PASSENGER_ATTRIBUTES",
        joinColumns = @JoinColumn(name = "PASSENGER_ID", referencedColumnName = "ID")
    )
    private Map<String, String> attributes = new HashMap<>();

    public void addAttribute(String key, String value) {
        attributes.put(key, value);
    }
}
// Dans Main.java
Passenger john = new Passenger("John Smith");
john.addAttribute("VIP", "Yes");
john.addAttribute("FREQUENT_FLYER", "Yes");
em.persist(john);

Checking:

USE M04_EX07;
SELECT * FROM PASSENGERS;
-- John Smith (ID 1)
SELECT * FROM PASSENGER_ATTRIBUTES;
-- PASSENGER_ID=1, ATTRIBUTE_NAME='VIP',           ATTRIBUTE_VALUE='Yes'
-- PASSENGER_ID=1, ATTRIBUTE_NAME='FREQUENT_FLYER', ATTRIBUTE_VALUE='Yes'

4.11 Module 4 Summary

  • Analysis of relationship types and directions.
  • Using annotations to define relationships.
  • Using embeddable classes to represent the state of an entity.
  • Various demonstrations: embedding of simple classes, collections and maps of embeddables.

5. Model inheritance between Entities

5.1 Introduction to Entity inheritance

Entity class inheriting from another entity class:

  • An abstract class can be specified as an entity.
  • The abstract class defines a persistent state inherited by its subclasses.
  • An abstract entity class will be annotated with @Entity, mapped as an entity, and can be the target of queries.
  • JPA supports polymorphic associations and queries for entities.

MappedSuperclass:

An entity can inherit from a superclass that provides persistent state and mapping information, but which is not itself an entity. This mapped superclass cannot be queried directly. It can only define unidirectional relationships.

@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected int id;
    // ...
}

The persistence framework ignores annotations on simple non-entity superclasses (neither @Entity nor @MappedSuperclass).

The @AttributeOverride and @AssociationOverride annotations are used to override mappings in concrete classes.

5.2 Demo: Extend an Entity

Class hierarchy: Ticket (abstract entity) ← OneWayTicket, ReturnTicket.

@Entity
@Table(name = "TICKETS")
public abstract class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NUMBER")
    @Getter @Setter
    private String number;
}

@Entity
public class OneWayTicket extends Ticket {

    @Column(name = "LATEST_DEPARTURE_DATE")
    @Getter @Setter
    private String latestDepartureDate;
}

@Entity
public class ReturnTicket extends Ticket {

    @Column(name = "LATEST_RETURN_DATE")
    @Getter @Setter
    private String latestReturnDate;
}

Result: by default, Hibernate uses the Single Table strategy — a single TICKETS table with a DTYPE discriminator column.

USE M05_X01;
SELECT * FROM TICKETS;
-- OneWayTicket  : DTYPE='OneWayTicket', NUMBER, LATEST_DEPARTURE_DATE non null, LATEST_RETURN_DATE null
-- ReturnTicket  : DTYPE='ReturnTicket', NUMBER, LATEST_DEPARTURE_DATE null, LATEST_RETURN_DATE non null

5.3 Demo: Extending a non-Entity (MappedSuperclass)

Hierarchy: Ticket (@MappedSuperclass) ← OneWayTicket, ReturnTicket (entities with their own tables).

@MappedSuperclass
public abstract class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    protected int id;

    @Column(name = "NUMBER")
    @Getter @Setter
    protected String number;

    @ManyToOne
    @JoinColumn(name = "passenger_id")
    @Getter @Setter
    protected Passenger passenger;
}

@Entity
@Table(name = "ONE_WAY_TICKET")
public class OneWayTicket extends Ticket {

    @Column(name = "LATEST_DEPARTURE_DATE")
    @Getter @Setter
    private String latestDepartureDate;
}

@Entity
@Table(name = "RETURN_TICKET")
@AssociationOverride(
    name = "passenger",
    joinColumns = @JoinColumn(name = "pass_id")
)
public class ReturnTicket extends Ticket {

    @Column(name = "LATEST_RETURN_DATE")
    @Getter @Setter
    private String latestReturnDate;
}

Checking:

USE M05_EX02;
SELECT * FROM PASSENGERS;       -- John Smith (ID 1)
SELECT * FROM ONE_WAY_TICKET;   -- AA1234 avec passenger_id=1
SELECT * FROM RETURN_TICKET;    -- BB5678 avec pass_id=1 (colonne surchargée par @AssociationOverride)

5.4 Mapping Strategies

Three basic strategies for mapping a class hierarchy to a relational database:

Single Table per Class Hierarchy

All classes in the hierarchy are mapped into a single table. This table has a discriminator column that identifies the specific subclass.

  • Advantage: good support for polymorphic relationships.
  • Disadvantage: requires columns corresponding to subclass-specific state to be nullable.

Annotations:

  • @Inheritance(strategy = InheritanceType.SINGLE_TABLE) — on the root class.
  • @DiscriminatorColumn(name = "TYPE_COL") — discriminator column (only on the root).
  • @DiscriminatorValue("VALUE") — discriminator value for each concrete class.

Joined Subclass Strategy

The root of the hierarchy is represented by a table. Each subclass is represented by a separate table containing its specific fields. The join is done via the primary key column of the superclass table which serves as a foreign key to the subclass table.

  • Advantage: good support for polymorphic relationships.
  • Disadvantage: requires one or more join operations → may hurt performance.

Annotation: @Inheritance(strategy = InheritanceType.JOINED).

Table for Concrete Class Strategy

Each concrete subclass is mapped into a separate table. All properties, including inherited properties, are mapped into the class table.

  • Disadvantage: poor support for polymorphic relationships. Typically requires a SQL UNION for hierarchy queries. Can cause problems with ID generation strategies (duplicate IDs between tables).

Annotation: @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS).

5.5 Demo: Single Table per Class Hierarchy

@Entity
@Table(name = "TICKETS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "TICKET_TYPE")
public abstract class Ticket {
    @Id @GeneratedValue private int id;
    @Column(name = "NUMBER") @Getter @Setter private String number;
}

@Entity
@DiscriminatorValue("O")
public class OneWayTicket extends Ticket {
    @Column(name = "LATEST_DEPARTURE_DATE") @Getter @Setter
    private String latestDepartureDate;
}

@Entity
@DiscriminatorValue("R")
public class ReturnTicket extends Ticket {
    @Column(name = "LATEST_RETURN_DATE") @Getter @Setter
    private String latestReturnDate;
}

Result: a single table TICKETS with TICKET_TYPE not null.

USE M05_EX03;
SELECT * FROM TICKETS;
-- AA1234 : TICKET_TYPE='O', LATEST_DEPARTURE_DATE non null, LATEST_RETURN_DATE null
-- BB5678 : TICKET_TYPE='R', LATEST_DEPARTURE_DATE null,     LATEST_RETURN_DATE non null

5.6 Demo: Joined Subclass Strategy

@Entity
@Table(name = "TICKETS")
@Inheritance(strategy = InheritanceType.JOINED)
// Pas de @DiscriminatorColumn nécessaire
public abstract class Ticket {
    @Id @GeneratedValue private int id;
    @Column(name = "NUMBER") @Getter @Setter private String number;
}

@Entity
@Table(name = "ONE_WAY_TICKET")
// Pas de @DiscriminatorValue nécessaire
public class OneWayTicket extends Ticket {
    @Column(name = "LATEST_DEPARTURE_DATE") @Getter @Setter
    private String latestDepartureDate;
}

@Entity
@Table(name = "RETURN_TICKET")
public class ReturnTicket extends Ticket {
    @Column(name = "LATEST_RETURN_DATE") @Getter @Setter
    private String latestReturnDate;
}

Result: 3 tables created. Inserting a OneWayTicket generates two INSERT: one in TICKETS (common part) and one in ONE_WAY_TICKET (specific part).

USE M05_EX04;
SELECT * FROM TICKETS;          -- ID=1 (AA1234), ID=2 (BB5678)
SELECT * FROM ONE_WAY_TICKET;   -- ID=1, LATEST_DEPARTURE_DATE
SELECT * FROM RETURN_TICKET;    -- ID=2, LATEST_RETURN_DATE

5.7 Demo: Table for Concrete Class Strategy

@Entity
// Pas de @Table sur la superclasse (pas de table pour elle)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Ticket {
    @Id @GeneratedValue private int id;
    @Column(name = "NUMBER") @Getter @Setter private String number;
}

@Entity
@Table(name = "ONE_WAY_TICKET")
public class OneWayTicket extends Ticket {
    @Column(name = "LATEST_DEPARTURE_DATE") @Getter @Setter
    private String latestDepartureDate;
}

@Entity
@Table(name = "RETURN_TICKET")
public class ReturnTicket extends Ticket {
    @Column(name = "LATEST_RETURN_DATE") @Getter @Setter
    private String latestReturnDate;
}

Result: 2 tables created. Each table contains common columns (ID, NUMBER) and specific columns.

USE M05_EX05;
SELECT * FROM ONE_WAY_TICKET;
-- ID, NUMBER, LATEST_DEPARTURE_DATE (tout dans une seule table)
SELECT * FROM RETURN_TICKET;
-- ID, NUMBER, LATEST_RETURN_DATE (tout dans une seule table)

No indication that the information in these two tables comes from the same class hierarchy → poor support for polymorphic relationships.

5.8 Demo: Conversion (AttributeConverter)

Problem: a Java Boolean field must be stored in the database as "Yes"/"No" (varchar).

Solution: JPA 3.0 provides the jakarta.persistence.AttributeConverter<X, Y> interface with two methods:

  • convertToDatabaseColumn(X attribute) → Y
  • convertToEntityAttribute(Y dbData)
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class VipConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Yes" : "No";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Yes".equalsIgnoreCase(dbData);
    }
}
@Entity
@Table(name = "PASSENGERS")
public class Passenger {

    @Id @GeneratedValue private int id;

    @Column(name = "NAME")
    @Getter @Setter private String name;

    @Column(name = "VIP")
    @Convert(converter = VipConverter.class)
    @Getter @Setter
    private Boolean vip;
}

Result: the VIP column is of type varchar(255) in the database.

USE M05_EX06;
SELECT * FROM PASSENGERS;
-- John Smith (VIP='Yes' → Boolean true dans le programme)
-- Mike Johnson (VIP='No' → Boolean false dans le programme)

5.9 Module 5 Summary

  • Building class hierarchies with inheritance from entities and non-entities.
  • Applying mapping strategies:
  • Single Table per Class Hierarchy — a table, discriminator.
  • Join Subclass Strategy — separate tables with joins.
  • Table per Concrete Class Strategy — independent tables per concrete class.
  • Creation of AttributeConverter for conversion between Java representation and database.

6. Transaction management

6.1 Transaction concept and ACID properties

A transaction is a sequence of operations performed on a single logical unit of work. To qualify a logical unit of work as a transaction, it must have the ACID properties:

PropertyDescription
AtomicityA transaction must be an atomic unit: all its modifications are carried out, or none are.
ConsistencyAfter a transaction, all data must be in a consistent state, conforming to the defined rules (constraints, triggers, cascades).
InsulationChanges to a given transaction must be isolated from changes made by other competing transactions.
SustainabilityAfter a transaction is completed, its effects are permanently stored in the system.

6.2 The EntityTransaction interface

EntityManager is the interface used to interact with the persistence context. EntityTransaction controls transactions on entity managers in resource-local mode.

Get an instance: em.getTransaction().

EntityTransaction methods:

MethodDescription
begin()Starts a resource transaction.
commit()Commits the current transaction, writing unflushed changes to the database.
rollback()Cancels the current transaction.
setRollbackOnly()Marks the current transaction as read-only (commit impossible).
getRollbackOnly()Determines if the current transaction has been marked for rollback.
isActive()Indicates whether a resource transaction is in progress.

6.3 Demo: Working with Transactions

The application contains a Ticket entity with the id, number and price fields.

@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NUMBER")
    @Getter private String number;

    @Column(name = "PRICE")
    @Getter @Setter private double price;

    public Ticket(String number, double price) {
        this.number = number;
        this.price = price;
    }
}

Test 1 — Persist a ticket:

@Test
void runTransaction1_persist() {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();
    Ticket ticket = new Ticket("AA1234", 100.0);
    em.persist(ticket);
    em.getTransaction().commit();
    em.close();
}
// SELECT * FROM TICKET → price=100

Concurrent updates issue without lock:

  • runTransaction1 finds the ticket, changes the price to 95, sets a breakpoint (transaction not committed).
  • runTransaction2 finds ticket, changes price to 93, commits → price in BD = 93.
  • runTransaction1 resumes, commits → price in BD = 95 (overwrites T2 change).

By default, without a lock, different transactions can freely update the same row without synchronization.

6.4 Optimistic Locking

Optimistic locking assumes that data will not be modified between reading and writing. This is the most common and recommended style of locking in modern persistence solutions.

Operation:

  1. The reader reads the version as part of the entity state.
  2. The writer checks the version at update time.
  3. A version mismatch causes the transaction to be rolled back.

Advantages:

  • Maximum concurrency (no database locks).
  • Less resource consumption on the database.

Prerequisites: the entity must have a version attribute annotated with @Version, of type int, long, Integer, or java.sql.Timestamp.

Lock Modes (LockModeType):

FashionDescription
NONENo locks applied.
OPTIMISTICOptimistic locking. Prevents dirty reads and unrepeatable reads.
OPTIMISTIC_FORCE_INCREMENTIn addition to OPTIMISTIC, forces version increment even without updates.
READSynonymous with OPTIMISTIC.
WRITESynonymous with OPTIMISTIC_FORCE_INCREMENT.
PESSIMISTIC_READOther transactions can read but not update the entity.
PESSIMISTIC_WRITEOther transactions cannot read or update the entity.
PESSIMISTIC_FORCE_INCREMENTIn addition to PESSIMISTIC_WRITE, forces version increment.

Dirty read: a transaction reads data not yet committed by another transaction. Unrepeatable read: in the same transaction, successive reads of the same entity give different results because another transaction has modified the data in the meantime.

6.5 Demo: Optimistic Locking

Add a version field to the Ticket entity:

@Entity
@Table(name = "TICKETS")
@NoArgsConstructor
public class Ticket {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "NUMBER")
    @Getter private String number;

    @Column(name = "PRICE")
    @Getter @Setter private double price;

    @Version
    @Getter
    private long version;   // Géré automatiquement par Hibernate

    public Ticket(String number, double price) {
        this.number = number;
        this.price = price;
    }
}

Behavior with optimistic locking:

  1. T1 finds the ticket (price=100, version=0).
  2. T2 finds ticket (price=100, version=0), changes price to 93, commit → price=93, version=1.
  3. T1 tries to commit price=95 but version changed → RollbackException caused by OptimisticLockException.
SELECT * FROM TICKET;
-- price=93, version=1  (seule T2 a réussi)

Hibernate automatically increments version on each successful commit. The provider checks the version during each modification and throws OptimisticLockException if the version has changed.

6.6 Pessimistic Locking

Pessimistic locking prevents concurrent reads. The lock is released when the transaction commits. It reduces maximum competition. deadlocks are possible and the application is responsible for avoiding them.

When an entity instance is locked with pessimistic locking, the provider locks:

  • The line corresponding to the persistent state of this instance.
  • Rows in additional tables (case of join inheritance or secondary table).

6.7 Demo: Pessimistic Locking

Scenario 1: T1 acquires the lock, T2 fails

@Test
void runTransaction1() {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();
    Ticket ticket = em.find(Ticket.class, 1, LockModeType.PESSIMISTIC_WRITE);
    ticket.setPrice(95.0);
    // Breakpoint ici — T1 tient le verrou
    em.getTransaction().commit();
    em.close();
}

@Test
void runTransaction2() {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();
    Ticket ticket = em.find(Ticket.class, 1);  // Bloqué car T1 tient le verrou
    ticket.setPrice(93.0);
    em.getTransaction().commit();
    em.close();
}
  • T1 commits first (price=95, version=1).
  • T2 fails with RollbackException (OptimisticLockException) because T1 modified the object while T2 was in progress.

Scenario 2: T1 and T2 both use PESSIMISTIC_WRITE

  • T1 acquires the lock and stops at the breakpoint.
  • T2 tries to acquire lock and waits (stuck at SQL level).
  • T1 commit → price=95, version=1.
  • T2 finally acquires the lock, stops at breakpoint.
  • T2 commit → price=93, version=2.

Both transactions succeed, each in sequence. No data is lost.

-- Après T1 : price=95, version=1
-- Après T2 : price=93, version=2

6.8 Module 6 Summary

  • Introduction to transactions and analysis of their ACID properties.
  • Analysis of transaction control possibilities with Hibernate.
  • Demonstration of optimistic locking (via @Version) and pessimistic locking (via LockModeType).

7. Working with JPQL

7.1 Introduction to JPQL

JPQL (Java Persistence Query Language) is similar to SQL but operates on objects instead of tables, on attributes instead of columns, and on relationships instead of references.

JPQL can be used for:

  • Reading: SELECT statement.
  • Mass updates: UPDATE instruction.
  • Mass deletions: DELETE instruction.

Interfaces for queries:

InterfaceDescription
jakarta.persistence.QueryOld interface (available since JPA 1.0). Useful when the result type is unknown or for polymorphic results.
jakarta.persistence.TypedQuery<X>Extends Query. Processes results in a type-safe manner. Use when a more specific type of result is expected.

7.2 JPQL syntax and capabilities

Structure of a SELECT:

SELECT [clause]
FROM [entity_name alias]
[JOIN [entity.field] [alias]]
[WHERE [conditions]]
[GROUP BY [expressions]]
[HAVING [conditions]]
[ORDER BY [expressions]]

SELECT capabilities:

  • Can return a single object or data item, a list, or an array of objects/data.
  • The SELECT clause can contain object and attribute expressions, functions and aggregate functions, and constructors.
  • The FROM clause can contain JOIN (inner join) or LEFT JOIN (outer join).
  • The WHERE clause supports subqueries.
  • DELETE is polymorphic: all instances of subclasses meeting the criteria will be deleted.

7.3 Demo: Simple JPQL queries

Entity Passenger with id and name. The @BeforeAll method creates two passengers (John and Mike).

@Test
void testSimpleQueries() {
    EntityManager em = emf.createEntityManager();

    // Query non-typée (résultat Object)
    Query query = em.createQuery("SELECT p FROM Passenger p");
    List results = query.getResultList();

    // TypedQuery (résultat typé Passenger)
    TypedQuery<Passenger> typedQuery = em.createQuery("SELECT p FROM Passenger p", Passenger.class);
    List<Passenger> typedResults = typedQuery.getResultList();

    assertEquals(2, results.size());
    assertEquals(2, typedResults.size());

    em.close();
}

Both queries generate the same SQL: SELECT id, name FROM PASSENGERS.

7.4 Demo: Named Queries and Single Result Queries

Define Named Queries on the entity:

@Entity
@NamedQueries({
    @NamedQuery(
        name = "Passenger.findAll",
        query = "SELECT p FROM Passenger p ORDER BY p.name"
    ),
    @NamedQuery(
        name = "Passenger.findByName",
        query = "SELECT p FROM Passenger p WHERE p.name = :name"
    )
})
@Table(name = "PASSENGERS")
public class Passenger {
    @Id @GeneratedValue private int id;
    @Column(name = "NAME") @Getter @Setter private String name;
}

Using Named Queries:

@Test
void testNamedQueries() {
    EntityManager em = emf.createEntityManager();

    TypedQuery<Passenger> findAllQuery =
        em.createNamedQuery("Passenger.findAll", Passenger.class);

    TypedQuery<Passenger> findByNameQuery =
        em.createNamedQuery("Passenger.findByName", Passenger.class);
    findByNameQuery.setParameter("name", "John Smith");

    assertEquals(2, findAllQuery.getResultList().size());
    assertEquals(1, findByNameQuery.getResultList().size());

    em.close();
}

Single result queries (getSingleResult):

@Test
void testSingleQueries() {
    EntityManager em = emf.createEntityManager();

    // Requête non typée avec getSingleResult
    Query countQuery = em.createQuery("SELECT COUNT(p) FROM Passenger p");
    long count = (long) countQuery.getSingleResult();

    // TypedQuery avec getSingleResult
    TypedQuery<Long> typedCountQuery =
        em.createQuery("SELECT COUNT(p) FROM Passenger p", Long.class);
    long typedCount = typedCountQuery.getSingleResult();

    assertEquals(2L, count);
    assertEquals(2L, typedCount);

    em.close();
}

7.5 Demo: Parameterized queries

@ParameterizedTest
@ValueSource(strings = {"John Smith", "Michael Johnson"})
void testParameterizedQueries(String passengerName) {
    EntityManager em = emf.createEntityManager();

    TypedQuery<Passenger> query = em.createQuery(
        "SELECT p FROM Passenger p WHERE p.name = :name",
        Passenger.class
    );
    query.setParameter("name", passengerName);

    assertEquals(passengerName, query.getSingleResult().getName());

    em.close();
}

The test runs twice: once with “John Smith”, once with “Michael Johnson”. The :name parameter is injected by @ValueSource.

7.6 Demo: Update and Delete with JPQL

UPDATE and DELETE queries must be executed in a transaction via executeUpdate().

@AfterAll
void cleanup() {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();

    // Update en masse
    Query updateQuery = em.createQuery(
        "UPDATE Passenger p SET p.name = 'Unknown'"
    );
    int updatedRows = updateQuery.executeUpdate();
    assertEquals(2, updatedRows);

    // Delete en masse
    Query deleteQuery = em.createQuery("DELETE FROM Passenger p");
    int deletedRows = deleteQuery.executeUpdate();
    assertEquals(2, deletedRows);

    em.getTransaction().commit();
    em.close();
}

executeUpdate() returns the number of rows updated or deleted.

7.7 Module 7 Summary

  • Review of JPQL and its capabilities (read, update, delete).
  • Demonstration of various queries: simple, named, single result, parameterized, update/delete.

8. The Criteria API

8.1 Introduction to the Criteria API

The Criteria API is an alternative to JPQL for defining queries on objects. It is especially useful for building dynamic queries whose exact structure is only known at runtime.

Central interface: CriteriaBuilder

  • Serves as the main factory for criteria queries and criteria query elements.
  • Get a CriteriaBuilder: em.getCriteriaBuilder().

8.2 Demo: Working with the Criteria API

Entities used: Flight (id, number) and Passenger (id, name, flight).

Test 1 — Simple query:

@Test
void testSimpleCriteriaBuilder() {
    EntityManager em = emf.createEntityManager();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Passenger> cq = cb.createQuery(Passenger.class);
    Root<Passenger> root = cq.from(Passenger.class);
    cq.select(root);

    TypedQuery<Passenger> query = em.createQuery(cq);
    List<Passenger> passengers = query.getResultList();

    assertEquals(2, passengers.size());

    em.close();
}

Generated SQL selects from PASSENGERS and joins with FLIGHT.

Test 2 — Parameterized query:

@ParameterizedTest
@ValueSource(strings = {"John Smith", "Michael Johnson"})
void testParameterizedCriteria(String passengerName) {
    EntityManager em = emf.createEntityManager();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Passenger> cq = cb.createQuery(Passenger.class);
    Root<Passenger> root = cq.from(Passenger.class);

    // Ajout d'une clause WHERE sur le nom
    cq.select(root).where(cb.equal(root.get("name"), passengerName));

    TypedQuery<Passenger> query = em.createQuery(cq);
    List<Passenger> result = query.getResultList();

    assertEquals(1, result.size());

    em.close();
}

Test 3 — SELECT DISTINCT:

Both passengers have the same flight. We want to recover separate flights.

@Test
void testDistinctSelect() {
    EntityManager em = emf.createEntityManager();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Flight> cq = cb.createQuery(Flight.class);
    Root<Passenger> root = cq.from(Passenger.class);

    // Sélection distincte sur l'attribut "flight"
    Selection<Flight> flightSelection = root.get("flight");
    cq.select(flightSelection).distinct(true);

    TypedQuery<Flight> query = em.createQuery(cq);
    List<Flight> flights = query.getResultList();

    // Les deux passagers ont le même vol → 1 seul vol distinct
    assertEquals(1, flights.size());

    em.close();
}

The generated SQL includes DISTINCT and a join between PASSENGERS and FLIGHT.

8.3 Demo: Go further with the Criteria API

Test 4 — Multiselect with several conditions:

@Test
void testMultiSelect() {
    EntityManager em = emf.createEntityManager();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    // CriteriaQuery sur un tableau d'objets (plusieurs colonnes)
    CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
    Root<Passenger> root = cq.from(Passenger.class);

    // Sélectionner le numéro de vol et le nom du passager
    Selection<String> flightNumber = root.get("flight").get("number");
    Selection<String> passengerName = root.get("name");

    cq.multiselect(flightNumber, passengerName)
      .distinct(true)
      .where(
          cb.and(
              cb.isNotNull(root.get("flight").get("number")),
              cb.isNotNull(root.get("name"))
          )
      );

    TypedQuery<Object[]> query = em.createQuery(cq);
    List<Object[]> results = query.getResultList();

    // 2 passagers avec leurs numéros de vol
    assertEquals(2, results.size());

    em.close();
}

Test 5 — Join:

@Test
void testJoin() {
    EntityManager em = emf.createEntityManager();

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
    Root<Passenger> root = cq.from(Passenger.class);

    // Jointure LEFT entre Passenger et Flight
    Join<Passenger, Flight> flightJoin = root.join("flight", JoinType.LEFT);

    // Sélectionner le numéro de vol (depuis la jointure) et le nom du passager (depuis root)
    cq.multiselect(flightJoin.get("number"), root.get("name"));

    TypedQuery<Object[]> query = em.createQuery(cq);
    List<Object[]> results = query.getResultList();

    // Afficher tous les éléments
    for (Object[] row : results) {
        System.out.println("Flight: " + row[0] + ", Passenger: " + row[1]);
    }

    assertEquals(2, results.size());

    em.close();
}

The generated SQL includes a LEFT JOIN between PASSENGERS and FLIGHT on FLIGHT_ID.

8.4 Course Summary

Module 8 Summary:

  • Introduction to the CriteriaBuilder interface.
  • Demonstration of query construction via the Criteria API: simple, parameterized queries, SELECT DISTINCT, SELECT with several conditions, and join operations.

Course conclusions:

This course covered the most important concepts of Hibernate with comprehensive demonstrations:

  1. ORM and JPA — Introduction and demonstration of advantages and disadvantages. Analysis of object-relational impedance mismatch.
  2. Entities — Mapping of tables to POJOs, entity access types, mapping of database objects, definition of primary keys.
  3. Entity Relationships — Relationship types (one-to-one, one-to-many, many-to-one, many-to-many), one-way and two-way relationships, relationship creation.
  4. Entity Inheritance — Inheritance hierarchies, mapping strategies (Single Table, Joined, Table per Class).
  5. Transactions — Transaction management, optimistic locking and pessimistic locking for concurrent updates.
  6. JPQL — Object queries via JPQL.
  7. Criteria API — Dynamic queries via the Criteria API.


Search Terms

java · se · deep · dive · hibernate · fundamentals · backend · architecture · full-stack · web · class · entities · entity · jpql · relationships · api · criteria · locking · primary · single · strategy · classes · embeddable · embedding

Interested in this course?

Contact us to book it or get a custom training plan for your team.