Table of Contents
- 2.1 ORM, JPA and Hibernate — Fundamental Concepts
- 2.2 Advantages of JPA and Hibernate
- 2.3 Potential disadvantages
- 2.4 Object-Relational Impedance Mismatch
- 2.4.1 The granularity problem
- 2.4.2 The inheritance problem
- 2.4.3 The Identity Problem
- 2.4.4 The problem of associations (Associations)
- 2.4.5 The Data Navigation problem
- 2.5 Demo: Create a project, Entity Classes and persistent objects
- 2.6 Module 2 Summary
- 3.1 What is an Entity?
- 3.2 Table mapping annotations
- 3.3 Demo: Secondary Table with a single field
- 3.4 Demo: Secondary Table with several fields
- 3.5 Demo: Multiple Secondary Tables
- 3.6 Entity Access Type
- 3.7 Entity Primary Keys and Entity Identity
- 3.8 Demo: Primary Keys with @GeneratedValue
- 3.9 Demo: Embeddable Primary Key and Embedded ID
- 3.10 Demo: Embeddable Primary Key and ID Class
- 3.11 Module 3 Summary
- 4.1 Relationship types and directions
- 4.2 Demo: One-to-many and Many-to-one relationships
- 4.3 Demo: Many-to-many Relations
- 4.4 Annotations for defining relationships
- 4.5 Demo: Join Tables on a column
- 4.6 Demo: Join Tables on multiple columns
- 4.7 Classes Embeddable
- 4.8 Demo: Embedding classes in Entities
- 4.9 Demo: Embedding class collections in Entities
- 4.10 Demo: Embedding class maps in Entities
- 4.11 Module 4 Summary
- 5.1 Introduction to Entity inheritance
- 5.2 Demo: Extend an Entity
- 5.3 Demo: Extend a non-Entity (MappedSuperclass)
- 5.4 Mapping Strategies
- 5.5 Demo: Single Table per Class Hierarchy
- 5.6 Demo: Joined Subclass Strategy
- 5.7 Demo: Table per Concrete Class Strategy
- 5.8 Demo: Conversion (AttributeConverter)
- 5.9 Module 5 Summary
- 6.1 ACID transaction concept and properties
- 6.2 EntityTransaction interface
- 6.3 Demo: Working with transactions
- 6.4 Optimistic Locking
- 6.5 Demo: Optimistic Locking
- 6.6 Pessimistic Locking
- 6.7 Demo: Pessimistic Locking
- 6.8 Module 6 Summary
- 7.1 Introduction to JPQL
- 7.2 JPQL syntax and capabilities
- 7.3 Demo: Simple JPQL queries
- 7.4 Demo: Named Queries and single-result queries
- 7.5 Demo: Parameterized queries
- 7.6 Demo: Update and Delete with JPQL
- 7.7 Module 7 Summary
- 8.1 Introduction to the Criteria API
- 8.2 Demo: Working with the Criteria API
- 8.3 Demo: Go further with the Criteria API
- 8.4 Course summary
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-coredependency also addsJakarta Persistence APIas 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 valuecreaterecreates the database from scratch on each execution. For an incremental update, useupdate.
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:
@Entitymarks 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.@Idmarks 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 toPassenger, mapped by theairportfield ofPassenger.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 thePASSENGERStable.@OneToMany(mappedBy = "passenger"): a passenger can have several tickets, mapped by thepassengerfield ofTicket.
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 theEntityManagerFactoryusing the persistence-unit name defined inpersistence.xml.emf.createEntityManager()creates theEntityManager, 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
@Entityor 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 declaringfinalclasses 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 implementingSerializable. - Enums.
- Collections of primitive types, embeddable types, or entity types.
3.2 Table mapping annotations
Annotations that specify table mapping:
| Annotation | Description |
|---|---|
@Table | Specifies the primary table for the annotated entity. If absent, the table name corresponds to the name of the entity class. |
@SecondaryTable | Specifies a secondary table for the entity. If absent, all persistent fields are mapped to the primary table. |
@SecondaryTables | Specifies 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 inpkJoinColumns.
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
@Idannotation 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 access | Property-based access |
|---|---|
| Allows you to omit getters for unexposed fields | Accessor methods can perform additional logic |
| Fields declared on a single line, better readability | Useful 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
@EmbeddedIdor@IdClass.
Rules for composite primary keys:
- Must be public with a public constructor with no arguments.
- Must be serializable.
- Must define the
equals()andhashCode()methods. - Logical equality semantics must be consistent with database equality.
- 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)
@Embeddableindicates that the class will be embedded in another entity. It must implementSerializablebecause 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:
| Type | Description |
|---|---|
@OneToMany | An entity is associated with several others |
@ManyToOne | Multiple entities are associated with a single |
@ManyToMany | Several entities are associated with several others |
@OneToOne | An 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-onerelationship, 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
mappedByattribute.
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
| Annotation | Description |
|---|---|
@JoinTable | Specifies the cross-reference table for mapping a relationship. Must be specified on the owner side. |
@JoinColumn | Specifies the column for joining an entity association. |
@JoinColumns | Defines 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 UNIONfor 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)→ YconvertToEntityAttribute(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
AttributeConverterfor 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:
| Property | Description |
|---|---|
| Atomicity | A transaction must be an atomic unit: all its modifications are carried out, or none are. |
| Consistency | After a transaction, all data must be in a consistent state, conforming to the defined rules (constraints, triggers, cascades). |
| Insulation | Changes to a given transaction must be isolated from changes made by other competing transactions. |
| Sustainability | After 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:
| Method | Description |
|---|---|
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:
runTransaction1finds the ticket, changes the price to 95, sets a breakpoint (transaction not committed).runTransaction2finds ticket, changes price to 93, commits → price in BD = 93.runTransaction1resumes, 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:
- The reader reads the version as part of the entity state.
- The writer checks the version at update time.
- 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):
| Fashion | Description |
|---|---|
NONE | No locks applied. |
OPTIMISTIC | Optimistic locking. Prevents dirty reads and unrepeatable reads. |
OPTIMISTIC_FORCE_INCREMENT | In addition to OPTIMISTIC, forces version increment even without updates. |
READ | Synonymous with OPTIMISTIC. |
WRITE | Synonymous with OPTIMISTIC_FORCE_INCREMENT. |
PESSIMISTIC_READ | Other transactions can read but not update the entity. |
PESSIMISTIC_WRITE | Other transactions cannot read or update the entity. |
PESSIMISTIC_FORCE_INCREMENT | In 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:
- T1 finds the ticket (price=100, version=0).
- T2 finds ticket (price=100, version=0), changes price to 93, commit → price=93, version=1.
- T1 tries to commit price=95 but version changed →
RollbackExceptioncaused byOptimisticLockException.
SELECT * FROM TICKET;
-- price=93, version=1 (seule T2 a réussi)
Hibernate automatically increments
versionon each successful commit. The provider checks the version during each modification and throwsOptimisticLockExceptionif 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 (viaLockModeType).
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:
SELECTstatement. - Mass updates:
UPDATEinstruction. - Mass deletions:
DELETEinstruction.
Interfaces for queries:
| Interface | Description |
|---|---|
jakarta.persistence.Query | Old 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
SELECTclause can contain object and attribute expressions, functions and aggregate functions, and constructors. - The
FROMclause can containJOIN(inner join) orLEFT JOIN(outer join). - The
WHEREclause supports subqueries. DELETEis 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
:nameparameter 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
PASSENGERSand joins withFLIGHT.
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
DISTINCTand a join betweenPASSENGERSandFLIGHT.
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 JOINbetweenPASSENGERSandFLIGHTonFLIGHT_ID.
8.4 Course Summary
Module 8 Summary:
- Introduction to the
CriteriaBuilderinterface. - Demonstration of query construction via the Criteria API: simple, parameterized queries,
SELECT DISTINCT,SELECTwith several conditions, and join operations.
Course conclusions:
This course covered the most important concepts of Hibernate with comprehensive demonstrations:
- ORM and JPA — Introduction and demonstration of advantages and disadvantages. Analysis of object-relational impedance mismatch.
- Entities — Mapping of tables to POJOs, entity access types, mapping of database objects, definition of primary keys.
- Entity Relationships — Relationship types (one-to-one, one-to-many, many-to-one, many-to-many), one-way and two-way relationships, relationship creation.
- Entity Inheritance — Inheritance hierarchies, mapping strategies (Single Table, Joined, Table per Class).
- Transactions — Transaction management, optimistic locking and pessimistic locking for concurrent updates.
- JPQL — Object queries via JPQL.
- 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