Skip to main content

Command Palette

Search for a command to run...

Hibernate 6: custom SQL functions in Spring Data JPA repositories

Updated
โ€ข7 min readโ€ขView as Markdown
Hibernate 6: custom SQL functions in Spring Data JPA repositories
A
I'm Arnau Aregall, a hands-on Tech Lead with 13+ years of experience building software systems with passion, across Banking, Insurance, HR Tech, and enterprise platforms. I work at the intersection of software architecture, engineering culture, and product delivery. Technically I specialize in backend engineering: Java, Spring Framework, microservices, distributed systems, cloud native applications, and scalable data platforms for B2B/B2C use-cases are my speciality, but I'm equally comfortable steering end-to-end web solutions across the full stack with Angular, React, and Next.js. I believe in building systems that last: SOLID principles, DDD, thoughtful application of design patterns, and a strong focus on code quality and testability are part of how I approach technical decisions โ€” spanning cloud architecture and security, Infrastructure as Code, GitOps, CI/CD, observability, and site reliability.

Introduction

This article will walk through the process of configuring custom SQL functions using Hibernate 6 and Spring Data.

The goal is to demonstrate, using Hibernate 6 and Spring Data JPA:

  • How to declare our custom SQL functions on our codebase (not as database objects) using Hibernate API.

  • How to call our functions from our queries using Hibernate Query Language (HQL) or JPA specifications.

TLDR; Show me the code!

The full source code of the article is published on GitHub ๐Ÿ˜‰

https://github.com/ArnauAregall/hibernate-6-custom-functions

Why would we need a custom Hibernate function?

On many occasions, I have found myself managing entities whose table column declarations are not what we know as basic types (varchar, bigint, etc).

If we are taking real benefit of Postgres, it's quite common that our table columns are arrays of any type or even JSON objects (jsonb).

When we need to query those types of columns from our application code, Spring Data query methods are quite limited even though those generate good criteria queries for most of our cases.

We often find ourselves in need of implementing a custom SQL function, which probably will make use of the native SQL syntax of our database platform or maybe even some database plugins.

To be able to call those functions in HQL queries or JPA specifications, we need to register the functions so Hibernate knows how to transform the HQL to SQL syntax.

Of course, a shortcut to solve the problem would be using native SQL syntax in our Spring repositories methods with @Query("select * from my_table where my_custom_function(my_column)", nativeQuery=true) and declaring our custom SQL functions as database objects. But as you know, every project has its own rules or restrictions, and maybe even that function needs to be used in a complex JPA specification - in those cases, we will be likely forced to register the function.

You may think that it's worth doing some extra weight-lifting in this regard when we have a non-trivial or very particular need in our business logic - in the next section, I will show you a trivial example that still requires custom functions.

Understanding by example: querying Products by tags

Let's imagine that our e-commerce application has a domain model named Product that represents a product we want to sell online.

It would have an internal ID and an SKU code, the product name and its price, and also some tags so we could quickly find similar products.

Our database schema table would have the following structure on Postgres:

CREATE TABLE IF NOT EXISTS product (
    id UUID UNIQUE NOT NULL,
    sku VARCHAR(100) UNIQUE NOT NULL,
    name VARCHAR(200) NOT NULL,
    price FLOAT NOT NULL,
    tags varchar[],
    PRIMARY KEY (id)
);

The table structure may sound quite familiar. The only particularity is we have a column named tags which is an array of varchars.

Let's say our application's functional requirement is to be able to filter Products by tags. Logically the criteria would be "find all products where the tags array contains electronics".

The SQL query using Postgres dialect would look as follows:

select p.* from products p where (p.tags @> '{"electronics"}');

On our application layer, we would first map the table to a JPA entity as follows:

package tech.aaregall.lab.hibernate6.persistence.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import java.util.UUID;

@Entity
@Table(name = "product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String sku;
    private String name;
    private Float price;
    private String[] tags;

    protected Product() {}

    public Product(String sku, String name, Float price, String[] tags) {
        this.sku = sku;
        this.name = name;
        this.price = price;
        this.tags = tags;
    }

    // getters & setters ommited for brevity
}

Then, using Spring Data JPA repositories, we would likely implement a query method to filter Products whose tags contain the required tag.

package tech.aaregall.lab.hibernate6.persistence.repository;

