JPA (Java Persistence API)

Object-Relational Mapping for Database Access

← Back to Index

What is JPA?

Think of JPA like a translator between Java and databases:

JPA (Java Persistence API) is a specification for mapping Java objects to database tables. It's an ORM (Object-Relational Mapping) framework that eliminates most SQL code.

Popular implementations: Hibernate, EclipseLink, OpenJPA

Without JPA (Manual SQL)

// Lots of boilerplate code!
Connection conn = DriverManager.getConnection(url);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setLong(1, userId);
ResultSet rs = stmt.executeQuery();

User user = null;
if (rs.next()) {
    user = new User();
    user.setId(rs.getLong("id"));
    user.setName(rs.getString("name"));
    user.setEmail(rs.getString("email"));
}
rs.close();
stmt.close();
conn.close();

With JPA (Simple!)

// One line!
User user = entityManager.find(User.class, userId);

Creating an Entity

Simple Entity

import jakarta.persistence.*;

@Entity  // Maps to database table
@Table(name = "users")  // Optional: specify table name
public class User {

    @Id  // Primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // Auto-increment
    private Long id;

    @Column(name = "user_name", length = 50, nullable = false)
    private String username;

    @Column(unique = true)
    private String email;

    private int age;  // Maps to "age" column automatically

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    // Constructors
    public User() { }

    public User(String username, String email) {
        this.username = username;
        this.email = email;
        this.createdAt = new Date();
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

JPA creates this table automatically:

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_name VARCHAR(50) NOT NULL,
    email VARCHAR(255) UNIQUE,
    age INT,
    created_at TIMESTAMP
);

EntityManager - Your Database Interface

Injecting EntityManager

@Stateless
public class UserRepository {

    @PersistenceContext  // Inject EntityManager
    private EntityManager em;

    // CRUD operations below...
}

Create (Insert)

public void createUser(User user) {
    em.persist(user);  // INSERT INTO users ...
    // ID is automatically generated and set
}

Read (Select)

// Find by primary key
public User findUser(Long id) {
    return em.find(User.class, id);  // SELECT * FROM users WHERE id = ?
}

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

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

Update

public void updateUser(Long id, String newEmail) {
    User user = em.find(User.class, id);
    if (user != null) {
        user.setEmail(newEmail);  // Just change the object!
        // JPA automatically updates database (no em.update() needed!)
    }
}

Delete

public void deleteUser(Long id) {
    User user = em.find(User.class, id);
    if (user != null) {
        em.remove(user);  // DELETE FROM users WHERE id = ?
    }
}

Relationships

@OneToMany - One User Has Many Orders

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Order> orders = new ArrayList<>();

    // Getters/setters
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    private String productName;
    private double total;

    @ManyToOne
    @JoinColumn(name = "user_id")  // Foreign key column
    private User user;

    // Getters/setters
}

Usage:

User user = new User("Alice");

Order order1 = new Order("Laptop", 999.99);
order1.setUser(user);

Order order2 = new Order("Mouse", 29.99);
order2.setUser(user);

user.getOrders().add(order1);
user.getOrders().add(order2);

em.persist(user);  // Saves user + both orders (cascade!)

@ManyToMany - Students and Courses

