JPQL Joins, Subqueries, and Functions in JPA

Illustration for JPQL Joins, Subqueries, and Functions in JPA
By Last updated:

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.persistencejakarta.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.