Advanced Java

JPA & Hibernate

Master Object-Relational Mapping

What is JPA?

Java Persistence API (JPA) is a specification for Object-Relational Mapping (ORM)


JPA is:


JPA is NOT:

Why JPA?

Without ORM:


With JPA:

JPA Implementations

Several implementations exist:

Implementation Description
Hibernate Most popular, feature-rich
EclipseLink Reference implementation
OpenJPA Apache project

We'll use Hibernate - the most widely adopted ORM framework

Setting Up JPA with Hibernate

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.4.1.Final</version>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- src/main/resources/META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
  <persistence-unit name="my-unit">
    <properties>
      <property name="jakarta.persistence.jdbc.url"
                value="jdbc:postgresql://localhost:5432/mydb"/>
      <property name="jakarta.persistence.jdbc.user" value="user"/>
      <property name="jakarta.persistence.jdbc.password" value="password"/>
      <property name="hibernate.dialect"
                value="org.hibernate.dialect.PostgreSQLDialect"/>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
      <property name="hibernate.show_sql" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

Bootstrapping the EntityManager

Spring Boot hides this. In pure JPA you create it yourself:

import jakarta.persistence.*;

// Create ONCE per application - expensive, thread-safe
EntityManagerFactory emf =
    Persistence.createEntityManagerFactory("my-unit");

// Create per unit of work - cheap, NOT thread-safe
EntityManager em = emf.createEntityManager();
try {
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    em.persist(new User("alice@example.com"));

    tx.commit();
} finally {
    em.close();
}

emf.close(); // on application shutdown

Creating Your First Entity

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "email", nullable = false, unique = true)
    private String email;
    
    @Column(name = "full_name", length = 100)
    private String fullName;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    // Constructors, getters, setters
}

JPA Annotations Explained


Generation Strategies:

One-to-Many Relationship

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Book> books = new ArrayList<>();
}

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    
    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
}

Many-to-Many Relationship

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

@Entity
public class Course {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

Cascade Types

Control how operations propagate to related entities:

Cascade Type Description
PERSIST Propagate persist operation
MERGE Propagate merge operation
REMOVE Propagate remove operation
REFRESH Propagate refresh operation
DETACH Propagate detach operation
ALL All of the above

Fetch Strategies

LAZY Loading (Default for collections):

@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;

Data loaded only when accessed


EAGER Loading (Default for single entities):

@ManyToOne(fetch = FetchType.EAGER)
private Author author;

Data loaded immediately with parent entity


Best Practice: Use LAZY by default, load eagerly only when needed

The DAO Pattern with EntityManager

Without Spring Data, you write a thin data access object:

public class UserDao {

    private final EntityManager em;

    public UserDao(EntityManager em) {
        this.em = em;
    }

    public void save(User user) {
        em.persist(user);
    }

    public User findById(Long id) {
        return em.find(User.class, id);
    }

    public User findByEmail(String email) {
        return em.createQuery(
                "SELECT u FROM User u WHERE u.email = :email", User.class)
            .setParameter("email", email)
            .getSingleResult();
    }

