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
This commit is contained in:
heowc
2022-03-08 18:32:23 +09:00
committed by Jens Schauder
parent f7c5310dc9
commit 60ae66d4e7
3 changed files with 164 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ import org.springframework.util.Assert;
* @author Nicolas Cirigliano
* @author Jens Schauder
* @author Сергей Цыпанов
* @author Wonchul Heo
*/
public abstract class AbstractJpaQuery implements RepositoryQuery {
@@ -143,13 +144,21 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
@Nullable
private Object doExecute(JpaQueryExecution execution, Object[] values) {
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(method.getParameters(), values);
JpaParametersParameterAccessor accessor = obtainParameterAccessor(values);
Object result = execution.execute(this, accessor);
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
}
private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) {
if (provider == PersistenceProvider.HIBERNATE) {
return new HibernateJpaParametersParameterAccessor(method.getParameters(), values, em);
} else {
return new JpaParametersParameterAccessor(method.getParameters(), values);
}
}
protected JpaQueryExecution getExecution() {
JpaQueryExecution execution = this.execution.getNullable();

View File

@@ -0,0 +1,86 @@
package org.springframework.data.jpa.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Method;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import org.hibernate.jpa.TypedParameterValue;
import org.hibernate.type.StandardBasicTypes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Unit test for {@link JpaParametersParameterAccessor}.
*
* @author Wonchul Heo
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:infrastructure.xml")
class JpaParametersParameterAccessorTests {
@PersistenceContext
private EntityManager em;
private Query query;
@BeforeEach
void setUp() {
query = mock(Query.class);
}
@Test // GH-2370
void createsJpaParametersParameterAccessor() throws Exception {
Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class);
Object[] values = { null };
JpaParameters parameters = new JpaParameters(withNativeQuery);
JpaParametersParameterAccessor accessor = new JpaParametersParameterAccessor(parameters, values);
bind(parameters, accessor);
verify(query).setParameter(eq(1), isNull());
}
@Test // GH-2370
void createsHibernateParametersParameterAccessor() throws Exception {
Method withNativeQuery = SampleRepository.class.getMethod("withNativeQuery", Integer.class);
Object[] values = { null };
JpaParameters parameters = new JpaParameters(withNativeQuery);
JpaParametersParameterAccessor accessor =
new HibernateJpaParametersParameterAccessor(parameters, values, em);
bind(parameters, accessor);
ArgumentCaptor<TypedParameterValue> captor = ArgumentCaptor.forClass(TypedParameterValue.class);
verify(query).setParameter(eq(1), captor.capture());
TypedParameterValue captorValue = captor.getValue();
assertThat(captorValue.getType()).isEqualTo(StandardBasicTypes.INTEGER);
assertThat(captorValue.getValue()).isNull();
}
private void bind(JpaParameters parameters, JpaParametersParameterAccessor accessor) {
ParameterBinderFactory.createBinder(parameters).bind(QueryParameterSetter.BindableQuery.from(query),
accessor,
QueryParameterSetter.ErrorHandling.LENIENT);
}
interface SampleRepository {
@org.springframework.data.jpa.repository.Query(
value = "select 1 from user where age = :age",
nativeQuery = true)
User withNativeQuery(Integer age);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2017-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.query;
import javax.persistence.EntityManager;
import org.hibernate.Session;
import org.hibernate.TypeHelper;
import org.hibernate.jpa.TypedParameterValue;
import org.hibernate.type.Type;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
/**
* {@link org.springframework.data.repository.query.ParameterAccessor} based on an {@link Parameters} instance.
* In addition to the {@link JpaParametersParameterAccessor} functions, the bindable value is provided by
* fetching the method type when there is null.
*
* @author Wonchul Heo
*/
public class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAccessor {
private final TypeHelper typeHelper;
/**
* Creates a new {@link ParametersParameterAccessor}.
*
* @param parameters must not be {@literal null}.
* @param values must not be {@literal null}.
* @param em must not be {@literal null}.
*/
HibernateJpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values, EntityManager em) {
super(parameters, values);
Session session = em.unwrap(Session.class);
this.typeHelper = session.getSessionFactory().getTypeHelper();
}
public Object getValue(Parameter parameter) {
Object value = super.getValue(parameter.getIndex());
if (value == null) {
Type type = typeHelper.basic(parameter.getType());
if (type == null) {
return null;
}
return new TypedParameterValue(type, null);
}
return value;
}
@Override
public Object[] getValues() {
return super.getValues();
}
}