Hello! Welcome back to your course on backend development with Java and Spring.
In our last lesson, we tackled the N+1 query problem, a common performance pitfall. You learned how to solve it using fetch joins, primarily by writing custom JPQL queries with the @Query annotation. This was your first deep dive into customizing data retrieval beyond derived query methods.
Today, we will build directly on that knowledge. We'll explore what happens when even JPQL isn't enough. The learning outcome for this lesson is to use native SQL queries with @Query(nativeQuery = true) for complex scenarios. This powerful feature allows you to bypass the JPA abstraction and use the full, unadulterated power of your database's specific SQL dialect, giving you ultimate control when you need it most.
1. JPQL vs. Native SQL: A Quick Refresher
Before we write our first native query in Spring, it's essential to solidify the difference between JPQL (Java Persistence Query Language) and native SQL. While they look similar, they operate at different levels of abstraction.
Spring Data JPA @Query Annotation: JPQL and Native SQL
The article "Spring Data JPA @Query Annotation: JPQL and Native SQL" from CodingNomads provides a concise explanation of this core difference.
Please read the sections "Native SQL vs. JPQL" and "JPQL Crash Course". As you read, focus on these key distinctions: Target: Native SQL targets database tables and columns directly. JPQL targets Java entities and their fields. Syntax: Notice how fruit_bearing (column name) in SQL becomes fruitBearing (field name) in JPQL. Portability: JPQL is database-agnostic, meaning the same query works for MySQL, PostgreSQL, etc. Native SQL can use features specific to one database, making it less portable.
To summarize the key points:
- JPQL is an object-oriented query language. You write queries against your
Entityclasses and their fields, and the JPA provider (Hibernate) translates them into the appropriate SQL for your configured database. - Native SQL is the raw SQL you would write directly in a database client like MySQL Workbench. It's powerful but tied to a specific database dialect.
2. Writing Native Queries with @Query
Spring Data JPA makes it seamless to switch from JPQL to native SQL. You use the same @Query annotation but with an additional attribute: nativeQuery = true.
When you set this flag, Spring Data JPA no longer attempts to interpret the query as JPQL. Instead, it passes the query string directly to the underlying database via JDBC.
Let's look at a simple example.
Spring Data JPA @Query Annotation with Example
The article "Spring Data JPA @Query Annotation with Example" from GeeksforGeeks shows a straightforward side-by-side comparison.
Review the brief code snippets under the "Native:" heading. Notice how the native SQL query is almost identical to the JPQL one, but it's wrapped in an @Query annotation with nativeQuery = true.
Here is a comparative example based on what you just read. Imagine a Student entity that maps to a students table.
JPQL Query:
@Query("SELECT s FROM Student s WHERE s.age > 21")
List<Student> findOlderStudentsJPQL();
- Refers to the
Studententity. - Uses the
agefield of the entity.
Native SQL Query:
@Query(
value = "SELECT * FROM students WHERE age > 21",
nativeQuery = true)
List<Student> findOlderStudentsNative();
- Sets
nativeQuery = true. - Refers to the
studentstable. - Can use
*because it's standard SQL. - The results will still be mapped to the
Studententity, provided the column names in the result set (*) match the fields defined in the entity.
3. Handling Dynamic Parameters in Native Queries
Hardcoded queries are of limited use. Just like with JPQL, you can pass method parameters into your native queries. You have two main syntaxes for this:
- Positional Parameters (
?): Parameters are referenced by their position (?1,?2, etc.). This is simple but can be error-prone if the order of parameters changes. - Named Parameters (
:): Parameters are referenced by name (:paramName). This requires using the@Param("paramName")annotation on the corresponding method argument but is much more readable and maintainable. Named parameters are the recommended approach.
Spring Data JPA @Query Annotation: JPQL and Native SQL
Let's return to the CodingNomads article, which explains how to use parameters effectively.
Read the section "Supporting Variables with @Query". Pay close attention to the examples for both positional (?) and named (:) parameters. Note the use of the @Param annotation for the named parameter style.
Now, let's see a more complex example that reflects a real-world use case.
Spring Data JPA @Query Annotation with Example
The GeeksforGeeks article demonstrates a native query that includes a JOIN and a named parameter. This is a perfect example of a scenario where you might opt for native SQL.
Focus on the code block within "Step 5: Create Your Repository Interface". Analyze the native query defined in the AddressRepo interface. Notice: The nativeQuery = true flag is set. The value contains a standard SQL query with a JOIN. It uses a named parameter :employeeId to filter the results. The method parameter is annotated with @Param("employeeId") to bind the value.
This example shows how you can construct a fairly complex, parameterized query while still leveraging Spring Data JPA's ability to execute it and map the results back to your entities.
4. When Should You Use Native Queries?
Derived queries and JPQL should be your default choices because they are database-agnostic and work well with the object-oriented paradigm. However, you should use native queries for complex scenarios where the alternatives fall short.
Here are the primary reasons to use native SQL:
- Using Database-Specific Features: You need to use a function or keyword that is unique to your database and not part of the standard SQL supported by JPQL. For example, using hierarchical queries in Oracle (
CONNECT BY) or vendor-specific JSON functions. Since you have experience with MySQL, an example would be usingGROUP_CONCAT()to aggregate strings. - Highly Complex Joins and Subqueries: While JPQL supports joins and subqueries, some extremely complex query structures can be more clearly and efficiently expressed in native SQL.
- Performance Tuning: Native queries allow you to use database-specific query hints (e.g., forcing the use of a particular index) to optimize a critical query that isn't performing well.
- Calling Stored Procedures: While there are other ways to call them, using
@Querycan be a straightforward method for simple cases.
The trade-off is clear: you gain power and control at the cost of portability. If you write a native query for MySQL and later decide to migrate to PostgreSQL, you may need to rewrite that query.
Test your understanding!
You have an Employee entity that maps to an employees table. The table has columns id, first_name, last_name, department, and salary.
In your EmployeeRepository, write a method findTopEarnersInDepartment that uses a native query to find all employees in a given department whose salary is above a certain threshold. The method should accept the department (String) and minSalary (double) as parameters. Use named parameters.
Show answer
Here is the correct implementation within the EmployeeRepository interface:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
@Query(
value = "SELECT * FROM employees WHERE department = :dept AND salary > :minSal",
nativeQuery = true
)
List<Employee> findTopEarnersInDepartment(
@Param("dept") String department,
@Param("minSal") double minSalary
);
}
This solution correctly uses nativeQuery = true, references the database table and columns, and binds the method arguments to the query using named parameters with @Param.
Conclusion
You have now completed your foundational toolkit for querying data in Spring Data JPA. You can use derived queries for simple cases, JPQL with @Query for custom logic, and now native SQL with @Query(nativeQuery = true) for maximum power and control.
Key Takeaways:
- Native SQL queries are executed directly by the database, allowing you to use vendor-specific features that JPQL doesn't support.
- To enable native SQL in a repository method, you use the
@Queryannotation with thenativeQuery = trueattribute. - Your query string must use actual table and column names, not entity and field names.
- You can pass parameters to your native queries using positional (
?1) or named (:namewith@Param) placeholders, with named parameters being the preferred approach for clarity and maintainability. - Use native queries judiciously for complex scenarios where derived queries or JPQL are insufficient, as they reduce database portability.
What's Next?
So far in this module, we've focused heavily on how to write queries to get data out of the database. In the next lesson, we will shift our focus to the infrastructure that manages the communication itself. We will explore a critical component for any production-grade application: database connection pooling. You will learn its role and benefits, focusing on HikariCP, the default connection pool in Spring Boot.