Skip to content

Commit 5b9d1d8

Browse files
committed
Support converting underscores column name to camel case property name for projecting native query
> By default, Spring Boot configures the physical naming strategy with CamelCaseToUnderscoresNamingStrategy. Then we should convert underscores back to camel case. Fix spring-projectsGH-3462
1 parent 8c1174f commit 5b9d1d8

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

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

+65-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Arrays;
2727
import java.util.Collection;
2828
import java.util.HashMap;
29+
import java.util.HashSet;
2930
import java.util.List;
3031
import java.util.Map;
3132
import java.util.Set;
@@ -47,6 +48,7 @@
4748
import org.springframework.data.repository.query.ResultProcessor;
4849
import org.springframework.data.repository.query.ReturnedType;
4950
import org.springframework.data.util.Lazy;
51+
import org.springframework.jdbc.support.JdbcUtils;
5052
import org.springframework.lang.Nullable;
5153
import org.springframework.util.Assert;
5254

@@ -62,6 +64,7 @@
6264
* @author Сергей Цыпанов
6365
* @author Wonchul Heo
6466
* @author Julia Lee
67+
* @author Yanming Zhou
6568
*/
6669
public abstract class AbstractJpaQuery implements RepositoryQuery {
6770

@@ -149,7 +152,7 @@ private Object doExecute(JpaQueryExecution execution, Object[] values) {
149152
Object result = execution.execute(this, accessor);
150153

151154
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
152-
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
155+
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType(), method.isNativeQuery()));
153156
}
154157

155158
private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) {
@@ -304,16 +307,30 @@ static class TupleConverter implements Converter<Object, Object> {
304307

305308
private final ReturnedType type;
306309

310+
private final boolean nativeQuery;
311+
307312
/**
308313
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
309314
*
310315
* @param type must not be {@literal null}.
311316
*/
312317
public TupleConverter(ReturnedType type) {
313318

319+
this(type, false);
320+
}
321+
322+
/**
323+
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
324+
*
325+
* @param type must not be {@literal null}.
326+
* @param nativeQuery is this converter for native query?
327+
*/
328+
public TupleConverter(ReturnedType type, boolean nativeQuery) {
329+
314330
Assert.notNull(type, "Returned type must not be null");
315331

316332
this.type = type;
333+
this.nativeQuery = nativeQuery;
317334
}
318335

319336
@Override
@@ -334,7 +351,7 @@ public Object convert(Object source) {
334351
}
335352
}
336353

337-
return new TupleBackedMap(tuple);
354+
return new TupleBackedMap(tuple, nativeQuery);
338355
}
339356

340357
/**
@@ -350,12 +367,20 @@ private static class TupleBackedMap implements Map<String, Object> {
350367

351368
private final Tuple tuple;
352369

353-
TupleBackedMap(Tuple tuple) {
370+
private final boolean nativeQuery;
371+
372+
TupleBackedMap(Tuple tuple, boolean nativeQuery) {
354373
this.tuple = tuple;
374+
this.nativeQuery = nativeQuery;
355375
}
356376

357377
@Override
358378
public int size() {
379+
380+
if (nativeQuery) {
381+
return keySet().size();
382+
}
383+
359384
return tuple.getElements().size();
360385
}
361386

@@ -378,6 +403,14 @@ public boolean containsKey(Object key) {
378403
tuple.get((String) key);
379404
return true;
380405
} catch (IllegalArgumentException e) {
406+
if (nativeQuery) {
407+
try {
408+
tuple.get(JdbcUtils.convertPropertyNameToUnderscoreName((String) key));
409+
return true;
410+
} catch (IllegalArgumentException ignored) {
411+
return false;
412+
}
413+
}
381414
return false;
382415
}
383416
}
@@ -405,6 +438,13 @@ public Object get(Object key) {
405438
try {
406439
return tuple.get((String) key);
407440
} catch (IllegalArgumentException e) {
441+
if (nativeQuery) {
442+
try {
443+
return tuple.get(JdbcUtils.convertPropertyNameToUnderscoreName((String) key));
444+
} catch (IllegalArgumentException ignored) {
445+
return null;
446+
}
447+
}
408448
return null;
409449
}
410450
}
@@ -432,19 +472,40 @@ public void clear() {
432472
@Override
433473
public Set<String> keySet() {
434474

435-
return tuple.getElements().stream() //
475+
Set<String> keys = tuple.getElements().stream() //
436476
.map(TupleElement::getAlias) //
437477
.collect(Collectors.toSet());
478+
479+
if (nativeQuery) {
480+
Set<String> camelCasedKeys = keys.stream() //
481+
.map(JdbcUtils::convertUnderscoreNameToPropertyName) //
482+
.collect(Collectors.toSet());
483+
keys = new HashSet<>(keys);
484+
keys.addAll(camelCasedKeys);
485+
}
486+
487+
return keys;
438488
}
439489

440490
@Override
441491
public Collection<Object> values() {
492+
493+
if (nativeQuery) {
494+
return keySet().stream().map(this::get).collect(Collectors.toList());
495+
}
496+
442497
return Arrays.asList(tuple.toArray());
443498
}
444499

445500
@Override
446501
public Set<Entry<String, Object>> entrySet() {
447502

503+
if (nativeQuery) {
504+
return keySet().stream() //
505+
.map(e -> new HashMap.SimpleEntry<>(e, get(e))) //
506+
.collect(Collectors.toSet());
507+
}
508+
448509
return tuple.getElements().stream() //
449510
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
450511
.collect(Collectors.toSet());

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java

+11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
* @author Jeff Sheets
3434
* @author JyotirmoyVS
3535
* @author Greg Turnquist
36+
* @author Yanming Zhou
3637
*/
3738
@Entity
3839
@NamedEntityGraphs({ @NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }),
@@ -101,6 +102,8 @@ public class User {
101102

102103
@Column(nullable = false, unique = true) private String emailAddress;
103104

105+
@Column(name = "secondary_email_address") private String secondaryEmailAddress;
106+
104107
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) private Set<User> colleagues;
105108

