Skip to the content.

spring-native-query

Github Issues Github Stars Java Twitter LICENSE

Spring Native Query

Elevate your code quality and your team’s productivity.

Spring Native Query is the definitive library to revolutionize native query access in Spring Boot applications (with full support for versions 3 and 4). Designed for developers who demand flexibility without sacrificing organization, it eliminates the complexity of concatenating SQL strings in Java and delivers a clean, secure, and powerful development experience.

Why use Spring Native Query?

1. Clean and Decoupled Architecture

Say goodbye to SQL strings polluting your Java classes.

2. Dynamic and Smart SQL

Unleash the full power of Freemarker as a Template Engine for your queries.

3. Productivity and Security

Write less boilerplate code and deliver more value.


Key Features


Usage Example

In this example, Spring Native Query is used to:

This example demonstrates how to use the Spring Native Query library in a Spring Boot 4 application to perform native SQL queries and map results directly to Java classes.

Demo Repository

You can find a complete demo project here: spring-boot-4-database-native-query-demo

Project Structure

src/
  main/
    java/
      io/github/gasparbarancelli/demo/nativequery/
        NativequeryApplication.java
        SaleCustomerResult.java
        SaleFullResult.java
        SalesController.java
        SalesFilter.java
        SalesNativeQuery.java
    resources/
      application.properties
      db/migration/V1__import.sql
      nativeQuery/findSales.sql
      nativeQuery/findSalesCustomers.sql

Dependencies

To use NativeQuery in your Spring Boot project, add the following dependencies to your pom.xml:

<!-- NativeQuery library -->
<dependency>
    <groupId>io.github.gasparbarancelli</groupId>
    <artifactId>spring-native-query</artifactId>
    <version>4.0.0</version>
</dependency>

<!-- Spring Boot Starter Data JPA -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- MySQL Connector (or your preferred database) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- Flyway for database migrations (optional, used in this example to migrate the database schema) -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

Code Examples

NativequeryApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.web.config.EnableSpringDataWebSupport;

// Main Spring Boot application class
@SpringBootApplication
// Enables Spring Data Web support to use Pageable objects in REST endpoints to Spring Boot 4; do not add this configuration in Spring Boot 3.
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
public class NativequeryApplication {
    // Main method to start the application
    public static void main(String[] args) {
        SpringApplication.run(NativequeryApplication.class, args);
    }
}

SaleCustomerResult.java

import java.math.BigDecimal;
import java.time.LocalDateTime;

// DTO for sales results by customer
public class SaleCustomerResult {
    private Integer id; // Sale ID
    private BigDecimal totalAmount; // Total sale amount
    private String status; // Sale status
    private Integer customerId; // Customer ID
    private String customerFullName; // Customer full name
    private String customerEmail; // Customer email
    private Integer customerActive; // Customer active status
    // getters and setters
}

SaleFullResult.java

import java.math.BigDecimal;
import java.time.LocalDateTime;

// Record for complete sale result, including items, payments, and customer
public record SaleFullResult(
    int id, // Sale ID
    LocalDateTime saleDate, // Sale date
    int customerId, // Customer ID
    String customerFullName, // Customer full name
    String customerEmail, // Customer email
    int customerActive, // Customer active status
    int saleItemId, // Sale item ID
    int saleItemQuantity, // Item quantity
    BigDecimal saleItemUnitPrice, // Item unit price
    BigDecimal saleItemDiscount, // Item discount
    int productId, // Product ID
    String productName, // Product name
    String productDescription, // Product description
    BigDecimal productPrice, // Product price
    int productActive, // Product active status
    int salePaymentId, // Payment ID
    String salePaymentPaymentType, // Payment type
    BigDecimal salePaymentPaidAmount, // Paid amount
    LocalDateTime salePaymentPaymentDate, // Payment date
    BigDecimal totalAmount, // Total sale amount
    String status // Sale status
) {}

SalesFilter.java

import io.github.gasparbarancelli.NativeQueryOperator;
import io.github.gasparbarancelli.NativeQueryParam;

