Skip to content

Commit fb6d7a3

Browse files
committed
Restrict TypedParameterValue usage to native queries only.
Use Hibernate parameter accessor for native queries only to avoid affecting JPQL queries. See #3137
1 parent 09a7e7a commit fb6d7a3

File tree

6 files changed

+105
-17
lines changed

6 files changed

+105
-17
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/HibernateJpaParametersParameterAccessor.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
* @author Greg Turnquist
4141
* @since 2.7
4242
*/
43-
class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAccessor {
43+
public class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAccessor {
4444

4545
private final BasicTypeRegistry typeHelper;
4646

@@ -51,7 +51,7 @@ class HibernateJpaParametersParameterAccessor extends JpaParametersParameterAcce
5151
* @param values must not be {@literal null}.
5252
* @param em must not be {@literal null}.
5353
*/
54-
HibernateJpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values, EntityManager em) {
54+
public HibernateJpaParametersParameterAccessor(Parameters<?, ?> parameters, Object[] values, EntityManager em) {
5555

5656
super(parameters, values);
5757

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.stream.Collectors;
3333

3434
import org.springframework.core.convert.converter.Converter;
35+
import org.springframework.data.jpa.provider.HibernateJpaParametersParameterAccessor;
3536
import org.springframework.data.jpa.provider.PersistenceProvider;
3637
import org.springframework.data.jpa.repository.EntityGraph;
3738
import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution;
@@ -153,7 +154,11 @@ private Object doExecute(JpaQueryExecution execution, Object[] values) {
153154

154155
private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) {
155156

156-
return provider.getParameterAccessor(method.getParameters(), values, em);
157+
if (method.isNativeQuery() && PersistenceProvider.HIBERNATE.equals(provider)) {
158+
return new HibernateJpaParametersParameterAccessor(method.getParameters(), values, em);
159+
}
160+
161+
return new JpaParametersParameterAccessor(method.getParameters(), values);
157162
}
158163

159164
protected JpaQueryExecution getExecution() {

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java

+7-11
Original file line numberDiff line numberDiff line change
@@ -233,31 +233,27 @@ public boolean isIsNullParameter() {
233233
/**
234234
* Prepares the object before it's actually bound to the {@link jakarta.persistence.Query;}.
235235
*
236-
* @param value must not be {@literal null}.
236+
* @param value the value to be prepared.
237237
*/
238238
@Nullable
239239
public Object prepare(Object value) {
240240

241-
Assert.notNull(value, "Value must not be null");
242-
243-
Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(value);
244-
245-
if (unwrapped == null || expression.getJavaType() == null) {
246-
return unwrapped;
241+
if (value == null || expression.getJavaType() == null) {
242+
return value;
247243
}
248244

249245
if (String.class.equals(expression.getJavaType()) && !noWildcards) {
250246

251247
switch (type) {
252248
case STARTING_WITH:
253-
return String.format("%s%%", escape.escape(unwrapped.toString()));
249+
return String.format("%s%%", escape.escape(value.toString()));
254250
case ENDING_WITH:
255-
return String.format("%%%s", escape.escape(unwrapped.toString()));
251+
return String.format("%%%s", escape.escape(value.toString()));
256252
case CONTAINING:
257253
case NOT_CONTAINING:
258-
return String.format("%%%s%%", escape.escape(unwrapped.toString()));
254+
return String.format("%%%s%%", escape.escape(value.toString()));
259255
default:
260-
return unwrapped;
256+
return value;
261257
}
262258
}
263259

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java

+45
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
package org.springframework.data.jpa.repository.query;
1717

1818
import static org.assertj.core.api.Assumptions.assumeThat;
19+
import static org.assertj.core.api.Assertions.assertThat;
1920
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.ArgumentMatchers.eq;
2022
import static org.mockito.Mockito.mock;
2123
import static org.mockito.Mockito.never;
2224
import static org.mockito.Mockito.verify;
@@ -36,7 +38,9 @@
3638
import org.junit.jupiter.api.BeforeEach;
3739
import org.junit.jupiter.api.Test;
3840
import org.junit.jupiter.api.extension.ExtendWith;
41+
import org.mockito.ArgumentCaptor;
3942
import org.springframework.data.jpa.domain.sample.User;
43+
import org.springframework.data.jpa.provider.HibernateJpaParametersParameterAccessor;
4044
import org.springframework.data.jpa.provider.PersistenceProvider;
4145
import org.springframework.data.jpa.repository.EntityGraph;
4246
import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType;
@@ -65,12 +69,14 @@ class AbstractJpaQueryTests {
6569

6670
private Query query;
6771
private TypedQuery<Long> countQuery;
72+
private JpaQueryExecution execution;
6873

6974
@BeforeEach
7075
@SuppressWarnings("unchecked")
7176
void setUp() {
7277
query = mock(Query.class);
7378
countQuery = mock(TypedQuery.class);
79+
execution = mock(JpaQueryExecution.class);
7480
}
7581

7682
@Test // DATADOC-97
@@ -150,6 +156,37 @@ void shouldAddEntityGraphHintForLoad() throws Exception {
150156
verify(result).setHint("jakarta.persistence.loadgraph", entityGraph);
151157
}
152158

159+
@Test // GH-3137
160+
void shouldCreateHibernateJpaParameterParametersAccessorForNativeQuery() throws Exception {
161+
162+
JpaQueryMethod queryMethod = getMethod("findByLastnameNativeQuery", String.class);
163+
164+
AbstractJpaQuery jpaQuery = new DummyJpaQuery(queryMethod, em);
165+
166+
jpaQuery.execute(new Object[] {"some last name"});
167+
168+
ArgumentCaptor<JpaParametersParameterAccessor> captor = ArgumentCaptor.forClass(JpaParametersParameterAccessor.class);
169+
verify(execution).execute(eq(jpaQuery), captor.capture());
170+
JpaParametersParameterAccessor parameterAccessor = captor.getValue();
171+
172+
assertThat(parameterAccessor).isInstanceOf(HibernateJpaParametersParameterAccessor.class);
173+
}
174+
175+
@Test // GH-3137
176+
void shouldCreateGenericJpaParameterParametersAccessorForNonNativeQuery() throws Exception {
177+
178+
JpaQueryMethod queryMethod = getMethod("findByFirstname", String.class);
179+
AbstractJpaQuery jpaQuery = new DummyJpaQuery(queryMethod, em);
180+
181+
jpaQuery.execute(new Object[] {"some first name"});
182+
183+
ArgumentCaptor<JpaParametersParameterAccessor> captor = ArgumentCaptor.forClass(JpaParametersParameterAccessor.class);
184+
verify(execution).execute(eq(jpaQuery), captor.capture());
185+
JpaParametersParameterAccessor parameterAccessor = captor.getValue();
186+
187+
assertThat(parameterAccessor).isNotInstanceOf(HibernateJpaParametersParameterAccessor.class);
188+
}
189+
153190
private JpaQueryMethod getMethod(String name, Class<?>... parameterTypes) throws Exception {
154191

155192
Method method = SampleRepository.class.getMethod(name, parameterTypes);
@@ -164,6 +201,9 @@ interface SampleRepository extends Repository<User, Integer> {
164201
@QueryHints({ @QueryHint(name = "foo", value = "bar") })
165202
List<User> findByLastname(String lastname);
166203

204+
@org.springframework.data.jpa.repository.Query(value = "select u from User u where u.lastname = ?1", nativeQuery = true)
205+
List<User> findByLastnameNativeQuery(String lastname);
206+
167207
@QueryHints(value = { @QueryHint(name = "bar", value = "foo") }, forCounting = false)
168208
List<User> findByFirstname(String firstname);
169209

@@ -186,6 +226,11 @@ class DummyJpaQuery extends AbstractJpaQuery {
186226
super(method, em);
187227
}
188228

229+
@Override
230+
protected JpaQueryExecution getExecution() {
231+
return execution;
232+
}
233+
189234
@Override
190235
protected Query doCreateQuery(JpaParametersParameterAccessor accessor) {
191236
return query;

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java

+32-1
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,32 @@
2222

2323
import jakarta.persistence.criteria.CriteriaBuilder;
2424

25-
import org.junit.jupiter.api.Test;
25+
import org.eclipse.persistence.internal.jpa.querydef.ParameterExpressionImpl;
2626
import org.springframework.data.repository.query.Parameters;
2727
import org.springframework.data.repository.query.parser.Part;
2828

29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.Answers;
32+
import org.mockito.Mock;
33+
import org.mockito.junit.jupiter.MockitoExtension;
34+
import org.mockito.junit.jupiter.MockitoSettings;
35+
import org.mockito.quality.Strictness;
36+
2937
/**
3038
* Unit tests for {@link ParameterMetadataProvider}.
3139
*
3240
* @author Jens Schauder
41+
* @author Julia Lee
3342
*/
43+
@ExtendWith(MockitoExtension.class)
44+
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
3445
class ParameterMetadataProviderUnitTests {
3546

47+
@Mock(answer = Answers.RETURNS_DEEP_STUBS) Part part;
48+
49+
private ParameterExpressionImpl parameterExpression = new ParameterExpressionImpl(null, String.class);
50+
3651
@Test // DATAJPA-863
3752
void errorMessageMentionesParametersWhenParametersAreExhausted() {
3853

@@ -49,4 +64,20 @@ void errorMessageMentionesParametersWhenParametersAreExhausted() {
4964
.withMessageContaining("parameter");
5065
}
5166

67+
@Test // GH-3137
68+
void returnAugmentedValueForStringExpressions() {
69+
when(part.getProperty().getLeafProperty().isCollection()).thenReturn(false);
70+
71+
assertThat(createParameterMetadata(Part.Type.STARTING_WITH).prepare("starting with")).isEqualTo("starting with%");
72+
assertThat(createParameterMetadata(Part.Type.ENDING_WITH).prepare("ending with")).isEqualTo("%ending with");
73+
assertThat(createParameterMetadata(Part.Type.CONTAINING).prepare("containing")).isEqualTo("%containing%");
74+
assertThat(createParameterMetadata(Part.Type.NOT_CONTAINING).prepare("not containing")).isEqualTo("%not containing%");
75+
assertThat(createParameterMetadata(Part.Type.LIKE).prepare("%like%")).isEqualTo("%like%");
76+
assertThat(createParameterMetadata(Part.Type.IS_NULL).prepare(null)).isEqualTo(null);
77+
}
78+
79+
private ParameterMetadataProvider.ParameterMetadata createParameterMetadata(Part.Type partType) {
80+
when(part.getType()).thenReturn(partType);
81+
return new ParameterMetadataProvider.ParameterMetadata<>(parameterExpression, part, null, EscapeCharacter.DEFAULT);
82+
}
5283
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryWithNullLikeIntegrationTests.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class QueryWithNullLikeIntegrationTests {
6666
void setUp() {
6767
repository.saveAllAndFlush(List.of( //
6868
new EmployeeWithName("Frodo Baggins"), //
69-
new EmployeeWithName("Bilbo Baggins")));
69+
new EmployeeWithName("Bilbo Baggins"),
70+
new EmployeeWithName(null)));
7071
}
7172

7273
@Test
@@ -273,7 +274,14 @@ void mismatchedReturnTypeShouldCauseException() {
273274
@Test // GH-1184
274275
void alignedReturnTypeShouldWork() {
275276
assertThat(repository.customQueryWithAlignedReturnType()).containsExactly(new Object[][] {
276-
{ "Frodo Baggins", "Frodo Baggins with suffix" }, { "Bilbo Baggins", "Bilbo Baggins with suffix" } });
277+
{ "Frodo Baggins", "Frodo Baggins with suffix" }, { "Bilbo Baggins", "Bilbo Baggins with suffix" }, { null, null} });
278+
}
279+
280+
@Test
281+
void nullOptionalParameterShouldReturnAllEntries() {
282+
List<EmployeeWithName> result = repository.customQueryWithOptionalParameter(null);
283+
284+
assertThat(result).hasSize(3);
277285
}
278286

279287
@Transactional
@@ -291,6 +299,9 @@ public interface EmployeeWithNullLikeRepository extends JpaRepository<EmployeeWi
291299
@Query(value = "select * from EmployeeWithName as e where e.name like %:partialName%", nativeQuery = true)
292300
List<EmployeeWithName> customQueryWithNullableParamInNative(@Nullable @Param("partialName") String partialName);
293301

302+
@Query("select e from EmployeeWithName e where (:partialName is null or e.name like %:partialName%)")
303+
List<EmployeeWithName> customQueryWithOptionalParameter(@Nullable @Param("partialName") String partialName);
304+
294305
List<EmployeeWithName> findByNameStartsWith(@Nullable String partialName);
295306

296307
List<EmployeeWithName> findByNameEndsWith(@Nullable String partialName);

0 commit comments

Comments
 (0)