While JPQL basics cover simple queries, real-world enterprise applications often require complex joins, nested subqueries, and advanced functions. These allow developers to retrieve structured data, compute aggregates, and optimize performance.
The Java Persistence Query Language (JPQL) provides a SQL-like but entity-oriented query mechanism. Unlike SQL, which works with tables, JPQL works with entities and their relationships. This tutorial will guide you through joins, subqueries, and built-in functions in JPQL, complete with examples, best practices, and pitfalls to avoid.
Entities Setup Example
We’ll use a simple domain model for demonstration:
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees = new ArrayList<>();
}
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
}
JPQL Joins
Joins in JPQL allow navigating relationships between entities.
1. Inner Join
SELECT e FROM Employee e JOIN e.department d WHERE d.name = 'IT'
Fetches employees belonging to the IT department.
2. Left Join
SELECT e FROM Employee e LEFT JOIN e.department d
Returns employees regardless of whether they belong to a department.
3. Fetch Join
SELECT e FROM Employee e JOIN FETCH e.department
Loads employees and their departments in one query (avoids N+1 select problem).
JPQL Subqueries
JPQL supports subqueries in WHERE
and HAVING
clauses but not in FROM
.
Example 1: Employees with salary above average
SELECT e FROM Employee e
WHERE e.salary > (SELECT AVG(emp.salary) FROM Employee emp)
Example 2: Departments with employees
SELECT d FROM Department d
WHERE EXISTS (SELECT e FROM Employee e WHERE e.department = d)
Example 3: Employees in departments with more than 5 staff
SELECT e FROM Employee e
WHERE e.department IN (
SELECT d FROM Department d WHERE SIZE(d.employees) > 5
)
JPQL Functions
JPQL provides string, numeric, and date functions for transformations.
String Functions
CONCAT(e.name, ' - ', e.department.name)
UPPER(e.name)
LOWER(e.name)
SUBSTRING(e.name, 1, 3)
SELECT CONCAT(UPPER(e.name), ' works in ', d.name)
FROM Employee e JOIN e.department d
Numeric Functions
ABS(e.salary)
MOD(e.id, 2)
SQRT(e.salary)
SELECT e.name, MOD(e.id, 2) FROM Employee e
Date/Time Functions
CURRENT_DATE
CURRENT_TIME
CURRENT_TIMESTAMP
SELECT e.name FROM Employee e WHERE e.hireDate < CURRENT_DATE
CRUD Operations Example
Persisting an Entity
Department d = new Department();
d.setName("Finance");
Employee e1 = new Employee();
e1.setName("Alice");
e1.setSalary(90000.0);
e1.setDepartment(d);
entityManager.getTransaction().begin();
entityManager.persist(d);
entityManager.persist(e1);
entityManager.getTransaction().commit();
Querying with Joins
TypedQuery<Employee> query = entityManager.createQuery(
"SELECT e FROM Employee e JOIN FETCH e.department WHERE e.salary > :minSalary",
Employee.class);
query.setParameter("minSalary", 50000.0);
List<Employee> results = query.getResultList();
Real-World Use Cases
- HR Systems → Query employees by department and salary ranges.
- E-commerce → Retrieve orders with nested customer-product joins.
- Banking → Audit queries using date/time functions.
Anti-Patterns and Pitfalls
- Using
JOIN FETCH
everywhere → can load unnecessary data. - Overusing subqueries → may impact performance, use joins instead.
- Using
EnumType.ORDINAL
in queries → brittle when enum changes. - Ignoring lazy loading pitfalls → can cause runtime errors if session closed.
Best Practices
- Use JOIN FETCH only where needed.
- Use Named Queries for reusable queries.
- Apply pagination for large datasets.
- Monitor SQL output for N+1 query issues.
📌 JPA Version Notes
- JPA 2.0 → Added Criteria API, Metamodel.
- JPA 2.1 → Introduced stored procedures, entity graphs.
- JPA 2.2 → Added support for Java 8 Date/Time API.
- Jakarta Persistence (EE 9/10/11) → Package renamed from
javax.persistence
→jakarta.persistence
.
Conclusion and Key Takeaways
- JPQL joins simplify querying across entity relationships.
- Subqueries allow filtering with nested logic.
- Functions enhance querying with string, numeric, and date operations.
- Apply best practices to avoid N+1 problems and performance bottlenecks.
FAQ: Expert-Level Questions
1. What’s the difference between JPA and Hibernate?
JPA is a specification; Hibernate is a JPA implementation.
2. How does JPA handle the persistence context?
It’s like a classroom attendance register, tracking managed entities.
3. What are the drawbacks of eager fetching in JPA?
Loads unnecessary data, slowing performance.
4. How can I solve the N+1 select problem with JPA?
Use JOIN FETCH
, batch fetching, or entity graphs.
5. Can I use JPA without Hibernate?
Yes, with EclipseLink, OpenJPA, or DataNucleus.
6. What’s the best strategy for inheritance mapping in JPA?
Depends: SINGLE_TABLE
(fast), JOINED
(normalized), TABLE_PER_CLASS
(rare).
7. How does JPA handle composite keys?
Using @IdClass
or @EmbeddedId
.
8. What changes with Jakarta Persistence?
Package renamed to jakarta.persistence
.
9. Is JPA suitable for microservices?
Yes, but lightweight alternatives may be better for high-performance apps.
10. When should I avoid using JPA?
For batch-heavy, complex reporting, or ultra-low latency systems.