diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 8709375502..821d1d342b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -60,6 +60,7 @@ * @author Nicolas Cirigliano * @author Jens Schauder * @author Сергей Цыпанов + * @author Wonchul Heo */ public abstract class AbstractJpaQuery implements RepositoryQuery { @@ -151,13 +152,21 @@ public Object execute(Object[] parameters) { @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(); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java b/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java new file mode 100644 index 0000000000..3054f27fe1 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/HibernateJpaParametersParameterAccessor.java @@ -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(); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java new file mode 100644 index 0000000000..edc724b09e --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaParametersParameterAccessorTests.java @@ -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 javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.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 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); + } +}