15 Object-Relational Mapping (ORM) (Draft)
The persistence of complex data structures in a program can be achieved using a data-base. The Object-Oriented model and the Relational models, while similar in some aspects have a few incompatible aspects – usually called mistmatches) – that make the integration difficult. While low-level interaction through queries is possible – using the Java DataBase Connectivity (JDBC) API – modern applications adopt an adaptation layer called Object-Relational Mapping (ORM) that makes code simpler and allow flexibility and optimization.
15.1 Persistence and Direct data access
15.1.1 Persistence
Persistence is the property of data that can outlive the current process lifespan, i.e. when a program is started it will have data from previous work sessions available.
For Java developers, this means we would like the state of certain objects to live beyond the scope of the JVM, so that the same state is available in a different JVM, or even in a different program written in a different programming language.
The key requirement for persistence is that data must survive program execution. Simple program use in-memory data which is stored in RAM and lost when the program stops. Persistent data need to be stored on disk, databases, or external storage device. The key differences are: volatility, lifespan, and accessibility. In-memory data is volatile and disappears when power is lost or the application terminates, while persistent data remains intact across sessions. The lifespan of in-memory data is limited to the duration of program execution, whereas persistent data can exist indefinitely. Finally, in-memory data is accessible only within the running process, while persistent data can be shared across different applications, users, and even platforms, making it essential for long-term data storage and multi-user systems.
15.1.2 Object/Relational Mismatch
The incompatibilities between the relational data model compared object-oriented paradigm are usually known ad object/Relational Mismatch or impedance mismatch:
- the relational model represents data in a tabular format with references based on values,
- object-oriented languages represent data as an interconnected graph of objects with inheritance and polymorphism.
The most important mismatches consist in:
- Granularity Object models often have more classes than tables in the database.
- Subtypes (inheritance) The relational model doesn’t have inheritance as a first-class construct, There are standard ways to represent subtyping.
- Identity The relational model defines exactly one notion of “sameness”: the primary key. Java defines both object identity a==b and value equality a.equals(b). It is common to have more than one object in a given Java program representing the same row of a database table.
- Associations Associations are represented as unidirectional references in Java Relational databases treat all associations as bidirectional and represent them via foreign keys Bidirectional relationship in Java require code on both sides of the association Just by looking at the Java code we cannot be sure of the multiplicity of an association
- Data navigation In Java, we navigate associations from one object to the next, walking the object network. That is inefficient when retrieving relational data The goals is to minimize the number of SQL queries
15.1.3 Object-Relational Mapping (ORM)
Object-Relational Mapping (ORM) is a technique for managing database interactions using object-oriented programming, converting data between a relational database and the memory.
An ORM solution:
- provides an abstraction layer that decouples business logic from database access,
- translates objects in the application into database records and vice-versa,
- automates data conversion between object models and relational schemas,
- reduces (in most cases eliminates) the need for direct SQL queries for basic operations,
- automates query generation and result mapping to objects,
- reduces manual connection and transaction management.
The alternative to ORM is Direct Data Access through the JDBC API:
- Direct Data Access
- Uses native SQL to interact with the database
- Directly embeds SQL in the application code
- Manually handles every relationship and transaction
- ORM
- Uses object-based operations to interact with the database
- SQL queries can be automatically generated based on object models
- Simplifies complex relationships and data handling
ORM Pros:
- Reduces the need to insert SQL within our programming language
- Improves code maintainability and readability
- Reduces development time by automating database interactions
- Supports multiple databases with minimal changes
ORM Cons:
- Less control over query optimization and performance (but it is possible to use direct SQL queries)
- Potential overhead compared to manually optimized SQL
- Learning curve for ORM frameworks and configurations
15.2 JPA Mapping
Jakarta Persistence API (JPA), formerly Java Persistence API, is a Java specification for managing relational data using an object-oriented approach. It defines a standard way to map Java objects to database tables, manage entity lifecycle, and execute queries.
JPA itself does not provide an implementation; it relies on providers such as Hibernate to handle persistence operations.
JPA provides two approaches for defining entity mappings: annotation based (modern approach) and XML-based (mostly for legacy systems), in this chapter we will focus on annotations approach.
JPA annotations are applied at different levels:
- Entity-Level: defines an entity and its table mapping
- Field-Level: defines the mapping of individual fields to database columns
- Relation-Level: defines associations between entities
15.2.1 Entity
An entity is a purely structural component that represents a persistent object that is mapped to a database table. It is a fundamental building block of the application’s domain model and serves as an abstraction over relational database records.
Entities encapsulate only data structure and constraints, without containing business logic. Each entity instance corresponds to a row in a relational table. An entity defines fields (mapped to table columns) and relationships with other entities. Must have at least one primary key to ensure uniqueness.
By defining entities, we model the granularity of the object domain, ensuring a structured representation of data in the application.
The following annotations at the class level can be used to define entity mapping:
@EntityMarks a class as a JPA entity.The class must have a no-argument constructor with at least package visibility.
If not specified, the table name defaults to the class name.
@Table(name, indexes...)Specifies the database table on which the entity will be mappedname(String, optional): the name of the db tableindexes(Index[], optional): array of the indexes of the table
@Index(name, columnList, unique)Indexes are defined within the@Tableannotationname: (String) the name of the indexcolumnList: (String) comma-separated list of column names that will be included in the index.unique: (boolean, default: false) whether the index enforces uniqueness across all indexed columns. If multiple columns are indexed together, their combination must be unique
@Inheritance(strategy)specifies the inheritance strategy for an entity hierarchystrategy(Enum:InheritanceType):
Relational databases do not support subtypes (inheritance) natively, so ORM provides different strategies to map class hierarchies to tables. There are three main mapping strategies:
- Single Table Inheritance (
SINGLE_TABLE) – One table stores all subclasses, with a type discriminator column, this is the most efficient strategy but wastes space; - Joined Table Inheritance (
JOINED) – Each class has its own table, linked through foreign keys, normalizes data but adds complexity; - Table Per Class (
TABLE_PER_CLASS) – Each class has a separate table with all its attributes; avoids duplication but makes querying harder.
@DiscriminatorColumn(name, discriminatorType, length)Used withSINGLE_TABLEinheritance to distinguish entity typesname(String, default:"DTYPE"): name of the discriminator columndiscriminatorType(Enum:DiscriminatorType): type of the columnSTRING|CHAR|INTEGER,length(Integer, default: 31): max length for STRING type
@DiscriminatorValue(value)Used withSINGLE_TABLE, defines the value stored in the discriminator column for a specific entityvalue(String): discriminator value for this entity
@MappedSuperclass. Defines a non-entity superclass, the class itself does not correspond to a database table.It can be used to share mapped fields among subclasses. Its annotated fields (
@Id,@Column, etc.) are inherited and mapped into the tables of concrete entity subclasses. Ideal for shared attributes such as id, timestamps, audit fields, or other common metadata. With this annotation, inheritance exists only on the Java side, there is no inheritance structure in the database —- i.e. no join, no discriminator column –.
15.2.2 Fields
Each entity field corresponds to a database column and can have the constraints and properties typical of fields in a relational database schema.
Data type conversion between object model and relational model:
Stringin code →VARCHARin databasebooleanin code →TINYINT(1)in some databases
Some languages support richer types than relational databases, ORM provides mapping rules for correct conversion. Custom type mapping possible if default conversions are not sufficient.
The fields can be annotated with the following annotations:
@IdDeclares the field as the primary key (PK) that establish the identity of an entity instance; it defines entity uniqueness. Can be auto-generated (e.g., auto-increment, UUID) or manually assigned.@GeneratedValue(strategy)Define an a PK (marked with@Id) as autogenerated. It can specify which different generationstategy(Enum:GenerationType) is adopted:AUTO: default, ORM decides the best strategyIDENTITY: uses database auto-incrementSEQUENCE: uses a database sequenceTABLE: uses a table-based sequence
@Column(name, nullable, length, unique, insertable, updatable)Details the mapping of a field to a database column, overriding the default one:name(String, optional): the name of the column in the mapping tablenullable(boolean, default: false): if true, allows null valueslength(int, default: 255): defines the maximum length for a String fieldunique(boolean, default: false): if true, enforces a unique constraintinsertable(boolean, default: true): if false, prevents insertionupdatable(boolean, default: true): if false, prevents updates
@TransientExcludes a field from persistence, thus the field will not be mapped to a column. By default all attributes in an entity class are mapped to fields in the corresponding table.@Enumerated(value)Maps an enumerative type to a database column. The parametervaluedefines how defines how it is stored in the db:ORDINAL: stores enum positions (0,1,2…)STRING: stores enum names as strings
@LobMarks a field as a large object (BLOB/CLOB).@Temporal(value)Specifies the temporal type of ajava.util.Dateorjava.util.Calendar. Parametervalue: (Enum: TemporalType) defines how the date/time is storedDATE: Stores only the date (without time).TIME: Stores only the time (without date).TIMESTAMP: Stores both date and time
15.2.3 Relations
Relationships define the associations of the object-oriented model, how entities are linked in the relational model.
Four main types of relationships:
- One-to-One (1:1) (
@OneToOne): each instance of the entity is associated with exactly one instance of the related entity; - Many-to-One (N:1) (
@ManyToOne): multiple instances of the entity are associated with a single instance of the related entity; - One-to-Many (1:N) (
@OneToMany): one instance of the entity is related to multiple instances of the related entity; - Many-to-Many (N:N) (
@ManyToMany): entities are related in a many-to-many fashion, requiring a join table.
JPA manages relationships using foreign keys and ensures data consistency using entity associations. Foreign keys are automatically managed by JPA based on relationship annotations, customization is allowed by using @JoinColumn (One-to-One, Many-to-One) or @JoinTable (Many-to-Many).
When defining relationships it is often useful to distinguish between Owning vs. Inverse side:
- the owning side is the entity that stores the foreign key,
- the inverse side is the entity that is referenced.
Another importan distinction is between Bidirectional vs. Unidirectional relationships:
- Unidirectional: only the owning side is aware of the relationship
- Bidirectional: both sides are aware of the relationship; the inverse side uses
mappedByto refer to the field on the owning side.
The fields that implement an association and will be mapped to a relationship in the relational model can be annotated with the following annotations:
@OneToOne(cascade, fetch, mappedBy, orphanRemoval, optional)cascade(Enum: CascadeType[], optional): defines the cascading strategyfetch(Enum: FetchType, optional, default: EAGER): defines the fetch strategymappedBy(String): specifies the field in the owning entity that maps this relationship. Used only on inverse sideorphanRemoval(boolean, default: false): if true, removes related entities when they are no longer referencedoptional(boolean, default: true): if false, enforces a non-null foreign key constraint
@ManyToOne(cascade, fetch, optional)cascade(CascadeType[], optional): defines the cascading strategyfetch(FetchType, optional, default: EAGER): defines the fetch strategyoptional(boolean, default: true): if false, enforces a non-null foreign key constraint
@OneToMany(mappedBy, cascade, fetch, orphanRemoval)mappedBy(String, required): specifies the field in the owning entity that manages the foreign keycascade(Enum: CascadeType[], optional): defines the cascading strategyfetch(Enum: FetchType, optional, default: LAZY): defines the fetch strategyorphanRemoval(boolean, default: false): if true, removes orphaned entities automatically
@JoinColumncannot be used with@OneToManybecause the foreign key is in the “many” side (@ManyToOne) It usesmappedByto indicate that the relationship is managed by the “many” side@ManyToMany(cascade, fetch)cascade(CascadeType[], optional): defines the cascading strategyfetch(FetchType, optional, default: LAZY): defines the fetch strategy
@JoinTablecan be used to specify the table and foreign key mappings@JoinColumn(name, referencedColumnName, nullable, unique, insertable, updatable)used on the owning side with One-to-One, Many-to-One, to define the foreign key, by default, JPA automatically names the foreign key column as{fieldName}_idname(String, optional): defines the foreign key column namereferencedColumnName(String, optional): the column name in the referenced entity’s tablenullable(boolean, default: true): if false, prevents NULL valuesunique(boolean, default: false): if true, enforces uniquenessinsertable(boolean, default: true): if false, prevents insert operationsupdatable(boolean, default: true): if false, prevents updates
It can be used on the owning side only, the inverse side of the relationship uses
mappedByto reference the owning entity and avoid an extra foreign key@JoinTable(name, joinColumns, inverseJoinColumns)Used with@ManyToManyrelationships to define a join table, by default JPA automatically creates a join tablename(String, required): name of the join tablejoinColumns(Array of@JoinColumn): defines foreign key columns for the owning entityinverseJoinColumns(Array of@JoinColumn): defines foreign key columns for the inverse entity
15.2.3.1 Fetch strategy
The Fetch (or loading) strategy defines how the related entities are loaded and thus determines the data navigation in the model.
Navigating relationships in ORM requires fetching data efficiently. The two primary data loading strategies are:
- Lazy Loading (
LAZY): related data is only loaded when explicitly accessed it improves initial performance but may cause multiple queries. - Eager Loading (
EAGER): related data is loaded immediately with the entity. It reduces query count but can load unnecessary data
Choosing the right strategy depends on performance trade-offs and data access patterns
15.2.3.2 Cascading strategy
Cascading allows propagation of operations (e.g., persist, update, delete) from a parent entity to its related entities.
ORMs let you configure cascading per operation type:
- Persist: saves related entities when the parent is saved.
- Merge: updates related entities when the parent is updated.
- Remove: deletes related entities when the parent is deleted.
- Detach: detaches related entities when the parent is removed from the persistence context.
- Refresh: reloads related entities when the parent is refreshed.
Cascade strategies:
- Full cascade: applies all operations to related entities.
- Selective cascade: allows only specific operations to be propagated.
- No cascade: relationships must be managed manually.
Cascading simplifies persistence management but must be configured carefully to avoid unintended side effects.
All relationship annotations share the:
cascade(Enum:CascadeType[]): defines how persistence operations affect related entities. It accepts an array of CascadeType values, meaning you can specify different cascade behaviors for different operations:ALL: applies all operations to related entitiesPERSIST: saves child entities when the parent is savedMERGE: updates child entities when the parent is updatedREMOVE: deletes child entities when the parent is deletedREFRESH: reloads child entities when the parent is refreshedDETACH: detaches child entities when the parent is detached
15.2.4 Example
@Entity
public class User {
@Id
@Size(min = 16, max = 16)
private String cf;
private String name;
private Integer age;
@OneToMany(mappedBy = "customer")
private List<Order> orders = new ArrayList<>();
User(){}
// ...
}@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private User customer;
private String status;
Order(){}
//...
}15.3 JPA Persistence
15.3.1 Persistence Unit
JPA provides an abstraction layer over JDBC (Java DataBase Connectivity, the Java native DB connector), handling database connections and transactions. Entities are automatically managed in a persistence context, allowing seamless database interaction. JPA does not directly interact with the database; instead, it relies on a persistence provider (such as Hibernate) to execute queries and manage transactions
A Persistence Unit represents a logical configuration that groups:
- the database connection settings
- the list of managed entities
- transaction handling strategy
- JPA provider-specific settings
Each JPA application must declare at least one Persistence Unit. The Persistence Unit is the entry point for all JPA operations. The Persistence Unit is created when the application starts and remains available for database interactions.
persistence.xml file is the configuration file required to define how JPA interacts with the database. This file can include provider-specific properties that are not defined by JPA but are used to configure additional features of the chosen JPA provider.
Each Persistence Unit is defined by:
- a unique name
- the database connection settings (JDBC URL, driver, credentials)
- the transaction handling strategy (transaction-type):
RESOURCE_LOCAL: transactions are managed manually usingEntityTransaction,JTA: transactions are managed externally by a container.
- List of managed entities (optional if entities are defined with annotations and are in the classpath)
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_1.xsd"
version="3.1">
1 <persistence-unit name="sample">
<properties>
2 <property name="jakarta.persistence.jdbc.driver"
value="org.h2.Driver"/>
3 <property name="jakarta.persistence.jdbc.url"
value="jdbc:h2:mem:sampledb;DB_CLOSE_DELAY=-1"/>
4 <property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
5 <property name="jakarta.persistence.schema-generation.database.action"
value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>- 1
- the unique name of the unit
- 2
- JDBC driver used for connection
- 3
- JDBC connection string, with database name
- 4
- database account details
- 5
- Schema generation policy
ORM frameworks provide automatic schema generation policies based on the defined entities. Schema generation policy define how schema creation should be managed by the JPA provider. The possible policies are:
create: Generates the schema if it does not existdrop-and-create: Deletes and recreates the schema on each run (useful for testing)update: Modifies the schema to match entity changes (may cause issues with existing data)validate: Checks if the schema matches the entity definitions but does not modify itnone: The ORM does not perform any schema validation or modification
Choosing the right policy:
- Development: Create or Drop-and-Create for rapid iteration,
- Production: None or Validate to prevent unintended schema changes.
The EntityManagerFactory loads configuration from persistence.xml and initializes the JPA provider (e.g. Hibernate). It is a heavyweight object, meant to be created once per application and reused. It must always be closed when the application shuts down. It is responsible for managing persistence units and creating EntityManager instances
- 1
-
loads the configuration of the persistence unit
myPersistenceUnitdefined inpersistence.xml - 2
-
creates the
EntityManagerobject that defines a session.
15.3.2 Session and EntityManager
A Session (or persistence context) is a temporary environment where an ORM tracks and manages entities during their lifecycle. It acts as an intermediary between the application and the database, ensuring that objects remain synchronized with their corresponding records in the database. Its scope and lifespan must be carefully managed to avoid performance issues.
Session key features:
- Manages the identity and lifecycle of entities within an application session
- Keeps track of changes made to managed entities
- Automatically synchronizes data between in-memory objects and the database
- Reduces the need for explicit SQL statements, leveraging automatic persistence
- Provides a caching mechanism, reducing unnecessary database queries
- Ensures consistency within a transaction, committing or rolling back changes
Session operations:
- Opening a session – Creates a new persistence context
- Persisting an entity – Registers a new entity for saving
- Fetching an entity – Retrieves an entity from the database into the session
- Updating an entity – Modifies an entity already tracked by the session
- Detaching an entity – Stops tracking an entity without deleting it
- Flushing changes – Writes all pending updates to the database
- Closing a session – Ends the persistence context and detaches all entities
When an entity is retrieved using an ORM query, it is automatically added to the session, its state is tracked, and any modifications will be synchronized with the database when the session is flushed or committed.
Entities transition through different states during their lifecycle in an ORM Main entity states:
- Transient – The entity is created in memory but not yet persisted
- Persistent – The entity is associated with a database session and will be saved
- Detached – The entity is no longer managed by the ORM but still exists in memory
- Removed – The entity is marked for deletion and will be removed from the database
These operations modify the state of an entity, when performed
- Creating a new instance (e.g.
new EntityClass()) starts in Transient state - Persisting the entity (e.g.,
save()orpersist()) transitions it to Persistent - Closing or clearing the session detaches the entity (Detached)
- Deleting the entity moves it to the Removed state
If the same entity is requested again during the same session, the ORM will return the existing in-memory instance rather than executing another database query.
When executing a native SQL query that retrieves data not directly mapped to a known entity, the results are not added to the session. In such cases, the ORM treats the output as raw data, and no automatic tracking or synchronization occurs.
Detaching an entity means removing it from the session while keeping it in memory. Once detached, the ORM no longer tracks changes made to the entity, and these modifications will not be reflected in the database unless the entity is reattached to a session.
Detaching is not removing. Removing an entity means marking it for deletion from the database. It is still tracked by the session until the session is flushed or committed, at which point the corresponding record is physically deleted from the database.
Detaching only affects the ORM’s ability to track changes, while removing permanently deletes the entity’s data from the database.
Detaching can be useful when dealing with large datasets to reduce memory consumption, whereas removing is necessary when an entity - and all its information - is no longer needed in the system.
EntityManager is the implementation of the concept of session and is the basis of all querying operations in JPA.
It is the main interface for interacting with the database in JPA. It manages the lifecycle of entities and executes queries using both JPQL and native SQL It is a lightweight, short-lived object, typically created per transaction or request When obtained directly from the EntityManagerFactory, it must be explicitly closed with close() to release resources.
It provides the core API for:
Entity lifecycle management:
persist(Object entity): registers a new entity to be inserted into the database when a transaction is committedmerge(Object entity): updates an existing entity or reattaches a detached entity to the persistence contextdetach(Object entity): removes the given entity from the persistence contextremove(Object entity): marks an entity for deletion from the database when the transaction is committed
In addition there are two operation that allow synchronizing the in-memory content with the database:
flush(): write to the database all the pending changes without waiting for the transaction commit,refresh(): updated the in-memory objects with the latest changes present in the database, possibly overwriting attributes.
Note that any operation that modify data in the database requires an active transaction.
Querying: supports different query methods allowing structured, reusable, dynamic, or raw SQL queries to retrieve data
find(Class<T>, Object primaryKey): retrieves a single entity by its primary key
For more complex queries,
EntityManagerprovides methods that return a Query object, which allows customization and execution of different types of queries.Operations that read data do not require an active transaction
Transaction management: explicitly controls transactions when a RESOURCE_LOCAL transaction type is used.
getTransaction(): creates a transaction object
Session can be managed as:
- short-lived: A new session is created for each operation (or web request) and closed immediately after. This ensures that entities are not kept in memory longer than necessary. Prevents excessive memory consumption and avoids issues such as long-running transactions. Recommended for most applications, especially web-based systems where each request is stateless.
- long-lived: The session remains open for a longer period, potentially throughout a user session or across multiple operations. This allows entities to be reused without repeated database queries. However, it increases the risk of memory leaks, concurrency issues, and stale data. Can be useful in specific cases, such as batch processing or maintaining user state in desktop applications.
Use short-lived sessions by default unless there is a clear need for long-lived sessions. Avoid keeping a session open longer than necessary to reduce memory consumption.
Flush and commit changes regularly to maintain database consistency.
Be cautious with lazy loading when using detached entities, as it may trigger unexpected database queries outside of an active session. Modern ORM frameworks manage session automatically. Manual session management can be necessary to optimize performances
15.3.3 Transaction
JPA operates in a transactional context: modifications to the database must be executed within a transaction.
A Transaction is a sequence of one or more database operations that are executed as a single unit of work. Transactions, following the ACID properties, ensure data integrity by guaranteeing that either all operations within the transaction are successfully completed or none of them take effect. This prevents situations where partial updates could leave the database in an inconsistent state.
Transaction key features:
- Transactions ensure data integrity by grouping multiple operations together.
- A transaction must be explicitly committed to persist changes or rolled back to undo them.
- If an error occurs during a transaction, the system can abort all changes to maintain database consistency.
- Transactions typically operate within an active session, meaning they are tied to the ORM’s persistence context.
Transaction operations:
- Begin – Starts a new transaction.
- Commit – Saves all changes made within the transaction permanently.
- Rollback – Reverts all changes made within the transaction.
- Savepoint – Creates a checkpoint within a transaction to allow partial rollbacks.
- Isolation Level Configuration – Defines how transactions interact with each other to manage concurrent access to data.
Transactions must be explicitly started and committed to apply changes. If an error occurs, the transaction can be rolled back to maintain data integrity.
In a multi-user database system, multiple transactions can be executed concurrently. The most common concurrency issues are:
- Dirty Reads – A transaction reads uncommitted changes from another transaction.
- Non-Repeatable Reads – A transaction reads the same row twice but gets different values because another transaction modified it in between.
- Phantom Reads – A transaction executes the same query twice and gets different results because another transaction inserted or deleted rows.
Isolation Levels define how transactions interact with each other and how data consistency is maintained in concurrent operations. A lower isolation level increases performance but increases the risk of incurring in concurrency issues, while a higher isolation level ensures stronger data integrity but may reduce performance.
The possible isolation levels are:
Read Uncommitted
Transactions can read uncommitted changes from other transactions (dirty reads allowed). Highest concurrency, lowest data consistency.
Read Committed
A transaction can only read committed changes (no dirty reads). However, non-repeatable reads and phantom reads can still occur.
Repeatable Read
Prevents dirty reads and non-repeatable reads by ensuring that a row read within a transaction does not change until the transaction completes.
Phantom reads may still occur.
Serializable
The strictest level: transactions are fully isolated from each other by locking rows or entire tables. Prevents all concurrency issues but significantly reduces parallelism.
Isolation level depends on the application’s performance and consistency requirements. Typically high-performance application use Read Committed, while data-critical applications adopt Serializable.
Modern ORM frameworks manage transactions automatically Manual transaction management can be necessary to optimize performances
Keep transactions as short as possible to avoid locking resources for too long. Always handle rollback scenarios to prevent data inconsistencies in case of failures. Use proper isolation levels based on the application’s concurrency requirements.
EntityTransaction is the interface that represents a database transaction in JPA. EntityManager allows to retrieve a transaction via the method getTransaction() Its implementation is provided by the JPA provider. It provides methods to control transaction flow:
begin():starts a new transaction,isActive(): checks if the transaction is currently running,commit(): saves all changes made since the transaction began,rollback(): discards all changes if an error occurs.
15.4 JPA Querying
15.4.1 Querying in ORM
ORMs generate SQL automatically based on the structure of the defined entities. Basic queries can be constructed using entity attributes, allowing developers to work at the object level without writing SQL.
Simple queries are fully managed by the ORM (e.g., finding by primary key, inserting, updating, deleting). ORMs optimize queries by dynamically translating object-oriented queries into SQL tailored for the underlying database.
Key advantages:
- Minimizes direct SQL interaction, reducing complexity
- Ensures consistency by enforcing entity relationships and constraints
- Improves code readability and maintainability by abstracting low-level database interactions
When basic queries are not enough, ORMs offer more advanced techniques:
- Criteria API & Query Builders – Programmatic query construction
- Joins and custom queries – Fetching related data efficiently
- Native SQL queries – Direct SQL execution when necessary
Performance considerations:
- Use pagination and indexing to improve query speed
- Avoid N+1 query problems with proper relationship fetching (e.g., JOIN FETCH, batch fetching)
- Cache frequently accessed data
JPA provides multiple ways to query and manipulate data in a relational database:
- JPQL (Java Persistence Query Language): object-oriented query language like SQL but based on JPA entities instead of tables
- Named Queries: precompiled queries defined inside JPA entities for improved performance and reusability
- Criteria API: programmatic approach to dynamically build type-safe queries
- Native Queries: direct execution of raw SQL queries for advanced operations and database-specific optimizations
Each method serves a specific use case:
- JPQL and Named Queries: suitable for structured, static queries
- Criteria API: useful for dynamic queries with runtime conditions
- Native Queries: necessary when JPQL is insufficient or when using database-specific functions
15.4.2 Query interface
Query is the JPA interface used to execute queries. Its implementation is provided by the JPA provider. TypedQuery<T> is a query subtype, it ensures type safety by specifying the return type.
Created through EntityManager, it represents a compiled query ready for execution Supports different query types: JPQL, Named Queries, Native Queries Criteria API follows a different approach to query definition, using a programmatic, type-safe construction mechanism instead of string-based queries
EntityManager provides methods to create untyped generic Query objects. For these Query objects result type is not enforced at compile time, so manual casting may be required:
createQuery(String qlString): creates a JPQL querycreateNamedQuery(String name): retrieves a predefined Named QuerycreateNativeQuery(String sqlString): creates a query using raw SQLcreateNativeQuery(String sqlString, String resultSetMapping): executes a native query using a custom mapping
EntityManager also provides methods to create TypedQuery<T> objects. This allows the query to return the specified object when executed, ensures type safety, reducing the need for manual casting:
createQuery(String qlString, Class<T>): creates a typed JPQL query and maps results to entitiescreateNativeQuery(String sqlString, Class<T>): creates a typed native query and maps results to entitiescreateNamedQuery(String name, Class<T>): retrieves a predefined Named Query and maps results to entities
The Query interface provides methods to configure and customize queries before execution. Parameters can be assigned using named or positional parameters. Date and time values require a TemporalType to specify their granularity.
setParameter(String name, Object value): assigns a value to a named parametersetParameter(int position, Object value): assigns a value to a positional parametersetParameter(String name, Date value, TemporalType temporalType): assigns a date/time parameter with a specific TemporalTypesetParameter(int position, Date value, TemporalType temporalType): same as above, but for positional parameters
Once a query is configured, it must be executed to retrieve results These are the most common method that execute a query and get the results
getResultList(): retrieves multiple results; return type depends on the query type and operation:getResultStream(): similar to the previous, but it returns a Stream instead of a ListgetSingleResult(): retrieves a single result; return type depends on the query type and operation:executeUpdate(): executes UPDATE or DELETE queries; returns the number of affected rows
The result type depends on the query type and on the selection it performs
TypedQuery<T>returns objects of the specified typeT- Standard
QueryreturnsObjectorObject[]when selecting multiple fields - Partial field projections return
Object[](each element corresponds to a selected field) - Aggregates (SUM, COUNT, etc.) return a
Number, but the actual type depends on the database. Explicit casting is required
The execution method (getSingleResult(), getResultList(), …) determines if the result is a single item or a collection of iems
Pagination allows limiting the number of results and setting an offset
setMaxResults(int maxResults): limits the number of resultssetFirstResult(int startPosition): sets the offset for pagination
15.4.3 Java Persistence Query Language (JPQL)
JPQL (Java Persistence Query Language) is the query language defined by JPA to retrieve and manipulate entity data. It is similar to SQL, but operates on JPA entities and their attributes instead of database tables and columns. JPQL queries are translated into SQL by the JPA provider, making them database-independent.
Supports SELECT, UPDATE, DELETE operations, but does not allow INSERT statements. Entities are persisted (i.e. rows are inserted) using EntityManager.persist() method.
JPQL enables navigating entity relationships using JOIN, FETCH, and path expressions. Queries can be statically defined (Named Queries) or dynamically created at runtime.
JPQL – Query Structure
Queries must use entity class names and field names, not database-specific identifiers. The alias is required when using conditions or sorting. Joins are based on entity relationships instead of foreign keys, this enables a type-safe navigation through associations
Example:
SELECT u : <1>
FROM User u : <2>
JOIN u.orders o : <3>
WHERE o.status = 'SHIPPED' : <4>
ORDER BY u.name ASC : <5>- select all the colums (like User.* in SQL)
- define an alias since used in sorting
- the join uses the relation field in the class and defines an alias since uses it in a condition
- condition on a column (same as in SQL)
- ordering on a column (same as in SQL)
Supported operators in JPQL:
- Comparison operators:
=,<>,<,>,<=,>= - Logical operators:
AND,OR,NOT LIKEoperator: pattern matching (LIKE '%value%')INoperator: checks if a value is in a list (IN ('A', 'B', 'C'))BETWEENoperator: checks range (BETWEEN 10 AND 20)IS NULL/IS NOT NULL
JPQL supports two types of query parameters, which allow values to be dynamically assigned at runtime instead of hardcoding them in the query:
positional parameters (
?1,?2)Query query = em.createQuery("SELECT u FROM User u WHERE u.age > ?1 AND u.name = ?2"); query.setParameter(1, 30); query.setParameter(2, "Bob");named parameters (
:paramName)TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.age > :age", User.class); query.setParameter("age", 25);15.4.4 Named queries
Named Queries are predefined, reusable JPQL queries declared at the entity level. They allow better performance because the query is compiled once and cached. Named Queries help centralize query definitions, making the code more maintainable.
They are defined using the @NamedQuery annotation in the entity class Useful for static queries that do not change dynamically at runtime Multiple @NamedQuery on a single entity should be grouped using @NamedQueries to ensures cleaner and more organized code.
In fact some Java versions or JPA implementations do not support multiple standalone @NamedQuery annotations.
Named queries example:
@Entity
@NamedQueries({
@NamedQuery(name = "User.findByAge", query = "SELECT u FROM User u WHERE u.age = :age"),
@NamedQuery(name = "User.findMinors", query = "SELECT u FROM User u WHERE u.age < 18")
})
public class User {
@Id private String cf;
private String name;
private int age;
}Named Queries are executed using EntityManager.createNamedQuery() The returned Query or TypedQuery<T> object is used to set parameters and execute the query.
Query query = em.createNamedQuery("User.findByAge");
query.setParameter("age", 25);
List<User> users = query.getResultList();
TypedQuery<User> query = em.createNamedQuery("User.findMinors", User.class);
List<User> minors = query.getResultList();Native queries allows executing raw SQL directly on the database. They are used when JPQL is insufficient or when leveraging database-specific functions.
Main difference from JPQL:
- Use table and column names instead of entity and field names
- Queries are written in pure SQL, not JPQL
- Created via
createNativeQuery(), instead ofcreateQuery()
Parameter binding remains the same as JPQL, using setParameter(). Execution is performed using the same methods as JPQL queries
15.5 Repository Pattern
The Repository Pattern is an architectural pattern that provides an abstraction layer between the application’s business logic and the database. It defines a dedicated component responsible for retrieving, persisting, and managing entities, ensuring that data access operations are encapsulated and decoupled from other parts of the system.
Repository key features:
- Acts as an intermediary between the ORM and business logic.
- Encapsulates data access logic within a dedicated component.
- Keeps database operations separate from business logic.
- Provides a structured interface for querying and modifying entities, improving code maintainability.
- Centralizes data access in one place, reducing code duplication and enforcing consistency.
- Supports standard data access patterns, including CRUD operations, query methods, and custom queries.
- Improves testability by abstracting persistence, making it easier to mock DB interactions or repositories entirely.
Repositories provide a standard set of methods to manage entities in the database. The four primary operations (CRUD)in a repository are:
- Create – Inserts a new entity into the database.
- Read – Retrieves entities by ID or search criteria.
- Update – Modifies an existing entity and saves changes.
- Delete – Removes an entity from the database.
Using a repository for CRUD operations ensures a consistent API across the application.
Repositories leverage ORM capabilities to query the database using entity attributes. Query methods allow filtering and retrieving entities based on their attributes, reducing the need to write explicit SQL.
Queries can be composed dynamically, combining multiple conditions within repository methods:
findByNameAndEmail (name, email): Finds an entity based on its name and email attribute.existsByEmail(String email): Checks if an entity with a given email exists
Using repository query methods ensures cleaner and more maintainable business logic, keeping data access centralized.
When query methods are not enough, repositories can define custom query methods. Custom query methods are useful for:
- Complex filtering with advanced multiple conditions
- Aggregation queries (e.g., count, sum, averages)
- Joins and nested queries involving multiple entities
- Custom queries can be written using
- ORM query languages (e.g., JPQL, TypeORM Query Builder)
- Native SQL queries, for performance optimizations
While custom queries provide flexibility, they should be used carefully to maintain abstraction. If the result structure of a custom query does not match an entity, raw data (tuples) may be returned instead, causing a loss of abstraction
When handling large datasets, repositories provide mechanisms for efficient pagination and sorting.
- Pagination: limits the number of results returned per request to improve performance. Example strategies: limit/offset, cursor-based pagination.
- Sorting: orders results based on specified fields. Example: findAll(Sort.by(“name”).ascending()).
Proper use of pagination and sorting prevents excessive memory consumption and speeds up query execution.
15.6 Hibernate & H2
JPA is designed to be abstract, it requires:
- a JPA provider, implementing all the methods and managing the persistence unit,
- a Database, that will contain the data.
In the course we use Hibernate and H2 respectively for the two roles.
15.6.1 Hibernate
Hibernate is an Object-Relational Mapping (ORM) framework for Java that simplifies database interactions by allowing developers to work with Java objects instead of writing SQL queries manually.
It fully implements the Jakarta Persistence API (JPA), meaning it can function as a pure JPA provider, following all JPA specifications without requiring additional features. However, Hibernate is more than just a JPA provider. When integrated into higher-level frameworks such as Spring or Jakarta EE, it extends JPA with advanced capabilities such as caching, batch processing, and more efficient query execution Hibernate as JPA Provider.
When used as a pure JPA provider, Hibernate strictly adheres to the JPA specification:
- uses JPA interfaces, such as
EntityManagerFactoryandEntityManager, to handle persistence. - manages transactions through
EntityTransaction - provides its implementation of all the interfaces defined by JPA, making them available to work with.
- supports JPQL and Criteria API as query languages
- reads standard
persistence.xmlconfiguration, allowing easy portability across different JPA providers
When integrated with higher-level frameworks like Spring or Jakarta EE, Hibernate extends its basic JPA implementation with additional features, including:
- Sessions: a flexible and powerful alternative to EntityManager with automatic transaction control capabilities
- HQL (Hibernate Query Language): a more powerful alternative to JPQL with additional flexibility
- Advanced Caching Mechanisms: first-level cache (always active) and optional second-level cache for optimized performance
- Batch Processing: efficient handling of bulk data operations to improve write performance
- Event Listeners and Interceptors: hooks that allow executing custom logic during entity lifecycle events
- Flexible Fetch Strategies: advanced options for lazy and eager loading to optimize database queries
- NoSQL Support: ability to interact with NoSQL databases
Although Hibernate works as a pure JPA provider, it also provides additional properties that enhance performance and database interaction. Some database-related settings are better optimized using Hibernate properties. Hibernate adds dialect support, which allows it to optimize SQL for different DBMS. Schema generation via Hibernate is faster and more efficient than the standard JPA mechanism.
Key Hibernate-Specific Properties:
hibernate.dialect: defines the database type, allowing Hibernate to generate optimized SQLhibernate.hbm2ddl.auto: handles schema generation with more flexibility than JPA’s standard optionhibernate.show_sql: enables logging of SQL statements executed by Hibernatehibernate.format_sql: formats SQL output for better readability Caching
Hibernate automatically enables first-level caching, even when used as a JPA provider. Entities retrieved within the same transaction are stored in the persistence context, avoiding redundant queries.
Hibernate supports lazy loading using proxy objects, even when acting as a JPA provider. When an entity is marked as FetchType.LAZY, Hibernate does not load the related entity immediately. Hibernate automatically loads lazy associations when accessed, unless the session is closed. It does not require explicit calls to JOIN FETCH to load lazy associations.
Common issue: LazyInitializationException
Occurs if an entity with a lazy-loaded relation is accessed outside an active transaction. Hibernate expects the session to be open when accessing proxies.
15.6.2 H2
H2 is an open-source relational database written in Java, designed to be lightweight, fast, and easy to integrate into Java applications. It requires no installation and supports standard SQL, JDBC, and in-memory operations, making it a flexible solution for both development and testing environments.
H2 is fully compatible with JPA and ORM frameworks, allowing seamless integration with Hibernate. It also includes a web-based console that enables developers to debug queries and inspect the database structure efficiently.
This database supports three different execution modes:
In-Memory Mode
The database exists only in RAM and is lost when the application stops. Ideal for unit tests and temporary data storage
JDBC URL:
jdbc:h2:mem:testdbEmbedded Mode
The database is stored in a local file accessible only from the same Java process. Suitable for small standalone applications.
JDBC URL:
jdbc:h2:file:./data/mydbServer Mode
H2 runs as a separate process, allowing multiple clients to connect. Can be accessed remotely via TCP or Web API.
Useful when multiple applications need to share the same database
JDBC URL:
jdbc:h2:tcp://localhost/~/mydb
The H2 database includes a built-in web-based management interface. It provides access to the database for executing queries, modifying data, and managing schemas. Works similarly to traditional database workbenches but runs in a web browser. Useful for debugging, testing, and database administration
To start the console manually:
java -jar h2.jarAlternatively is is possible to programmatically start the console server in Java:
Server.createWebServer("-web").start();The console runs by default on port 8082: http://localhost:8082
There are a few basic properties that must be configure in Hibernate to operate with an H2 databse: These properties must be set in persistence.xml:
Database Connection
Driver and Dialect
Schema Management Controls automatic table creation and migration SQL Logging Enables logging of executed queries for debugging and performance analysis Configuration
Database Connection: defines how the application connects to H2
jakarta.persistence.jdbc.driver = org.h2.Driver jakarta.persistence.jdbc.url = jdbc:h2:mem:testdb jakarta.persistence.jdbc.user = sa (default value) jakarta.persistence.jdbc.password = (empty by default)The JDBC URL determines whether the database runs in memory, as a file, or as a server. The JDBC driver must be specified to allow the application to interact with H2.
Dialect and Schema Management (implemented by hibernate)
hibernate.dialect = org.hibernate.dialect.H2Dialect hibernate.hbm2ddl.auto = updateThe SQL dialect helps Hibernate generate efficient database-specific queries.
The property
hibernate.hbm2ddl.autohas a set of available valuesnone: no automatic schema generation. The database schema must be created manually.validate: verifies that the database schema matches the entity definitions but does not create or modify tables. It throws an error if the schema is incorrectupdate: updates the schema without dropping existing tables, it adds missing columns or constraints but does not remove them.create: drops existing tables and creates the schema from scratch every time the application starts.create-drop: creates the schema from scratch every time the application starts drops it when the session factory is closed.
SQL Logging (implemented by hibernate)
hibernate.show_sql = true hibernate.format_sql = true
15.7 Implementation of an ORM-based application
In this section, we will implement a simple ORM-based application using Hibernate and H2 step-by-step
- Setting up the environment
- Persistence configuration
- Handling EntityManager
- Defining entities
- Implementing repositories
- Implementing a main executor for our application
15.7.1 Dependencies import
First step is to define the pom.xml file of the application, to import the necessary dependencies.
- jakarta.persistence-api: JPA API for ORM
- hibernate-core: Hibernate as the JPA provider
- h2: the database
In the pom.xml:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.0.Final</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>15.7.2 Persistence configuration
In persistence.xml we must set up
- persistence unit
- JPA provider (Hibernate)
- database connection
- driver, URL and access credentials
- sql dialect
- automatic schema generation
- logging and debugging features
The persistence.xml must be placed in the resources/META-INF folder.
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1">
<persistence-unit name="flightPU">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.url"
value="jdbc:h2:file:./data/flightdb"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect"
value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>15.7.3 EntityManager handling
To avoid creating multiple EntityManagerFactory instances, we use a singleton utility class JPAUtil. The advantages is that it avoids redundant factory instances since EntityManagerFactory is expensive to create.
This class must offer some core features - Ensure proper resource management Providing a close() method to shut down Hibernate properly when the - application ends its lifecycle - Encapsulate EntityManagerFactory The factory is not exposed directly, but a controlled method provides access to EntityManager Allows application logic to interact only through EntityManager, ensuring proper lifecycle management
public class JPAUtil {
private static final EntityManagerFactory emf =
Persistence.
createEntityManagerFactory("flightPU");
public static EntityManager getEntityManager() {
return emf.createEntityManager();
}
public static void close() {
if (emf.isOpen()) {
emf.close();
}
}
}15.7.4 Entity definition
In this step we must create the data model of our application:
- Entities will be defined using JPA annotations: how Java objects and their attributes are mapped to database tables and columns.
- Relationships: how Java objects are related and database data are linked across tables
15.7.4.1 Entities
@Entity @Table(name = "airplanes")
public class Airplane {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String model;
@Column(nullable = false)
private String airline;
private int capacity;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private AirplaneStatus status;
}15.7.4.2 OneToOne relationships
A One-to-One relationship links two entities where each instance of one entity is associated with exactly one instance of another entity By default, One-to-One relationships are FetchType.EAGER Owner side It contains the @JoinColumn annotation because it holds the foreign key Hibernate considers this entity responsible for managing the relationship Inverse side It uses mappedBy to reference the relationship without creating an extra foreign key column It defines cascading and orphanRemoval behavior When querying the inverse side, Hibernate will JOIN the owner table to retrieve the associated object OneToOne relationships
Owner side:
@Entity
@Table(name = "passports")
public class Passport {
@Id
@Size(min = 8, max = 8)
private String number;
@Column(nullable = false)
private String nationality;
@OneToOne
@JoinColumn(name = "person_id",
nullable = false)
private Person person;
}Inverse side:
@Entity
@Table(name = "people")
public abstract class Person {
// ...
@OneToOne(mappedBy = "person",
cascade = CascadeType.ALL,
orphanRemoval = true)
private Passport passport;
}15.7.4.3 OneToMany/ManyToOne relationships
A One-to-Many (@OneToMany) means that one entity is related to multiple instances of another entity. A Many-to-One (@ManyToOne) means that multiple instances of one entity reference a single instance of another entity. These two annotations represent the same database relationship, just seen from different perspectives.
@ManyToOne - Owner Side This side contains the foreign key column in the database This is where the relationship is actually managed Default FetchType.EAGER
@Entity
@Table(name = "flights")
public class Flight {
//...
@ManyToOne
@JoinColumn(name="airplane_id",
nullable=false)
private Airplane airplane;
}@OneToMany - Inverse Side Uses mappedBy to reference the owner Does not create an additional foreign key column Default FetchType.LAZY OneToMany/ManyToOne relationships
@Entity
@Table(name = "airplanes")
public class Airplane {
// ...
@OneToMany(mappedBy = "airplane")
private Set<Flight> flights;
}15.7.4.4 ManyToMany relationships
A Many-to-Many relationship means that multiple instances of one entity are associated with multiple instances of another entity This relationship is represented by a join table, which stores the associations between the two entities JPA automatically creates the join table with two foreign keys Default FetchType: LAZY (on both sides)
Owner Side
@Entity @Table(name = "flights")
public class Flight {
// ...
@ManyToMany
@JoinTable(
name = "flight_person",
joinColumns = @JoinColumn(name = "flight_id"),
inverseJoinColumns = @JoinColumn(name = "person_id")
)
private Set<Person> passengers = new HashSet<>();
}The entity that defines the @JoinTable annotation owns the relationship. It explicitly specifies the join table name and the foreign key columns.
Inverse side
@Entity
@Table(name = "people")
public class Person {
//...
@ManyToMany(mappedBy = "passengers")
private Set<Flight> flights = new HashSet<>();
}Inverse side uses mappedBy to reference the owner side. Does not create the join table, as it is already managed by the owner ManyToMany relationships.
15.7.5 Repository
JPA does not define a repository concept, and neither does Hibernate when used as a pure JPA provider. This means that repository classes must be implemented manually to handle entity persistence.
The main purpose of a repository is to limit direct access to EntityManager, ensuring a structured and maintainable approach. Instead of passing EntityManager throughout the application, repositories encapsulate database operations, exposing only controlled methods for data access.
15.7.5.1 Generic Repository
Since JPA does not define a repository concept, a Generic Repository is a way to standardize common database operations. A GenericRepository class defines reusable methods using Java Generics, making it adaptable to different entity types. It allows to centralize basic CRUD operations for any entity type, avoiding code duplication. Create, update, and delete operations can be fully generalized, as they follow the same persistence logic for any entity. The only generic read operations, independent of entity-specific condition, are search by id and unconditioned search. This approach ensures that basic persistence logic is implemented once, and specific repositories can extend it for more advanced queries.
public class GenericRepository<T, ID> {
private final Class<T> entityClass;
protected GenericRepository(Class<T> entityClass) {
this.entityClass = entityClass;
}
public void update(T entity) {
EntityManager em = JPAUtil.getEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
em.merge(entity);
tx.commit();
em.close();
}
public void delete(ID id) {
EntityManager em = JPAUtil.getEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
T entity = em.find(entityClass, id);
if (entity != null) {
em.remove(entity); }
tx.commit();
em.close();
}
public void save(T entity) {
EntityManager em = JPAUtil.getEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(entity);
tx.commit();
em.close();
}
public T findById(ID id) {
EntityManager em = JPAUtil.getEntityManager();
T entity = em.find(entityClass, id);
em.close();
return entity;
}
public List<T> findAll() {
EntityManager em = JPAUtil.getEntityManager();
List<T> result = em.createQuery("SELECT e FROM "
+ entityClass.getSimpleName()
+ " e", entityClass)
.getResultList();
em.close();
return result;
}
}15.7.5.2 Typed Repository
While a Generic Repository provides common CRUD operations for any entity, a Typed Repository is a specialized implementation for a specific entity type.
It extends the Generic Repository and allows defining custom queries tailored to the entity’s attributes and business logic. A Typed Repository enables more complex data retrieval using JPQL, Criteria API, or native SQL queries, without exposing raw EntityManager operations in the application logic. This approach ensures a structured separation of concerns, keeping generic persistence logic in the Generic Repository and custom queries in the Typed Repository.
public class FlightRepository extends GenericRepository<Flight, Long> {
public FlightRepository() {
super(Flight.class);
}
public List<Flight> findByAirplaneModel(String model) {
EntityManager em = JPAUtil.getEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Flight> query = cb.createQuery(Flight.class);
Root<Flight> root = query.from(Flight.class);
Join<Flight, Airplane> airplaneJoin = root.join("airplane");
query.select(root)
.where(cb.equal(airplaneJoin.get("model"), model));
List<Flight> flights = em.createQuery(query).getResultList();
em.close();
return flights;
}
}15.7.6 Main executor
The Main Executor serves as the entry point for the application, handling entity persistence and data manipulation in the database. It is necessary to explicitly close EntityManagerFactory when the application shuts down. A shutdown hook is registered to ensure that Hibernate properly releases resources at the end of execution. Note that in frameworks like Spring or Jakarta EE, this process is managed automatically it must be handled manually.
public class DatabaseInitializer {
private static final GenericRepository<Airplane, Long> airplaneRepository = new GenericRepository<>(Airplane.class) {};
private static final FlightRepository flightRepository = new FlightRepository();
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(JPAUtil::close));
DatabaseInitializer.initialize();
}
public static void initialize() {
Airplane airplane1 = createAirplane("Boeing 737", "Airways", 180, AirplaneStatus.IN_SERVICE);
Airplane airplane2 = createAirplane("Airbus A320", "Sky Airlines", 160, AirplaneStatus.IN_SERVICE);
airplaneRepository.save(airplane1);
airplaneRepository.save(airplane2);
Flight flight1 = createFlight("FL1234", "2024-04-01T10:00", "2024-04-01T13:00", airplane1);
Flight flight2 = createFlight("FL5678", "2024-04-02T12:00", "2024-04-02T16:00", airplane2);
flightRepository.save(flight1);
flightRepository.save(flight2);
}
}15.8 Testing JPA Applications
To test an application using an ORM:
- Use a PU dedicated to testing using drop-and-create policy
- Each test should start with well defined database contents
- Cleanup is required to avoid poisoning successive test cases
- Use different data (especially keys) in different tests Tests may be executed in parallel so they should not interfere in insert, delete, and updated operations.
Entities are simple data holders, representing the structure of the database
They do not contain business logic, only fields, relationships, and metadata (e.g., constraints)
Testing them is not necessary, as their correctness is enforced by
- The ORM itself, which ensures mappings are correctly applied
- The database schema, which enforces constraints (e.g., foreign keys, uniqueness)
- Other application tests, where entities are implicitly used and validated
The persistence unit in test can be a in-memory db to reduce test time. The database must be recreate often, on every test session. Enable logging to make debugging easier.
<property name="jakarta.persistence.jdbc.url"
value="jdbc:h2:mem:testuniversitydb;DB_CLOSE_DELAY=-1"/>
<property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>A typical set-up and tear-down of tests when tests do no write to db
@BeforeAll
static void populateDb() {
JPAUtil.setTestMode();
//…
}
@AfterAll
static void cleanDb() {
JPAUtil.close();
}While, in a settings where test cases need to write to the db, it must be reset before each test to avoid interference.
@BeforeEach
static void populateDb() {
JPAUtil.setTestMode();
//…
}
@AfterEach
static void cleanDb() {
JPAUtil.close();
}This latter configuration is much more expensive in terms of computation and time, so it should be adopted when strictly needed.