Skip to content

Commit 60ae66d

Browse files
heowcschauder
authored andcommitted
Null arguments now get wrapped in a TypedParameterValue for Hibernate.
This avoids problems where a JDBC driver fails to determine the correct type for such values. Closes #2370 Original pull request #2461
1 parent f7c5310 commit 60ae66d

File tree

3 files changed

+164
-1
lines changed

3 files changed

+164
-1
lines changed

Diff for: spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
* @author Nicolas Cirigliano
6161
* @author Jens Schauder
6262
* @author Сергей Цыпанов
63+
* @author Wonchul Heo
6364
*/
6465
public abstract class AbstractJpaQuery implements RepositoryQuery {
6566

@@ -143,13 +144,21 @@ public Object execute(Object[] parameters) {
143144
@Nullable
144145
private Object doExecute(JpaQueryExecution execution, Object[] values) {
145146

146-
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(method.getParameters(), values);
147+
JpaParametersParameterAccessor accessor = obtainParameterAccessor(values);
147148
Object result = execution.execute(this, accessor);
148149

149150
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
150151
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
151152
}
152153

154+
private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) {
155+
if (provider == PersistenceProvider.HIBERNATE) {
156+
return new HibernateJpaParametersParameterAccessor(method.getParameters(), values, em);
157+
} else {
158+
return new JpaParametersParameterAccessor(method.getParameters(), values);
159+
}
160+
}
161+
153162
protected JpaQueryExecution getExecution() {
154163

155164
JpaQueryExecution execution = this.execution.getNullable();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.springframework.data.jpa.repository.query;
2+
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;
8+
9+
import java.lang.reflect.Method;
10+
11+
import jakarta.persistence.EntityManager;
12+
import jakarta.persistence.PersistenceContext;
13+
import jakarta.persistence.Query;
14+
15+
import org.hibernate.jpa.TypedParameterValue;
16+
import org.hibernate.type.StandardBasicTypes;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.ExtendWith;
20+
import org.mockito.ArgumentCaptor;
21+
import org.springframework.data.jpa.domain.sample.User;
22+
import org.springframework.test.context.ContextConfiguration;
23+
import org.springframework.test.context.junit.jupiter.SpringExtension;
24+
25+
/**
26+
* Unit test for {@link JpaParametersParameterAccessor}.
27+
*
28+
* @author Wonchul Heo
29+
*/
30+
@ExtendWith(SpringExtension.class)
31+
@ContextConfiguration("classpath:infrastructure.xml")
32+
class JpaParametersParameterAccessorTests {
33+
34+
@PersistenceContext
35+
private EntityManager em;
36+
private Query query;
37+
38+
@BeforeEach
39+
void setUp() {
40+
query = mock(Query.class);
41+
}
42+
43+
@Test // GH-2370
44+
void createsJpaParametersParameterAccessor() throws Exception {
45+
46+
Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class);
47+
Object[] values = { null };
48+
JpaParameters parameters = new JpaParameters(withNativeQuery);
49+
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(parameters, values);
50+
51+
bind(parameters, accessor);
52+
53+
verify(query).setParameter(eq(1), isNull());
54+
}
55+
56+
@Test // GH-2370
57+
void createsHibernateParametersParameterAccessor() throws Exception {
58+
59+
Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class);
60+
Object[] values = { null };
61+
JpaParameters parameters = new JpaParameters(withNativeQuery);
62+
JpaParametersParameterAccessor accessor =
63+
new HibernateJpaParametersParameterAccessor(parameters, values, em);
64+
65+
bind(parameters, accessor);
66+
67+
ArgumentCaptor<TypedParameterValue> captor = ArgumentCaptor.forClass(TypedParameterValue.class);
68+
verify(query).setParameter(eq(1), captor.capture());
69+
TypedParameterValue captorValue = captor.getValue();
70+
assertThat(captorValue.getType()).isEqualTo(StandardBasicTypes.INTEGER);
71+
assertThat(captorValue.getValue()).isNull();
72+
}
73+
74+
private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) {
75+
ParameterBinderFactory.createBinder(parameters).bind(QueryParameterSetter.BindableQuery.from(query),
76+
accessor,
77+
QueryParameterSetter.ErrorHandling.LENIENT);
78+
}
79+
80+
interface SampleRepository {
81+
@org.springframework.data.jpa.repository.Query(
82+
value = "select 1 from user where age = :age",
83+
nativeQuery = true)
84+
User withNativeQuery(Integer age);
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2017-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.query;
17+
18+
import javax.persistence.EntityManager;
19+
20+
import org.hibernate.Session;
21+
import org.hibernate.TypeHelper;
22+
import org.hibernate.jpa.TypedParameterValue;
23+
import org.hibernate.type.Type;
24+
import org.springframework.data.repository.query.Parameter;
25+
import org.springframework.data.repository.query.Parameters;
26+
import org.springframework.data.repository.query.ParametersParameterAccessor;
27+
28+
/**
29+
* {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance.
30+
* In addition to the {@link JpaParametersParameterAccessor} functions, the bindable value is provided by
31+
* fetching the method type when there is null.
32+
*
33+
* @author Wonchul Heo
34+
*/
35+
public class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAccessor {
36+
37+
private final TypeHelper typeHelper;
38+
39+
/**
40+
* Creates a new {@link ParametersParameterAccessor}.
41+
*
42+
* @param parameters must not be {@literal null}.
43+
* @param values must not be {@literal null}.
44+
* @param em must not be {@literal null}.
45+
*/
46+
HibernateJpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values, EntityManager em) {
47+
super(parameters, values);
48+
Session session = em.unwrap(Session.class);
49+
this.typeHelper = session.getSessionFactory().getTypeHelper();
50+
}
51+
52+
public Object getValue(Parameter parameter) {
53+
Object value = super.getValue(parameter.getIndex());
54+
if (value == null) {
55+
Type type = typeHelper.basic(parameter.getType());
56+
if (type == null) {
57+
return null;
58+
}
59+
return new TypedParameterValue(type, null);
60+
}
61+
return value;
62+
}
63+
64+
@Override
65+
public Object[] getValues() {
66+
return super.getValues();
67+
}
68+
}

0 commit comments

Comments
 (0)