1. Overview
Database functions are essential components in database management systems, enabling the encapsulation of logic and execution within the database. They facilitate efficient data processing and manipulation.
In this tutorial, we’ll explore various approaches to calling custom database functions within JPA and Spring Boot applications.
2. Project Setup
We’ll demonstrate the concepts in the subsequent sections using the H2 database.
Let’s include Spring Boot Data JPA and H2 dependencies in our pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
3. Database Functions
Database functions are database objects that perform specific tasks by executing a set of SQL statements or operations within the database. This enhances the performance when the logic is data intensive. Although database functions and stored procedures operate similarly, they exhibit differences.
3.1. Functions vs. Stored Procedures
While different database systems may have specific differences between them, the major differences between them can be summarized in the following table:
Feature
Database Functions
Stored Procedures
Invocation
Can be invoked within queries
Must be called explicitly
Return Value
Always return a single value
May return none, single, or multiple values
Parameter
Support input parameters only
Support both input and output parameters
Calling
Cannot call stored procedures with a function
Can call functions with a stored procedure
Usage
Typically perform calculations or data transformations
Often used for complex business logic
3.2. H2 Function
To illustrate invoking database functions from JPA, we’ll create a database function in H2 to illustrate how to invoke it from JPA. The H2 database function is simply embedded Java source code that will be compiled and executed:
CREATE ALIAS SHA256_HEX AS '
import java.sql.*;
@CODE
String getSha256Hex(Connection conn, String value) throws SQLException {
var sql = "SELECT RAWTOHEX(HASH(''SHA-256'', ?))";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, value);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString(1);
}
}
return null;
}
';
This database function SHA256_HEX accepts a single input argument as a string, processing it through the SHA-256 hashing algorithm, and subsequently returns the hexadecimal representation of its SHA-256 hash.
4. Invoking as a Stored Procedure
The first approach is to invoke database functions similar to stored procedures within JPA. We accomplish it by annotating the entity class by @NamedStoredProcedureQuery. This annotation allows us to specify the metadata of a stored procedure directly within the entity class.
Here’s an example of the Product entity class with the defined stored procedure SHA256_HEX:
@Entity
@Table(name = "product")
@NamedStoredProcedureQuery(
name = "Product.sha256Hex",
procedureName = "SHA256_HEX",
parameters = @StoredProcedureParameter(mode = ParameterMode.IN, name = "value", type = String.class)
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Integer id;
private String name;
// constructor, getters and setters
}
In the entity class, we annotate our Product entity class with @NamedStoredProcedureQuery. We assign Product.sha256Hex as the name of the named stored procedure.
In our repository definition, we annotate the repository method with the @Procedure and refer to the name of our @NamedStoredProcedureQuery. This repository method takes a string argument, then supplies it to the database function, and returns the result of the database function call.
public interface ProductRepository extends JpaRepository<Product, Integer> {
@Procedure(name = "Product.sha256Hex")
String getSha256HexByNamed(@Param("value") String value);
}
We’ll see Hibernate invoking it like calling a stored procedure from the Hibernate log upon execution:
Hibernate:
{call SHA256_HEX(?)}
@NamedStoredProcedureQuery is primarily designed for invoking stored procedures. The database functions can be invoked alone, similar to stored procedures as well. However, it may not be ideal for database functions used in conjunction with select queries.
5. Native Query
Another approach to call database functions is through native queries. There are two different ways to invoke database functions using a native query.
5.1. Native Call
From the previous Hibernate log, we can see that Hibernate executed a CALL command. Likewise, we can use the same command to invoke our database function natively:
public interface ProductRepository extends JpaRepository<Product, Integer> {
@Query(value = "CALL SHA256_HEX(:value)", nativeQuery = true)
String getSha256HexByNativeCall(@Param("value") String value);
}
The execution result will be the same as we saw in the example of using @NamedStoredProcedureQuery.
5.2. Native Select
As we depicted before, we couldn’t use it in conjunction with select queries. We’ll switch it to a select query and apply the function to a column value from a table. In our example, we define a repository method that uses a native select query to invoke our database function on the name column of the Product table:
public interface ProductRepository extends JpaRepository<Product, Integer> {
@Query(value = "SELECT SHA256_HEX(name) FROM product", nativeQuery = true)
String getProductNameListInSha256HexByNativeSelect();
}
Upon execution, we can get the identical query from the Hibernate log as we defined because we defined it as a native query:
Hibernate:
SELECT
SHA256_HEX(name)
FROM
product
6. Function Registration
Function registration is a Hibernate process of defining and registering custom database functions that can be used within JPA or Hibernate queries. This helps Hibernate translate the custom functions into the corresponding SQL statements.
6.1. Custom Dialect
We can register custom functions by creating a custom dialect. Here’s the custom dialect class that extends the default H2Dialect and registers our function:
public class CustomH2Dialect extends H2Dialect {
@Override
public void initializeFunctionRegistry(FunctionContributions functionContributions) {
super.initializeFunctionRegistry(functionContributions);
SqmFunctionRegistry registry = functionContributions.getFunctionRegistry();
TypeConfiguration types = functionContributions.getTypeConfiguration();
new PatternFunctionDescriptorBuilder(registry, "sha256hex", FunctionKind.NORMAL, "SHA256_HEX(?1)")
.setExactArgumentCount(1)
.setInvariantType(types.getBasicTypeForJavaType(String.class))
.register();
}
}
When Hibernate initializes a dialect, it registers available database functions to the function registry via initializeFunctionRegistry(). We override the initializeFunctionRegistry() method to register additional database functions that the default dialect doesn’t include.
PatternFunctionDescriptorBuilder creates a JPQL function mapping that maps our database functions SHA256_HEX to a JPQL function sha256Hex and registers the mapping to the function registry. The argument ?1 indicates the first input argument for the database function.
6.2. Hibernate Configuration
We have to instruct Spring Boot to adopt the CustomH2Dialect instead of the default H2Dialect. Here the HibernatePropertiesCustomizer that comes in place. It’s an interface provided by Spring Boot to customize properties used by Hibernate.
We override the customize() method to put an additional property to indicate we’ll use CustomH2Dialect:
@Configuration
public class CustomHibernateConfig implements HibernatePropertiesCustomizer {
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("hibernate.dialect", "com.baeldung.customfunc.CustomH2Dialect");
}
}
Spring Boot automatically detects and applies the customization during the application start-up.
6.3. Repository Method
In our repository, we can now use the JPQL query that applies the newly defined function sha256Hex instead of the native query:
public interface ProductRepository extends JpaRepository<Product, Integer> {
@Query(value = "SELECT sha256Hex(p.name) FROM Product p")
List<String> getProductNameListInSha256Hex();
}
When we check the Hibernate log upon the execution, we see that Hibernate correctly translated the JPQL sha256Hex function to our database function SHA256_HEX:
Hibernate:
select
SHA256_HEX(p1_0.name)
from
product p1_07'
7. Conclusion
In this article, we conducted a brief comparison between database functions and stored procedures. Both offer a powerful means to encapsulate logic and execute within the database.
Moreover, we have explored different approaches to call database functions, including utilizing @NamedStoredProcedureQuery annotation, native queries, and function registration via custom dialects. By incorporating database functions into repository methods, we can easily build database-driven applications.
As usual, the examples providing a practical resource of the concepts discussed are available over on GitHub.