import org.springframework.data.repository.CrudRepository;
import tech.aaregall.lab.hibernate6.persistance.entity.Product;
import java.util.Collection;
import java.util.UUID;

public interface ProductRepository extends CrudRepository<Product, UUID> {

    Collection<Product> findAllByTagsContaining(String tag);

}

Quite familiar once again right?

Well, I am sorry to tell you that this approach will not work.

Our Spring application will fail when spinning up the context - the Spring Data JPA query lookup will fail while transforming the query method name to a JPA criteria by throwing the following exception:

org.hibernate.query.SemanticException: Operand of 'member of' operator must be a plural path

It would be logical to try changing the tags property mapping from a String[] to a type that JPA can recognize as a "plural" path on the criteria, as the error complains - maybe something like a collection:

@JdbcTypeCode(org.hibernate.type.SqlTypes.ARRAY)
private Set<String> tags;

Well, that will not work either - the column mapping would be compliant for JPA, but the same SemanticException will be thrown when starting our application. The query method findAllByTagsContaining still cannot be correctly transformed to a JPA criteria.

Is in this kind of situation that we may need to register custom SQL functions and implement our queries in HQL.

How to register custom functions in Hibernate 6

There are three key components or steps involved when registering custom functions:

    1. Our custom function implementation: a class containing the function rendering logic.
    1. The function contributor: a class that will register our custom function with a given function name.
    1. The Hibernate meta-information file: we will need to give a hint to Hibernate so it knows that we want to use at least one custom function contributor that takes care of registering our custom functions.

Implementing a custom function

Following the previous example, we will create a function that renders the SQL code to implement an "array contains" criteria.

The function will require exactly two arguments (the array and the value to look for in it) and will return a boolean.

This can be achieved by extending org.hibernate.dialect.function.StandardSQLFunction class and overriding the render method.

package tech.aaregall.lab.hibernate6.persistence.functions;

import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.BasicTypeReference;
import org.hibernate.type.SqlTypes;

import java.util.List;

class ArrayContainsSQLFunction extends StandardSQLFunction {

    private static final BasicTypeReference<Boolean> RETURN_TYPE = new BasicTypeReference<>("boolean", Boolean.class, SqlTypes.BOOLEAN);

    ArrayContainsSQLFunction(final String functionName) {
        super(functionName, true, RETURN_TYPE);
    }

    @Override
    public void render(SqlAppender sqlAppender, List<? extends SqlAstNode> arguments, SqlAstTranslator<?> translator) {
        if (arguments.size() != 2) {
            throw new IllegalArgumentException(STR."Function '\{getName()}' requires exactly 2 arguments");
        }

        sqlAppender.append("(");
        arguments.get(0).accept(translator);
        sqlAppender.append(" @> ARRAY[");
        arguments.get(1).accept(translator);
        sqlAppender.append("])");
    }

}

Registering the function with a FunctionContributor

We will register our custom function with the name array_contains.

We can achieve that by creating a new public class that implements the org.hibernate.boot.model.FunctionContributor interface, and overrides the contributeFunctions method.

package tech.aaregall.lab.hibernate6.persistence.functions;

import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.FunctionContributor;

public class CustomFunctionsContributor implements FunctionContributor {

    @Override
    public void contributeFunctions(FunctionContributions functionContributions) {
        functionContributions.getFunctionRegistry()
                .register("array_contains", new ArrayContainsSQLFunction("array_contains"));
    }
}

The Hibernate meta hint file

The last step will be creating the Hibernate meta hint file in our classpath to instruct we want to register a custom FunctionContributor:

src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor

And on that file, add our custom function contributor full class name:

tech.aaregall.lab.hibernate6.persistence.functions.CustomFunctionsContributor

Calling custom functions from Spring JPA Repositories

Now that we have our custom function registered in Hibernate, we can call it either using HQL syntax on @Query methods, or using a JPA specification in a Spring Data Repository.

package tech.aaregall.lab.hibernate6.persistence.repository;

import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import tech.aaregall.lab.hibernate6.persistance.entity.Product;

import java.util.Collection;
import java.util.UUID;

public interface ProductRepository extends CrudRepository<Product, UUID>, JpaSpecificationExecutor<Product> {