// Filter for sales queries
public record SalesFilter(
        Number id, // Sale ID
        // The library supports operations such as CONTAINING, STARTS_WITH, and ENDS_WITH, automatically adding wildcard characters.
        @NativeQueryParam(value = "name", operator = NativeQueryOperator.CONTAINING)
        String customerName // Customer name for search
) {
}

SalesNativeQuery.java

import io.github.gasparbarancelli.NativeQuery;
import io.github.gasparbarancelli.NativeQueryParam;
import io.github.gasparbarancelli.NativeQueryUseJdbcTemplate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;

// Interface for native queries using the nativequery library
@Repository
public interface SalesNativeQuery extends NativeQuery {
    // Query sales by customer
    // The library automatically executes an SQL file with the same name as this method and converts the response into a `SaleCustomerResult` result
    List<SaleCustomerResult> findSalesCustomers();
    // Paginated query of sales by customer; the library executes the query using pagination and returns a `Page<SaleCustomerResult>`.
    // Only queries performed with the entity manager can return a page; JdbcTemplate does not support this feature
    Page<SaleCustomerResult> findSalesCustomers(Pageable pageable);
    // Query all sales
    // The default NativeQuery implementation executes queries using the EntityManager, but you can change it and use the JdbcTemplate using the annotation below.
    @NativeQueryUseJdbcTemplate
    List<SaleFullResult> findSales();
    // Query sales by customer ID
    @NativeQueryUseJdbcTemplate
    // By default, the query uses the same file name as the method, but you can specify a different file name using the annotation below.
    @NativeQueryFileName("findSales")
    // You can add parameters to the query using the annotation below.
    List<SaleFullResult> findSalesByCustomerId(@NativeQueryParam(value = "customerId") int customerId);
    // Query sales using filter
    @NativeQueryUseJdbcTemplate
    // You can add custom objects to the query using the annotation below, where the library adds all the fields from the objects as parameters in the query.
    List<SaleFullResult> findSales(@NativeQueryParam(value = "filter", addChildren = true) SalesFilter filter);
    // Dynamic query using parameter map
    @NativeQueryUseJdbcTemplate
    // You can add a parameter map and use it in the query to perform more dynamic queries.
    List<SaleFullResult> findSales(Map<String, Object> params);
}

SalesController.java

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// REST controller for sales endpoints
@RestController
@RequestMapping("/sales")
public class SalesController {
    private final SalesNativeQuery salesNativeQuery; // Injects the query interface
    public SalesController(SalesNativeQuery salesNativeQuery) {
        this.salesNativeQuery = salesNativeQuery;
    }
    // Returns all sales
    @GetMapping
    public List<SaleFullResult> findSales() {
        return salesNativeQuery.findSales();
    }
    // Returns sales by customer ID
    @GetMapping("/customer/{customerId}")
    public List<SaleFullResult> findSalesByCustomerId(@PathVariable("customerId") int customerId) {
        return salesNativeQuery.findSalesByCustomerId(customerId);
    }
    // Returns sales filtered
    @PostMapping("/filter")
    public List<SaleFullResult> findSalesByFilter(@RequestBody SalesFilter filter) {
        return salesNativeQuery.findSales(filter);
    }
    // Returns sales using dynamic parameters
    @GetMapping("/dynamic")
    public List<SaleFullResult> findDynamicSales() {
        Map<String, Object> map = new HashMap<>();
        map.put("p.id", 1);
        map.put("c.id", 1);
        return salesNativeQuery.findSales(map);
    }
    // Returns aggregated sales by customer
    @GetMapping("/customers")
    public List<SaleCustomerResult> findSalesCustomers() {
        return salesNativeQuery.findSalesCustomers();
    }
    // Returns paginated aggregated sales by customer
    @GetMapping("/customers/pageable")
    public Page<SaleCustomerResult> findSalesCustomersPageable(Pageable pageable) {
        return salesNativeQuery.findSalesCustomers(pageable);
    }
}

application.properties

# Application and database configuration
spring.application.name=nativequery
native-query.package-scan=io.github.gasparbarancelli.demo.nativequery
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration

V1__import.sql

