The Java Persistence API (JPA) provides a robust way to map Java objects to relational database tables. While basic CRUD operations are straightforward, real-world applications demand dynamic querying, filtering, and joining across entities.
This is where the Java Persistence Query Language (JPQL) comes into play. JPQL is object-oriented, meaning you query entities, not tables. It provides database portability while offering SQL-like syntax tailored for Java applications.
In this tutorial, we’ll explore JPQL basics, complete with examples, CRUD operations, joins, best practices, and pitfalls to avoid.
What is JPQL?
JPQL is the query language of JPA. Unlike SQL, which works directly on database tables, JPQL works with entity objects and their fields.
Example Difference:
-
SQL:
SELECT * FROM employees e WHERE e.emp_name = 'John';
-
JPQL:
SELECT e FROM Employee e WHERE e.name = 'John'
Here, JPQL queries the Employee entity, not the employees
table.
Setting Up JPA for JPQL Queries
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
version="3.0">
<persistence-unit name="examplePU">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.example.Employee</class>
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:testdb"/>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
Entity Example for JPQL
import jakarta.persistence.*;
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String department;
private Double salary;
// getters and setters
}
Basic JPQL Queries
1. Select All
List<Employee> employees = entityManager
.createQuery("SELECT e FROM Employee e", Employee.class)
.getResultList();
2. Filtering with WHERE
List<Employee> employees = entityManager
.createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
.setParameter("dept", "HR")
.getResultList();
3. Ordering Results
List<Employee> employees = entityManager
.createQuery("SELECT e FROM Employee e ORDER BY e.salary DESC", Employee.class)
.getResultList();
4. Aggregates (COUNT, SUM, AVG)
Long count = entityManager
.createQuery("SELECT COUNT(e) FROM Employee e", Long.class)
.getSingleResult();
JPQL Joins
Inner Join
SELECT e FROM Employee e JOIN e.department d WHERE d.name = 'IT'
Left Join
SELECT e FROM Employee e LEFT JOIN e.department d
Fetch Join
SELECT e FROM Employee e JOIN FETCH e.department
⚡ Note: Fetch joins help prevent the N+1 select problem.
JPQL CRUD Operations
Create (Persist)
Employee emp = new Employee();
emp.setName("Alice");
emp.setDepartment("Finance");
emp.setSalary(80000.0);
entityManager.getTransaction().begin();
entityManager.persist(emp);
entityManager.getTransaction().commit();
Read (Find)
Employee emp = entityManager.find(Employee.class, 1L);
Update (Merge)
entityManager.getTransaction().begin();
emp.setSalary(90000.0);
entityManager.merge(emp);
entityManager.getTransaction().commit();
Delete (Remove)
entityManager.getTransaction().begin();
entityManager.remove(emp);
entityManager.getTransaction().commit();
Real-World Use Cases
- Reporting dashboards → Aggregated salary reports, employee counts.
- E-commerce → Query orders, customers, products with joins.
- Banking apps → Audit trails and transaction lookups.
Performance Considerations
- Use JOIN FETCH to avoid N+1 queries.
- Apply pagination with
setFirstResult
andsetMaxResults
. - Prefer named queries for reusable queries.
Anti-Patterns and Pitfalls
- Avoid
SELECT *
→ Always specify fields. - Don’t use
EnumType.ORDINAL
with enums in queries. - Be cautious with eager fetching → may load unnecessary data.
Best Practices
- Use parameters (
:param
) instead of concatenating strings. - Define Named Queries for frequently used queries.
- Monitor SQL output for performance tuning.
- Always prefer
JOIN FETCH
for associations when needed.
📌 JPA Version Notes
- JPA 2.0 → Introduced Criteria API for type-safe queries.
- JPA 2.1 → Added entity graphs, stored procedures.
- JPA 2.2 → Added Java 8 time support.
- Jakarta Persistence → Package renamed (
javax.persistence
→jakarta.persistence
).
Conclusion and Key Takeaways
- JPQL is an object-oriented query language for entities, not tables.
- It supports filtering, ordering, joins, and aggregates.
- Performance tuning (JOIN FETCH, pagination) is essential in production.
- Always use best practices like named queries and parameter binding.
FAQ: Expert-Level Questions
1. What’s the difference between JPA and Hibernate?
JPA is a specification; Hibernate is one of its implementations.
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?
It loads all associations upfront, wasting memory and bandwidth.
4. How can I solve the N+1 select problem with JPA?
By using JOIN FETCH
, batch fetching, or entity graphs.
5. Can I use JPA without Hibernate?
Yes, EclipseLink, OpenJPA, and DataNucleus are alternatives.
6. What’s the best strategy for inheritance mapping in JPA?
Depends on needs: SINGLE_TABLE
(fastest), JOINED
(normalized), TABLE_PER_CLASS
(rare).
7. How does JPA handle composite keys?
Using @IdClass
or @EmbeddedId
annotations.
8. What changes with Jakarta Persistence?
Package names switched to jakarta.persistence
.
9. Is JPA suitable for microservices?
Yes, but for ultra-high-performance microservices, lighter tools may be better.
10. When should I avoid using JPA?
When low-level performance or complex reporting queries are required.