    // HQL
    @Query("select p from Product p where array_contains(p.tags, :tag)")
    Collection<Product> findAllByTagsContaining(@Param("tag") String tag);

    // JPA specification
    default Collection<Product> findAllByTagsContaining_Specification(final String tag) {
        return findAll((root, criteriaQuery, criteriaBuilder) ->
                criteriaBuilder.isTrue(
                        criteriaBuilder.function("array_contains", Boolean.class,
                                root.get("tags").as(String[].class), 
                                criteriaBuilder.literal(tag))
                )
        );
    }

}

Implementing repository integration tests

We will create an integration test configuration class that we will declare a Postgres Docker container bean, using Testcontainers.

package tech.aaregall.lab.hibernate6.config;

import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;

@Configuration
class TestConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer<?> postgresContainer() {
        return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
    }

}

And finally, implement the basic ProductRepository integration tests for our query methods.

@SpringBootTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @BeforeEach
    void beforeEach() {
        productRepository.deleteAll();
    }

    @Test
    void findAllByTagsContaining() {
        productRepository.saveAll(List.of(
                new Product("sku-111", "Dog Food", 30.99f, new String[] {"pets", "home", "dogs", "animals"}),
                new Product("sku-222", "Smart TV", 899.99f, new String[] {"electronics", "home"})
        ));

        final Collection<Product> result = productRepository.findAllByTagsContaining("electronics");

        assertThat(result)
                .hasSize(1)
                .extracting(Product::getSku)
                .containsExactly("sku-222");
    }

    @Test
    void findAllByTagsContaining_Specification() {
        productRepository.saveAll(List.of(
                new Product("sku-333", "Running Shoes", 64.55f, new String[] {"sport", "running", "footing"}),
                new Product("sku-444", "Swimsuit", 30.99f, new String[] {"sport", "swimming", "water polo"})
        ));

        final Collection<Product> result = productRepository.findAllByTagsContaining_Specification("swimming");

        assertThat(result)
                .hasSize(1)
                .extracting(Product::getSku)
                .containsExactly("sku-444");
    }

}

Conclusions

This article demonstrated:

  • Situations when registering custom SQL functions into Hibernate can be helpful.

  • Identifying the key components to register a custom SQL function using Hibernate 6.

  • How to implement a custom SQL function using Hibernate 6.

  • How to call our custom SQL function in Spring Data JPA repositories either by using an HQL query or a JPA specification.

  • How to implement simple yet assertive integration tests for our queries.

The source code of the example application is available in the linked GitHub repository.

If you enjoyed the article, let me know by dropping a star โญ๏ธ on the GitHub repository!

O

Hi, I have a custom function that returns a table, I would like to treat the return value as a list of complex objects, do you know if it is possible to register it?

D

Great article, thanks for that! One minor note from my side - it didn't work until I changed the following line:

'public class CustomFunctionsContributor extends PostgreSQLDialect implements FunctionContributor {...}'

Perhaps it would be useful to someone :)

A

Thanks for your kind words and tip!

As of Hibernate 6 it should not be needed to extend the Hibernate SQL dialect classes in order to register a FunctionContributor following the steps described in this article.

If there's a specific need to define the Hibernate dialect in your project, the approach described should be compatible with the standard Hibernate dialect definition classes.

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

P

Very Useful article. I want to know one thing. If the return type of the custom sql function in Array, then How should one handle return type of the function

A

Thanks for your comment!

If I got it correctly, you would like to handle that your custom SQL functions returns an array.

Then you could register your function with your aimed return type with a BasicTypeReference instance.

private static final BasicTypeReference<String[]> RETURN_TYPE = new BasicTypeReference<>("string[]", String[].class, SqlTypes.ARRAY);

As an example, a function called array_first_and_last that gets the first and last elements of the array would be somehow like this in the post example:

select array[p.tags[1], p.tags[array_length(p.tags, 1)]] 
from products p
 where ...

After a correct registering and rendering implementation, you should be able to use it in a JPA repository as follows:

@Query("select array_first_and_last(p.tags) as array from Product p where p.sku = :sku")
String[] getFirstAndLastTagsBySku(@\Param("sku") String sku);

Hope it helps! Cheers ๐Ÿ‘

T

Great article that provides real value, thank you.

1
A

Thank you! Glad you find it useful, this small feedback means a lot to me. Cheers! ๐Ÿ‘