@Entity
public class Student {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(
        name = "student_course",  // Join table name
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses = new ArrayList<>();
}

@Entity
public class Course {
    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @ManyToMany(mappedBy = "courses")
    private List<Student> students = new ArrayList<>();
}

JPQL (JPA Query Language)

Like SQL but for objects!

Basic Queries

// Select all
List<User> users = em.createQuery("SELECT u FROM User u", User.class)
                          .getResultList();

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

// Order by
List<User> sorted = em.createQuery(
    "SELECT u FROM User u ORDER BY u.username ASC", User.class)
    .getResultList();

// Pagination
List<User> page = em.createQuery("SELECT u FROM User u", User.class)
                          .setFirstResult(0)     // Start at 0
                          .setMaxResults(10)    // Get 10 records
                          .getResultList();

Joins

// Inner join
List<Order> orders = em.createQuery(
    "SELECT o FROM Order o JOIN o.user u WHERE u.username = :username",
    Order.class)
    .setParameter("username", "Alice")
    .getResultList();

// Fetch join (avoids N+1 problem)
List<User> usersWithOrders = em.createQuery(
    "SELECT u FROM User u LEFT JOIN FETCH u.orders",
    User.class)
    .getResultList();

Aggregates

// Count
Long count = em.createQuery("SELECT COUNT(u) FROM User u", Long.class)
                  .getSingleResult();

// Average
Double avgAge = em.createQuery("SELECT AVG(u.age) FROM User u", Double.class)
                    .getSingleResult();

// Sum
Double total = em.createQuery(
    "SELECT SUM(o.total) FROM Order o WHERE o.user.id = :userId",
    Double.class)
    .setParameter("userId", 1L)
    .getSingleResult();

Native SQL (When JPQL Isn't Enough)

// Execute native SQL
List<User> users = em.createNativeQuery(
    "SELECT * FROM users WHERE email LIKE ?1", User.class)
    .setParameter(1, "%@gmail.com")
    .getResultList();

Named Queries

Define queries once, reuse everywhere

@Entity
@NamedQueries({
    @NamedQuery(
        name = "User.findAll",
        query = "SELECT u FROM User u"
    ),
    @NamedQuery(
        name = "User.findByEmail",
        query = "SELECT u FROM User u WHERE u.email = :email"
    ),
    @NamedQuery(
        name = "User.findAdults",
        query = "SELECT u FROM User u WHERE u.age >= 18"
    )
})
public class User {
    // Entity fields...
}

// Usage
List<User> users = em.createNamedQuery("User.findAll", User.class)
                          .getResultList();

User user = em.createNamedQuery("User.findByEmail", User.class)
                 .setParameter("email", "alice@example.com")
                 .getSingleResult();

Entity Lifecycle

NEW (Transient)
    ↓ em.persist()
MANAGED (Persistent) ← Changes tracked, auto-saved
    ↓ em.detach() or transaction ends
DETACHED (No longer tracked)
    ↓ em.merge()
MANAGED again
    ↓ em.remove()
REMOVED (Marked for deletion)
    ↓ transaction commits
Deleted from database

Lifecycle Callbacks

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    @PrePersist  // Before INSERT
    public void onCreate() {
        createdAt = LocalDateTime.now();
        System.out.println("About to create: " + name);
    }

    @PreUpdate  // Before UPDATE
    public void onUpdate() {
        updatedAt = LocalDateTime.now();
        System.out.println("Updating: " + name);
    }

    @PostPersist  // After INSERT
    public void afterCreate() {
        System.out.println("Created with ID: " + id);
    }

    @PostLoad  // After entity loaded from database
    public void afterLoad() {
        System.out.println("Loaded: " + name);
    }

    @PreRemove  // Before DELETE
    public void onDelete() {
        System.out.println("About to delete: " + name);
    }
}

Best Practices

✅ DO:

  • Use JPQL for most queries - Database-independent
  • Use fetch joins to avoid N+1 - Load related data efficiently
  • Use pagination for large result sets - setMaxResults()
  • Let transactions handle persistence - Changes auto-saved
  • Use @NamedQueries for common queries - Validated at startup
  • Keep entities simple - No business logic
  • Use cascade wisely - Understand propagation

❌ DON'T:

  • Don't forget @Id - Every entity needs primary key
  • Don't expose entities to view layer - Use DTOs
  • Don't use SELECT * - Fetch only needed columns
  • Don't load entire tables - Use pagination
  • Don't put business logic in entities - Keep them data-focused
  • Don't forget lazy vs eager loading - Default is lazy for collections

Summary

  • JPA maps Java objects to database tables automatically
  • @Entity: Marks a class as a database table
  • @Id: Primary key field
  • EntityManager: Your interface to the database (persist, find, remove)
  • JPQL: Object-oriented query language (like SQL for objects)
  • Relationships: @OneToMany, @ManyToOne, @ManyToMany
  • Cascade: Propagate operations to related entities
  • Lifecycle: New → Managed → Detached → Removed
  • Implementations: Hibernate (most popular), EclipseLink, OpenJPA
  • Benefits: No SQL code, database portability, automatic mapping