Skip to content

Commit eda37df

Browse files
committed
Handle nulls in stored procedure parameterss properly.
Hibernate supports TypedParameterValue in queries, but not stored procedures. Until hibernate/hibernate-orm#5438 is adopted, we have to dereference such parameters on our end first. Closes #2544.
1 parent 700a7dd commit eda37df

File tree

6 files changed

+237
-11
lines changed

6 files changed

+237
-11
lines changed

src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18-
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.*;
18+
import static org.springframework.data.jpa.repository.query.QueryParameterSetter.ErrorHandling.LENIENT;
1919

2020
import java.lang.reflect.Proxy;
2121
import java.util.Collections;
@@ -27,11 +27,13 @@
2727

2828
import javax.persistence.Parameter;
2929
import javax.persistence.Query;
30+
import javax.persistence.StoredProcedureQuery;
3031
import javax.persistence.TemporalType;
3132
import javax.persistence.criteria.ParameterExpression;
3233

3334
import org.apache.commons.logging.Log;
3435
import org.apache.commons.logging.LogFactory;
36+
import org.hibernate.jpa.TypedParameterValue;
3537
import org.springframework.lang.Nullable;
3638
import org.springframework.util.Assert;
3739

@@ -83,7 +85,21 @@ class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter {
8385
public void setParameter(BindableQuery query, JpaParametersParameterAccessor accessor,
8486
ErrorHandling errorHandling) {
8587

86-
Object value = valueExtractor.apply(accessor);
88+
final Object value;
89+
90+
// TODO: When https://github.com/hibernate/hibernate-orm/pull/5438 is merged we should be able to drop this.
91+
if (query.getQuery() instanceof StoredProcedureQuery) {
92+
93+
Object extractedValue = valueExtractor.apply(accessor);
94+
95+
if (extractedValue instanceof TypedParameterValue) {
96+
value = ((TypedParameterValue) extractedValue).getValue();
97+
} else {
98+
value = extractedValue;
99+
}
100+
} else {
101+
value = valueExtractor.apply(accessor);
102+
}
87103

88104
if (temporalType != null) {
89105

src/test/java/org/springframework/data/jpa/repository/procedures/MySqlStoredProcedureIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import javax.persistence.Id;
3232
import javax.persistence.NamedStoredProcedureQuery;
3333
import javax.sql.DataSource;
34-
import javax.transaction.Transactional;
3534

3635
import org.hibernate.dialect.MySQL8Dialect;
3736
import org.junit.jupiter.api.Test;
@@ -54,6 +53,7 @@
5453
import org.springframework.test.context.junit.jupiter.SpringExtension;
5554
import org.springframework.transaction.PlatformTransactionManager;
5655
import org.springframework.transaction.annotation.EnableTransactionManagement;
56+
import org.springframework.transaction.annotation.Transactional;
5757
import org.testcontainers.containers.MySQLContainer;
5858

5959
import com.mysql.cj.jdbc.MysqlDataSource;
@@ -194,7 +194,7 @@ public interface EmployeeRepositoryWithNoCursor extends JpaRepository<Employee,
194194
static class Config {
195195

196196
@SuppressWarnings("resource")
197-
@Bean(initMethod = "start")
197+
@Bean(initMethod = "start", destroyMethod = "stop")
198198
public MySQLContainer<?> container() {
199199

200200
return new MySQLContainer<>("mysql:8.0.24") //

src/test/java/org/springframework/data/jpa/repository/procedures/PostgresStoredProcedureIntegrationTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import javax.persistence.ParameterMode;
3535
import javax.persistence.StoredProcedureParameter;
3636
import javax.sql.DataSource;
37-
import javax.transaction.Transactional;
3837

3938
import org.hibernate.dialect.PostgreSQL91Dialect;
4039
import org.junit.jupiter.api.Test;
@@ -58,6 +57,7 @@
5857
import org.springframework.test.context.junit.jupiter.SpringExtension;
5958
import org.springframework.transaction.PlatformTransactionManager;
6059
import org.springframework.transaction.annotation.EnableTransactionManagement;
60+
import org.springframework.transaction.annotation.Transactional;
6161
import org.testcontainers.containers.PostgreSQLContainer;
6262

6363
/**
@@ -197,7 +197,7 @@ public interface EmployeeRepositoryWithRefCursor extends JpaRepository<Employee,
197197
static class Config {
198198

199199
@SuppressWarnings("resource")
200-
@Bean(initMethod = "start")
200+
@Bean(initMethod = "start", destroyMethod = "stop")
201201
public PostgreSQLContainer<?> container() {
202202

203203
return new PostgreSQLContainer<>("postgres:9.6.12") //
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2015-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.procedures;
17+
18+
import lombok.AccessLevel;
19+
import lombok.AllArgsConstructor;
20+
import lombok.Data;
21+
import lombok.NoArgsConstructor;
22+
23+
import java.util.Date;
24+
import java.util.Properties;
25+
import java.util.UUID;
26+
27+
import javax.persistence.Entity;
28+
import javax.persistence.EntityManagerFactory;
29+
import javax.persistence.GeneratedValue;
30+
import javax.persistence.GenerationType;
31+
import javax.persistence.Id;
32+
import javax.sql.DataSource;
33+
34+
import org.hibernate.dialect.PostgreSQL91Dialect;
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.api.extension.ExtendWith;
37+
import org.postgresql.ds.PGSimpleDataSource;
38+
import org.springframework.beans.factory.annotation.Autowired;
39+
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.ComponentScan;
41+
import org.springframework.context.annotation.FilterType;
42+
import org.springframework.core.io.ClassPathResource;
43+
import org.springframework.data.jpa.repository.JpaRepository;
44+
import org.springframework.data.jpa.repository.Temporal;
45+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
46+
import org.springframework.data.jpa.repository.query.Procedure;
47+
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
48+
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
49+
import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean;
50+
import org.springframework.orm.jpa.JpaTransactionManager;
51+
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
52+
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
53+
import org.springframework.test.context.ContextConfiguration;
54+
import org.springframework.test.context.junit.jupiter.SpringExtension;
55+
import org.springframework.transaction.PlatformTransactionManager;
56+
import org.springframework.transaction.annotation.EnableTransactionManagement;
57+
import org.springframework.transaction.annotation.Transactional;
58+
import org.testcontainers.containers.PostgreSQLContainer;
59+
60+
/**
61+
* Testcase to verify {@link org.springframework.jdbc.object.StoredProcedure}s properly handle null values.
62+
*
63+
* @author Greg Turnquist
64+
*/
65+
@Transactional
66+
@ExtendWith(SpringExtension.class)
67+
@ContextConfiguration(classes = PostgresStoredProcedureNullHandlingIntegrationTests.Config.class)
68+
public class PostgresStoredProcedureNullHandlingIntegrationTests {
69+
70+
@Autowired TestModelRepository repository;
71+
72+
@Test // 2544
73+
void invokingNullOnNonTemporalStoredProcedureParameterShouldWork() {
74+
repository.countUuid(null);
75+
}
76+
77+
@Test // 2544
78+
void invokingNullOnTemporalStoredProcedureParameterShouldWork() {
79+
repository.countLocalDate(null);
80+
}
81+
82+
@Data
83+
@AllArgsConstructor
84+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
85+
@Entity
86+
public class TestModel {
87+
88+
@Id
89+
@GeneratedValue(strategy = GenerationType.AUTO) private long id;
90+
private UUID uuid;
91+
private Date date;
92+
}
93+
94+
@Transactional
95+
public interface TestModelRepository extends JpaRepository<TestModel, Long> {
96+
97+
@Procedure("countByUuid")
98+
void countUuid(UUID this_uuid);
99+
100+
@Procedure("countByLocalDate")
101+
void countLocalDate(@Temporal Date localDate);
102+
}
103+
104+
@EnableJpaRepositories(considerNestedRepositories = true,
105+
includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = TestModelRepository.class))
106+
@EnableTransactionManagement
107+
static class Config {
108+
109+
@Bean(initMethod = "start", destroyMethod = "stop")
110+
public PostgreSQLContainer<?> container() {
111+
112+
return new PostgreSQLContainer<>("postgres:9.6.12") //
113+
.withUsername("postgres");
114+
}
115+
116+
@Bean
117+
public DataSource dataSource(PostgreSQLContainer<?> container) {
118+
119+
PGSimpleDataSource dataSource = new PGSimpleDataSource();
120+
dataSource.setUrl(container.getJdbcUrl());
121+
dataSource.setUser(container.getUsername());
122+
dataSource.setPassword(container.getPassword());
123+
124+
return dataSource;
125+
}
126+
127+
@Bean
128+
public AbstractEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
129+
130+
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
131+
factoryBean.setDataSource(dataSource);
132+
factoryBean.setPersistenceUnitRootLocation("simple-persistence");
133+
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
134+
factoryBean.setPackagesToScan(this.getClass().getPackage().getName());
135+
136+
Properties properties = new Properties();
137+
properties.setProperty("hibernate.hbm2ddl.auto", "create");
138+
properties.setProperty("hibernate.dialect", PostgreSQL91Dialect.class.getCanonicalName());
139+
properties.setProperty("hibernate.proc.param_null_passing", "true");
140+
properties.setProperty("hibernate.globally_quoted_identifiers", "true");
141+
properties.setProperty("hibernate.globally_quoted_identifiers_skip_column_definitions", "true");
142+
factoryBean.setJpaProperties(properties);
143+
144+
return factoryBean;
145+
}
146+
147+
@Bean
148+
PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
149+
return new JpaTransactionManager(entityManagerFactory);
150+
}
151+
152+
@Bean
153+
DataSourceInitializer initializer(DataSource dataSource) {
154+
155+
DataSourceInitializer initializer = new DataSourceInitializer();
156+
initializer.setDataSource(dataSource);
157+
158+
ClassPathResource script = new ClassPathResource("scripts/postgres-nullable-stored-procedures.sql");
159+
ResourceDatabasePopulator populator = new ResourceDatabasePopulator(script);
160+
populator.setSeparator(";;");
161+
initializer.setDatabasePopulator(populator);
162+
163+
return initializer;
164+
}
165+
}
166+
}

src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.springframework.data.jpa.repository.query;
22

3-
import static org.assertj.core.api.Assertions.*;
4-
import static org.mockito.ArgumentMatchers.*;
5-
import static org.mockito.Mockito.*;
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.eq;
5+
import static org.mockito.ArgumentMatchers.isNull;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.verify;
68

79
import java.lang.reflect.Method;
810

@@ -44,7 +46,8 @@ void createsJpaParametersParameterAccessor() throws Exception {
4446
Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class);
4547
Object[] values = { null };
4648
JpaParameters parameters = new JpaParameters(withNativeQuery);
47-
JpaParametersParameterAccessor accessor = PersistenceProvider.GENERIC_JPA.getParameterAccessor(parameters, values, em);
49+
JpaParametersParameterAccessor accessor = PersistenceProvider.GENERIC_JPA.getParameterAccessor(parameters, values,
50+
em);
4851

4952
bind(parameters, accessor);
5053

@@ -71,7 +74,7 @@ void createsHibernateParametersParameterAccessor() throws Exception {
7174

7275
private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) {
7376

74-
ParameterBinderFactory.createBinder(parameters)
77+
ParameterBinderFactory.createBinder(parameters) //
7578
.bind( //
7679
QueryParameterSetter.BindableQuery.from(query), //
7780
accessor, //
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
CREATE TABLE test_model
2+
(
3+
ID numeric NOT NULL,
4+
uuid UUID,
5+
local_date DATE,
6+
CONSTRAINT test_model_pk PRIMARY KEY (ID)
7+
);;
8+
9+
CREATE OR REPLACE FUNCTION countByUuid(this_uuid uuid)
10+
RETURNS int
11+
LANGUAGE 'plpgsql'
12+
AS
13+
$BODY$
14+
DECLARE
15+
c integer;
16+
BEGIN
17+
SELECT count(*)
18+
INTO c
19+
FROM test_model
20+
WHERE test_model.uuid = this_uuid;
21+
RETURN c;
22+
END;
23+
$BODY$
24+
;;
25+
26+
CREATE OR REPLACE FUNCTION countByLocalDate(this_local_date DATE)
27+
RETURNS int
28+
LANGUAGE 'plpgsql'
29+
AS
30+
$BODY$
31+
DECLARE
32+
c integer;
33+
BEGIN
34+
SELECT count(*)
35+
INTO c
36+
FROM test_model
37+
WHERE test_model.local_date = this_local_date;
38+
RETURN c;
39+
END;
40+
$BODY$
41+
;;

0 commit comments

Comments
 (0)