Hibernate Query Language (HQL) supports joins, but the syntax differs from SQL in important ways. Understanding how left joins work in HQL is essential for querying related entities without accidentally filtering out records.
HQL Join Syntax
Unlike SQL, HQL uses implicit joins based on mapped relationships. You don’t need to specify ON clauses — Hibernate uses your entity mappings.
Implicit Inner Join (default)
When you reference a relationship in a WHERE clause, Hibernate performs an inner join automatically:
// This generates an INNER JOIN
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o WHERE o.customer.name = :name", Order.class)
.setParameter("name", "Acme Corp")
.getResultList();The problem: this excludes orders without a customer (or with a null customer relationship).
Explicit LEFT JOIN
To include records where the relationship may be null, use LEFT JOIN (or LEFT OUTER JOIN — they’re equivalent in HQL):
// This generates a LEFT OUTER JOIN
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN o.customer c " +
"WHERE c.name IS NULL OR c.name = :name", Order.class)
.setParameter("name", "Acme Corp")
.getResultList();This includes all orders — even those without a customer.
JOIN FETCH for Eager Loading
A common issue with HQL joins is that related entities may trigger additional queries (the N+1 problem). Use JOIN FETCH to load them in a single query:
// Without JOIN FETCH — may cause N+1 queries
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN o.customer", Order.class)
.getResultList();
// With JOIN FETCH — single query
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN FETCH o.customer", Order.class)
.getResultList();LEFT JOIN FETCH
Combine left join with fetch to eagerly load optional relationships:
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN FETCH o.customer " +
"LEFT JOIN FETCH o.lineItems", Order.class)
.getResultList();This loads orders with their customers and line items in one query, even when those relationships are null.
Filtering on a Left-Joined Entity
To filter on a property of the joined entity without converting the left join to an inner join, put the condition in the ON clause (Hibernate 5.1+):
// Wrong — this effectively becomes an INNER JOIN
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN o.customer c WHERE c.active = true", Order.class)
.getResultList();
// Right — condition in ON clause preserves the LEFT JOIN
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN o.customer c ON c.active = true", Order.class)
.getResultList();Joining Unmapped Relationships
If two entities don’t have a mapped relationship, you can still join them using a theta-style join:
// Theta-style join (no mapped relationship)
List<Object[]> results = entityManager.createQuery(
"SELECT o, c FROM Order o LEFT JOIN Customer c ON o.customerId = c.id", Object[].class)
.getResultList();Note: this is less common and usually means you should consider adding a proper @ManyToOne mapping.
Common Patterns
Find orphaned records
// Orders without a customer
List<Order> orphans = entityManager.createQuery(
"SELECT o FROM Order o LEFT JOIN o.customer c WHERE c.id IS NULL", Order.class)
.getResultList();Count with grouping
// Count orders per customer, including customers with zero orders
List<Object[]> counts = entityManager.createQuery(
"SELECT c.name, COUNT(o.id) FROM Customer c LEFT JOIN c.orders o GROUP BY c.name", Object[].class)
.getResultList();