Implementing Raw SQL Queries in Spring Data JPA: Practices and Best Solutions

Nov 23, 2025 · Programming · 12 views · 7.8

Keywords: Spring Data JPA | Raw SQL Queries | @Query Annotation

Abstract: This article provides an in-depth exploration of using raw SQL queries within Spring Data JPA, focusing on the application of the @Query annotation's nativeQuery parameter. Through detailed code examples, it demonstrates how to execute native queries and handle results effectively. The analysis also addresses potential issues with embedding SQL directly in code and offers best practice recommendations for separating SQL logic from business code, helping developers maintain clarity and maintainability when working with raw SQL.

Overview of Raw SQL Queries in Spring Data JPA

In Spring Data JPA development, while the framework offers robust method naming queries and JPQL support, there are specific complex scenarios where developers need to utilize raw SQL queries to meet particular requirements. Spring Data JPA provides this capability through the @Query annotation's nativeQuery parameter, enabling the execution of native SQL queries.

Executing Native Queries with @Query Annotation

The @Query annotation in Spring Data JPA allows developers to write SQL statements directly by setting the nativeQuery parameter to true. This approach maintains Spring Data's declarative programming style while offering SQL's flexibility.

Here is a comprehensive usage example:

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {
    @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
    User findByEmail(String email);
    
    @Query(value = "SELECT COUNT(*) FROM orders WHERE user_id = ?1 AND status = 'COMPLETED'", nativeQuery = true)
    Long countCompletedOrdersByUserId(Long userId);
}

In this example, we define two query methods using raw SQL. The first method finds a user by email, and the second counts completed orders for a specified user. Parameter binding uses positional parameters ?1, where the number indicates the parameter's position in the method signature.

Projection Interfaces and Result Mapping

When query results do not require complete entity objects, projection interfaces can define the returned data structure. This method is particularly useful for scenarios needing only partial fields, improving query performance and reducing data transfer.

Define a projection interface:

public interface UserProjection {
    Long getId();
    String getUsername();
    String getEmail();
}

Use the projection in the Repository:

@Query(value = "SELECT id, username, email FROM users WHERE active = true", nativeQuery = true)
List<UserProjection> findActiveUsers();

The advantage of projection interfaces is type safety, with the compiler checking method correctness at compile time, while avoiding unnecessary field loading.

Using EntityManager for Complex Queries

For more complex query scenarios, especially those requiring dynamic SQL construction or handling non-standard result sets, JPA's EntityManager can be used directly. This method offers maximum flexibility but requires manual result mapping by the developer.

Custom result object:

public class UserStatistics {
    private final Long userId;
    private final String username;
    private final Long orderCount;
    
    public UserStatistics(Object[] columns) {
        this.userId = ((Number) columns[0]).longValue();
        this.username = (String) columns[1];
        this.orderCount = ((Number) columns[2]).longValue();
    }
    
    // Getters omitted for brevity
}

Execute query using EntityManager:

@PersistenceContext
private EntityManager entityManager;

public List<UserStatistics> getUserOrderStatistics() {
    String sql = "SELECT u.id, u.username, COUNT(o.id) " +
                 "FROM users u LEFT JOIN orders o ON u.id = o.user_id " +
                 "GROUP BY u.id, u.username";
    
    Query query = entityManager.createNativeQuery(sql);
    List<Object[]> results = query.getResultList();
    
    return results.stream()
                  .map(UserStatistics::new)
                  .collect(Collectors.toList());
}

Considerations for Embedding SQL in Code

Although Spring Data JPA supports writing SQL directly in code, this practice requires careful consideration. As noted in the reference article, hardcoding SQL strings in source code is generally considered an anti-pattern.

Main issues include:

Best Practice Recommendations

To balance flexibility and code quality, the following best practices are recommended:

1. Centralize SQL Statement Management

Store SQL statements centrally in resource files or configuration classes instead of scattering them across various Repositories:

@Component
public class SqlQueries {
    public static final String FIND_USER_BY_EMAIL = 
        "SELECT * FROM users WHERE email = ?1";
    public static final String COUNT_USER_ORDERS = 
        "SELECT COUNT(*) FROM orders WHERE user_id = ?1";
}

Reference in Repository:

@Query(value = SqlQueries.FIND_USER_BY_EMAIL, nativeQuery = true)
User findByEmail(String email);

2. Limit the Scope of Raw SQL Usage

Prioritize JPQL and method naming queries, using raw SQL only in the following scenarios:

3. Utilize Named Native Queries

Define named native queries in entity classes to enhance code readability and maintainability:

@Entity
@NamedNativeQueries({
    @NamedNativeQuery(
        name = "User.findActiveUsersWithOrders",
        query = "SELECT u.* FROM users u WHERE u.active = true " +
                "AND EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)",
        resultClass = User.class
    )
})
public class User {
    // entity definition
}

Usage in Repository:

@Query(nativeQuery = true)
List<User> findActiveUsersWithOrders();

Conclusion

Spring Data JPA provides powerful raw SQL query capabilities through the @Query annotation's nativeQuery parameter. In practical applications, developers should choose appropriate methods based on specific needs: use projection interfaces for simple field projections, EntityManager for complex result mappings, and adhere to the principle of centralized SQL statement management to ensure code maintainability and readability. By properly applying these techniques, developers can enjoy SQL's flexibility while maintaining clean architecture in Spring applications.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.