    public List<User> findAll() {
        return em.createQuery("SELECT u FROM User u", User.class)
            .getResultList();
    }
}

EntityManager Core Operations

The EntityManager is the central JPA interface:

Method Description
persist(entity) Insert a new entity (transient to managed)
find(Class, id) Look up by primary key
merge(entity) Re-attach and update a detached entity
remove(entity) Delete a managed entity
refresh(entity) Reload state from the database
flush() Synchronize the context to the database
createQuery(jpql) Build a JPQL query

JPQL Query Patterns

JPQL operates on entities and fields, not tables and columns:

Pattern JPQL
Equality SELECT u FROM User u WHERE u.email = :email
AND / OR ... WHERE u.name = :name AND u.email = :email
BETWEEN ... WHERE u.createdAt BETWEEN :start AND :end
Comparison ... WHERE u.age > :age
LIKE ... WHERE u.name LIKE :pattern
ORDER BY ... ORDER BY u.name ASC

List<User> users = em.createQuery(
        "SELECT u FROM User u WHERE u.age > :age", User.class)
    .setParameter("age", 18)
    .getResultList();

JPQL, Native and Named Queries

// JPQL
List<User> users = em.createQuery(
        "SELECT u FROM User u WHERE u.email LIKE :domain", User.class)
    .setParameter("domain", "%@example.com")
    .getResultList();

// Native SQL
List<User> recent = em.createNativeQuery(
        "SELECT * FROM users WHERE created_at > ?1", User.class)
    .setParameter(1, date)
    .getResultList();

// Modifying query - inside an active transaction
int updated = em.createQuery(
        "UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
    .setParameter("date", date)
    .executeUpdate();

// DTO projection with a constructor expression
List<UserDTO> dtos = em.createQuery(
        "SELECT new com.example.dto.UserDTO(u.id, u.email) FROM User u",
        UserDTO.class)
    .getResultList();

Declare reusable queries with @NamedQuery on the entity, then call em.createNamedQuery(...).

Pagination and Sorting

JPA paginates with setFirstResult and setMaxResults:

public class UserService {

    private final EntityManager em;

    public UserService(EntityManager em) {
        this.em = em;
    }

    public List<User> getUsers(int page, int size) {
        return em.createQuery(
                "SELECT u FROM User u ORDER BY u.fullName ASC", User.class)
            .setFirstResult(page * size)
            .setMaxResults(size)
            .getResultList();
    }

    public long countUsers() {
        return em.createQuery(
                "SELECT COUNT(u) FROM User u", Long.class)
            .getSingleResult();
    }
}

Entity Lifecycle

Entity Lifecycle

Transaction Management

In Java SE you drive the EntityTransaction manually:

public void createUser(User user) {
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    try {
        tx.begin();
        em.persist(user);
        tx.commit();          // flush and commit
    } catch (RuntimeException e) {
        if (tx.isActive()) {
            tx.rollback();    // any exception rolls back
        }
        throw e;
    } finally {
        em.close();
    }
}

public User findUser(Long id) {
    EntityManager em = emf.createEntityManager();
    try {
        User user = em.find(User.class, id);   // read-only, no tx needed
        if (user == null) throw new UserNotFoundException(id);
        return user;
    } finally {
        em.close();
    }
}

In a Jakarta EE container, JTA can manage transactions for you instead.

The N+1 Problem

The Problem:

// 1 query to get all authors
List<Author> authors = em.createQuery(
        "SELECT a FROM Author a", Author.class).getResultList();

// N queries - one for each author's books!
for (Author author : authors) {
    System.out.println(author.getBooks().size());
}

The Solution:

// Use JOIN FETCH to load everything in one query
List<Author> authors = em.createQuery(
        "SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books",
        Author.class).getResultList();

// Or use an EntityGraph as a fetch hint
EntityGraph<Author> graph = em.createEntityGraph(Author.class);
graph.addAttributeNodes("books");

List<Author> result = em.createQuery(
        "SELECT a FROM Author a", Author.class)
    .setHint("jakarta.persistence.fetchgraph", graph)
    .getResultList();

Performance Best Practices

1. Use Appropriate Fetch Strategy

2. Batch Operations

// Set hibernate.jdbc.batch_size, then flush and clear periodically
for (int i = 0; i < users.size(); i++) {
    em.persist(users.get(i));
    if (i % 50 == 0) {   // release managed entities to free memory
        em.flush();
        em.clear();
    }
}

3. Use Projections

// Load only needed fields with a constructor expression
List<UserDTO> dtos = em.createQuery(
        "SELECT new com.example.UserDTO(u.id, u.email) FROM User u",
        UserDTO.class).getResultList();

Second-Level Cache

Add the Hibernate JCache integration with an Ehcache provider:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

<!-- persistence.xml -->
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.region.factory_class" value="jcache"/>

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User { }

Database Migration with Flyway

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

Place migration files under src/main/resources/db/migration/ and run them standalone:

Flyway flyway = Flyway.configure()
    .dataSource("jdbc:postgresql://localhost:5432/mydb", "user", "password")
    .load();
flyway.migrate();

-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    full_name VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Set hibernate.hbm2ddl.auto to validate so Flyway owns the schema.

Auditing with Lifecycle Callbacks

Pure JPA tracks timestamps with @PrePersist and @PreUpdate:

@MappedSuperclass
public abstract class Auditable {

    @Column(updatable = false)
    private LocalDateTime createdDate;

    private LocalDateTime lastModifiedDate;

    @PrePersist
    public void onCreate() {
        createdDate = LocalDateTime.now();
        lastModifiedDate = createdDate;
    }

    @PreUpdate
    public void onUpdate() {
        lastModifiedDate = LocalDateTime.now();
    }
}

@Entity
public class User extends Auditable {
    // User fields
}

Hibernate also offers @CreationTimestamp and @UpdateTimestamp as shortcuts.

Exercise 1: Create Entity Model

Task: Design a blog application data model


Requirements:

  1. Create User entity (id, username, email, password)
  2. Create Post entity (id, title, content, createdAt)
  3. Create Comment entity (id, text, createdAt)
  4. Add One-to-Many: User → Posts
  5. Add One-to-Many: Post → Comments
  6. Add Many-to-One: Comment → User
  7. Add appropriate indexes

Exercise 2: Repository Queries

Task: Implement complex queries


Requirements:

  1. Find posts by author username
  2. Find posts created in date range
  3. Find posts with comments count > 10
  4. Find top 10 most commented posts
  5. Implement pagination for posts
  6. Create custom DTO projection

Exercise 3: Optimize Performance

Task: Identify and fix N+1 queries


Requirements:

  1. Enable SQL logging
  2. Find N+1 query problems
  3. Fix with JOIN FETCH or an EntityGraph fetch hint
  4. Implement second-level cache
  5. Add database migration with Flyway
  6. Measure performance improvements

Common Pitfalls

1. LazyInitializationException

Accessing a lazy association after the EntityManager is closed. Fetch it within the persistence context, use JOIN FETCH, or map to a DTO.

2. Leaking EntityManagers

Always close() the EntityManager in a finally block to release the connection.

3. Writing Without a Transaction

persist, merge and remove require an active EntityTransaction.

4. Too Many Queries

Monitor SQL with hibernate.show_sql=true and watch for N+1.

Summary

In this module, you learned:


Next Module: RESTful Web Services & APIs

Resources

Slide Overview