106109
@ManyToMany private Set<Role> roles;
@@ -173,6 +176,14 @@ public void setEmailAddress(String emailAddress) {
173176
this.emailAddress = emailAddress;
174177
}
175178

179+
public String getSecondaryEmailAddress() {
180+
return secondaryEmailAddress;
181+
}
182+
183+
public void setSecondaryEmailAddress(String secondaryEmailAddress) {
184+
this.secondaryEmailAddress = secondaryEmailAddress;
185+
}
186+
176187
public void setActive(boolean active) {
177188
this.active = active;
178189
}

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

+19
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
* @author Simon Paradies
9696
* @author Geoffrey Deremetz
9797
* @author Krzysztof Krason
98+
* @author Yanming Zhou
9899
*/
99100
@ExtendWith(SpringExtension.class)
100101
@ContextConfiguration("classpath:application-context.xml")
@@ -2970,6 +2971,24 @@ void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() {
29702971
.isNotNull();
29712972
}
29722973

2974+
@Test // DATAJPA-3462
2975+
void supportsProjectionsWithNativeQueriesAndUnderscoresColumnNameToCamelCaseProperty() {
2976+
2977+
User user = new User();
2978+
user.setEmailAddress("primary@something");
2979+
user.setSecondaryEmailAddress("secondary@something");
2980+
em.persist(user);
2981+
2982+
UserRepository.EmailOnly result = repository.findEmailOnlyByNativeQuery(user.getId());
2983+
2984+
String secondaryEmailAddress = result.getSecondaryEmailAddress();
2985+
2986+
assertThat(secondaryEmailAddress) //
2987+
.isEqualTo(user.getSecondaryEmailAddress()) //
2988+
.as("ensuring secondary email is actually not null") //
2989+
.isNotNull();
2990+
}
2991+
29732992
@Test // DATAJPA-1235
29742993
void handlesColonsFollowedByIntegerInStringLiteral() {
29752994

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* @author Simon Paradies
6363
* @author Diego Krupitza
6464
* @author Geoffrey Deremetz
65+
* @author Yanming Zhou
6566
*/
6667
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User>,
6768
UserRepositoryCustom, ListQuerydslPredicateExecutor<User> {
@@ -555,7 +556,7 @@ List<User> findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity
555556
NameOnly findByNativeQuery(Integer id);
556557

557558
// DATAJPA-1248
558-
@Query(value = "SELECT emailaddress FROM SD_User WHERE id = ?1", nativeQuery = true)
559+
@Query(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", nativeQuery = true)
559560
EmailOnly findEmailOnlyByNativeQuery(Integer id);
560561

561562
// DATAJPA-1235
@@ -721,6 +722,8 @@ interface NameOnly {
721722

722723
interface EmailOnly {
723724
String getEmailAddress();
725+
726+
String getSecondaryEmailAddress();
724727
}
725728

726729
interface IdOnly {

0 commit comments

Comments
 (0)