-- Main tables for sales domain
CREATE TABLE CUSTOMER
(
    id        INT          NOT NULL AUTO_INCREMENT,
    full_name VARCHAR(100) NOT NULL,
    email     VARCHAR(100) NULL,
    active    INT          NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE PRODUCT
(
    id          INT            NOT NULL AUTO_INCREMENT,
    name        VARCHAR(100)   NOT NULL,
    description VARCHAR(255) NULL,
    price       DECIMAL(10, 2) NOT NULL,
    active      INT            NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE SALE
(
    id           INT            NOT NULL AUTO_INCREMENT,
    sale_date    DATETIME       NOT NULL,
    customer_id  INT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status       VARCHAR(20)    NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (customer_id) REFERENCES CUSTOMER (id)
);

CREATE TABLE SALE_ITEM
(
    id         INT            NOT NULL AUTO_INCREMENT,
    sale_id    INT            NOT NULL,
    product_id INT            NOT NULL,
    quantity   INT            NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,
    discount   DECIMAL(10, 2) NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (sale_id) REFERENCES SALE (id),
    FOREIGN KEY (product_id) REFERENCES PRODUCT (id)
);

CREATE TABLE SALE_PAYMENT
(
    id           INT            NOT NULL AUTO_INCREMENT,
    sale_id      INT            NOT NULL,
    payment_type VARCHAR(20)    NOT NULL,
    paid_amount  DECIMAL(10, 2) NOT NULL,
    payment_date DATETIME       NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (sale_id) REFERENCES SALE (id)
);

INSERT INTO CUSTOMER (full_name, email, active)
VALUES ('John Doe', 'john.doe@email.com', 1),
       ('Jane Smith', 'jane.smith@email.com', 1),
       ('Alice Johnson', 'alice.j@email.com', 1),
       ('Bob Brown', 'bob.b@email.com', 1),
       ('Charlie Black', 'charlie.b@email.com', 1);

INSERT INTO PRODUCT (name, description, price, active)
VALUES ('Laptop', 'High performance laptop', 2500.00, 1),
       ('Smartphone', 'Latest model smartphone', 1800.00, 1),
       ('Headphones', 'Noise cancelling headphones', 350.00, 1),
       ('Monitor', '27 inch 4K monitor', 1200.00, 1),
       ('Keyboard', 'Mechanical keyboard', 400.00, 1);

INSERT INTO SALE (sale_date, customer_id, total_amount, status)
VALUES ('2024-06-01 10:00:00', 1, 2500.00, 'COMPLETED'),
       ('2024-06-01 11:00:00', 2, 1800.00, 'COMPLETED'),
       ('2024-06-01 12:00:00', 3, 350.00, 'COMPLETED'),
       ('2024-06-01 13:00:00', 4, 1200.00, 'COMPLETED'),
       ('2024-06-01 14:00:00', 5, 400.00, 'COMPLETED'),
       ('2024-06-02 10:00:00', 1, 2200.00, 'COMPLETED'),
       ('2024-06-02 11:00:00', 2, 1550.00, 'COMPLETED'),
       ('2024-06-02 12:00:00', 3, 350.00, 'COMPLETED'),
       ('2024-06-02 13:00:00', 4, 1200.00, 'COMPLETED'),
       ('2024-06-02 14:00:00', 5, 400.00, 'COMPLETED');

INSERT INTO SALE_ITEM (sale_id, product_id, quantity, unit_price, discount)
VALUES (1, 1, 1, 2500.00, 0.00),
       (2, 2, 1, 1800.00, 0.00),
       (3, 3, 1, 350.00, 0.00),
       (4, 4, 1, 1200.00, 0.00),
       (5, 5, 1, 400.00, 0.00),
       (6, 1, 1, 2200.00, 300.00),
       (7, 2, 1, 1550.00, 250.00),
       (8, 3, 1, 350.00, 0.00),
       (9, 4, 1, 1200.00, 0.00),
       (10, 5, 1, 400.00, 0.00);

INSERT INTO SALE_PAYMENT (sale_id, payment_type, paid_amount, payment_date)
VALUES (1, 'CREDIT_CARD', 2500.00, '2024-06-01 10:05:00'),
       (2, 'CREDIT_CARD', 1800.00, '2024-06-01 11:05:00'),
       (3, 'CASH', 350.00, '2024-06-01 12:05:00'),
       (4, 'CREDIT_CARD', 1200.00, '2024-06-01 13:05:00'),
       (5, 'CASH', 400.00, '2024-06-01 14:05:00'),
       (6, 'CREDIT_CARD', 2200.00, '2024-06-02 10:05:00'),
       (7, 'CREDIT_CARD', 1550.00, '2024-06-02 11:05:00'),
       (8, 'CASH', 350.00, '2024-06-02 12:05:00'),
       (9, 'CREDIT_CARD', 1200.00, '2024-06-02 13:05:00'),
       (10, 'CASH', 400.00, '2024-06-02 14:05:00');

findSales.sql

-- Native query to fetch complete sales
SELECT
    s.id AS id,
    s.sale_date AS saleDate,
    c.id AS customerId,
    c.full_name AS customerFullName,
    c.email AS customerEmail,
    c.active AS customerActive,
    si.id AS saleItemId,
    si.quantity AS saleItemQuantity,
    si.unit_price AS saleItemUnitPrice,
    si.discount AS saleItemDiscount,
    p.id AS productId,
    p.name AS productName,
    p.description AS productDescription,
    p.price AS productPrice,
    p.active AS productActive,
    sp.id AS salePaymentId,
    sp.payment_type AS salePaymentPaymentType,
    sp.paid_amount AS salePaymentPaidAmount,
    sp.payment_date AS salePaymentPaymentDate,
    s.total_amount AS totalAmount,
    s.status AS status
FROM SALE s
    LEFT JOIN CUSTOMER c ON s.customer_id = c.id
    LEFT JOIN SALE_ITEM si ON si.sale_id = s.id
    LEFT JOIN PRODUCT p ON si.product_id = p.id
    LEFT JOIN SALE_PAYMENT sp ON sp.sale_id = s.id
WHERE 1=1
-- <#if customerId??>
AND c.id = :customerId
-- </#if>
-- <#if params??>
-- <#list params as item>
AND ${item} = :${item}
-- </#list>
-- </#if>
-- <#if filterId??>
AND s.id = :filterId
-- </#if>
-- <#if filterName??>
AND c.full_name like :filterName
-- </#if>
ORDER BY s.id, si.id, sp.id

findSalesCustomers.sql

-- Native query to fetch aggregated sales by customer
SELECT
    s.id AS id,
    s.total_amount AS totalAmount,
    s.status AS status,
    c.id AS customerId,
    c.full_name AS customerFullName,
    c.email AS customerEmail,
    c.active AS customerActive
FROM SALE s
    LEFT JOIN CUSTOMER c ON s.customer_id = c.id
ORDER BY s.id

Testing Endpoints with cURL

Below are examples of cURL commands to test the main endpoints of the application:

1. Fetch all sales

curl -X GET http://localhost:8080/sales

2. Fetch sales by customer ID

curl -X GET http://localhost:8080/sales/customer/1

3. Fetch filtered sales (POST with JSON)

curl -X POST http://localhost:8080/sales/filter \
  -H "Content-Type: application/json" \
  -d '{"customerName":"smith"}'

4. Fetch dynamic sales

curl -X GET http://localhost:8080/sales/dynamic

5. Fetch aggregated sales by customer

curl -X GET http://localhost:8080/sales/customers

6. Fetch aggregated sales by customer (paginated)

curl -X GET "http://localhost:8080/sales/customers/pageable?page=0&size=10"

License

This project is licensed under the MIT License. See the LICENSE file for details.


Breaking Notes

Template Engine Change:

The project previously used Jtwig as the SQL template engine. However, Jtwig has been discontinued, so the template engine was replaced with Freemarker. All SQL file templating now uses Freemarker syntax. This change improves maintainability and future compatibility.


Notes


Contribute and Join Us

We welcome contributions from the community! If you have ideas, improvements, or bug fixes, feel free to open issues or submit pull requests on our GitHub repository.

Together, we can make Spring Native Query even better for everyone!