From 1b843fd34a24fef67979bcd1e84667f50a7af236 Mon Sep 17 00:00:00 2001 From: Manfred <77407079+manfred106@users.noreply.github.com> Date: Wed, 21 Feb 2024 03:31:55 +0000 Subject: [PATCH] BAEL-7296: Calling Custom Database Functions with JPA (#15869) --- .../spring-boot-persistence-4/pom.xml | 6 ++ .../customfunc/CustomFunctionApplication.java | 13 +++++ .../baeldung/customfunc/CustomH2Dialect.java | 25 ++++++++ .../customfunc/CustomHibernateConfig.java | 16 +++++ .../java/com/baeldung/customfunc/Product.java | 31 ++++++++++ .../customfunc/ProductRepository.java | 24 ++++++++ .../ProductRepositoryIntegrationTest.java | 58 +++++++++++++++++++ .../src/test/resources/product-data.sql | 17 ++++++ 8 files changed, 190 insertions(+) create mode 100644 persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomFunctionApplication.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomH2Dialect.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomHibernateConfig.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/Product.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/ProductRepository.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/test/java/com/baeldung/customfunc/ProductRepositoryIntegrationTest.java create mode 100644 persistence-modules/spring-boot-persistence-4/src/test/resources/product-data.sql diff --git a/persistence-modules/spring-boot-persistence-4/pom.xml b/persistence-modules/spring-boot-persistence-4/pom.xml index 13cb4d5b82..acff937114 100644 --- a/persistence-modules/spring-boot-persistence-4/pom.xml +++ b/persistence-modules/spring-boot-persistence-4/pom.xml @@ -62,6 +62,11 @@ modelmapper ${modelmapper.version} + + commons-codec + commons-codec + ${commons-codec.version} + org.projectlombok lombok @@ -88,6 +93,7 @@ 3.7.0 2.16.0 3.2.0 + 1.16.1 1.18.30 diff --git a/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomFunctionApplication.java b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomFunctionApplication.java new file mode 100644 index 0000000000..6e731501cb --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomFunctionApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.customfunc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CustomFunctionApplication { + + public static void main(String[] args) { + SpringApplication.run(CustomFunctionApplication.class, args); + } + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomH2Dialect.java b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomH2Dialect.java new file mode 100644 index 0000000000..56b26a8fcf --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomH2Dialect.java @@ -0,0 +1,25 @@ +package com.baeldung.customfunc; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.dialect.H2Dialect; + +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.query.sqm.produce.function.PatternFunctionDescriptorBuilder; +import org.hibernate.type.spi.TypeConfiguration; + +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(); + } + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomHibernateConfig.java b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomHibernateConfig.java new file mode 100644 index 0000000000..424970f59a --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/CustomHibernateConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.customfunc; + +import org.springframework.context.annotation.Configuration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; + +import java.util.Map; + +@Configuration +public class CustomHibernateConfig implements HibernatePropertiesCustomizer { + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put("hibernate.dialect", "com.baeldung.customfunc.CustomH2Dialect"); + } + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/Product.java b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/Product.java new file mode 100644 index 0000000000..bfbea7a0c5 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/Product.java @@ -0,0 +1,31 @@ +package com.baeldung.customfunc; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "product") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = {"id"}) +@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; + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/ProductRepository.java b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/ProductRepository.java new file mode 100644 index 0000000000..ea9ceba0ff --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/main/java/com/baeldung/customfunc/ProductRepository.java @@ -0,0 +1,24 @@ +package com.baeldung.customfunc; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.Procedure; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ProductRepository extends JpaRepository { + + @Procedure(name = "Product.sha256Hex") + String getSha256HexByNamedMapping(@Param("value") String value); + + @Query(value = "CALL SHA256_HEX(:value)", nativeQuery = true) + String getSha256HexByNativeCall(@Param("value") String value); + + @Query(value = "SELECT SHA256_HEX(name) FROM product", nativeQuery = true) + List getProductNameListInSha256HexByNativeSelect(); + + @Query(value = "SELECT sha256Hex(p.name) FROM Product p") + List getProductNameListInSha256Hex(); + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/test/java/com/baeldung/customfunc/ProductRepositoryIntegrationTest.java b/persistence-modules/spring-boot-persistence-4/src/test/java/com/baeldung/customfunc/ProductRepositoryIntegrationTest.java new file mode 100644 index 0000000000..a5b2ebe461 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/test/java/com/baeldung/customfunc/ProductRepositoryIntegrationTest.java @@ -0,0 +1,58 @@ +package com.baeldung.customfunc; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@SpringBootTest(classes = CustomFunctionApplication.class, properties = { + "spring.jpa.show-sql=true", + "spring.jpa.properties.hibernate.format_sql=true", + "spring.jpa.generate-ddl=true", + "spring.jpa.defer-datasource-initialization=true", + "spring.sql.init.data-locations=classpath:product-data.sql" +}) +@Transactional +public class ProductRepositoryIntegrationTest { + + private static final String TEXT = "Hand Grip Strengthener"; + private static final String EXPECTED_HASH_HEX = getSha256Hex(TEXT); + + @Autowired + private ProductRepository productRepository; + + private static String getSha256Hex(String value) { + return Hex.encodeHexString(DigestUtils.sha256(value.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void whenGetSha256HexByNamedMapping_thenReturnCorrectHash() { + var hash = productRepository.getSha256HexByNamedMapping(TEXT); + assertThat(hash).isEqualTo(EXPECTED_HASH_HEX); + } + + @Test + void whenGetSha256HexByNativeCall_thenReturnCorrectHash() { + var hash = productRepository.getSha256HexByNativeCall(TEXT); + assertThat(hash).isEqualTo(EXPECTED_HASH_HEX); + } + + @Test + void whenCallGetSha256HexNative_thenReturnCorrectHash() { + var hashList = productRepository.getProductNameListInSha256HexByNativeSelect(); + assertThat(hashList.get(0)).isEqualTo(EXPECTED_HASH_HEX); + } + + @Test + void whenCallGetSha256Hex_thenReturnCorrectHash() { + var hashList = productRepository.getProductNameListInSha256Hex(); + assertThat(hashList.get(0)).isEqualTo(EXPECTED_HASH_HEX); + } + +} \ No newline at end of file diff --git a/persistence-modules/spring-boot-persistence-4/src/test/resources/product-data.sql b/persistence-modules/spring-boot-persistence-4/src/test/resources/product-data.sql new file mode 100644 index 0000000000..7a59da68b7 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-4/src/test/resources/product-data.sql @@ -0,0 +1,17 @@ +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; + } +'; + +INSERT INTO product(name) VALUES('Hand Grip Strengthener'); \ No newline at end of file