What is JPA?
Think of JPA like a translator between Java and databases:
- 🗣️ You speak Java (objects, methods)
- 🗄️ Database speaks SQL (tables, rows)
- 🔄 JPA translates between them automatically
- ✨ You work with objects, JPA handles SQL
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