Add camelCase to snake_case fallback for native query projections.

Closes #3462
Original pull request: #3472
This commit is contained in:
Yanming Zhou
2024-05-13 13:57:46 +08:00
committed by Mark Paluch
parent 966b43c008
commit 3a2e433af0
4 changed files with 124 additions and 3 deletions

View File

@@ -47,6 +47,7 @@ import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.Lazy;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -62,6 +63,7 @@ import org.springframework.util.Assert;
* @author Сергей Цыпанов
* @author Wonchul Heo
* @author Julia Lee
* @author Yanming Zhou
*/
public abstract class AbstractJpaQuery implements RepositoryQuery {
@@ -149,7 +151,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
Object result = execution.execute(this, accessor);
ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor);
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType()));
return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType(), method.isNativeQuery()));
}
private JpaParametersParameterAccessor obtainParameterAccessor(Object[] values) {
@@ -304,6 +306,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
private final ReturnedType type;
private final boolean nativeQuery;
/**
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
*
@@ -311,9 +315,21 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
*/
public TupleConverter(ReturnedType type) {
this(type, false);
}
/**
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
*
* @param type must not be {@literal null}.
* @param nativeQuery is this converter for native query?
*/
public TupleConverter(ReturnedType type, boolean nativeQuery) {
Assert.notNull(type, "Returned type must not be null");
this.type = type;
this.nativeQuery = nativeQuery;
}
@Override
@@ -334,7 +350,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
}
}
return new TupleBackedMap(tuple);
return new TupleBackedMap(nativeQuery ? new NativeTupleWrapper(tuple) : tuple);
}
/**
@@ -451,4 +467,76 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
}
}
}
private static class NativeTupleWrapper implements Tuple {
private final Tuple underlying;
NativeTupleWrapper(Tuple underlying) {
this.underlying = underlying;
}
String fallback(String alias) {
return JdbcUtils.convertPropertyNameToUnderscoreName(alias);
}
@Override
public <X> X get(TupleElement<X> tupleElement) {
try {
return underlying.get(tupleElement);
} catch (IllegalArgumentException original) {
try {
return underlying.get(fallback(tupleElement.getAlias()), tupleElement.getJavaType());
} catch (IllegalArgumentException ignored) {
throw original;
}
}
}
@Override
public <X> X get(String s, Class<X> aClass) {
try {
return underlying.get(s, aClass);
} catch (IllegalArgumentException original) {
try {
return underlying.get(fallback(s), aClass);
} catch (IllegalArgumentException ignored) {
throw original;
}
}
}
@Override
public Object get(String s) {
try {
return underlying.get(s);
} catch (IllegalArgumentException original) {
try {
return underlying.get(fallback(s));
} catch (IllegalArgumentException ignored) {
throw original;
}
}
}
@Override
public <X> X get(int i, Class<X> aClass) {
return underlying.get(i, aClass);
}
@Override
public Object get(int i) {
return underlying.get(i);
}
@Override
public Object[] toArray() {
return underlying.toArray();
}
@Override
public List<TupleElement<?>> getElements() {
return underlying.getElements();
}
}
}

View File

@@ -33,6 +33,7 @@ import java.util.Set;
* @author Jeff Sheets
* @author JyotirmoyVS
* @author Greg Turnquist
* @author Yanming Zhou
*/
@Entity
@NamedEntityGraphs({ @NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }),
@@ -101,6 +102,8 @@ public class User {
@Column(nullable = false, unique = true) private String emailAddress;
@Column(name = "secondary_email_address") private String secondaryEmailAddress;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }) private Set<User> colleagues;
@ManyToMany private Set<Role> roles;
@@ -173,6 +176,14 @@ public class User {
this.emailAddress = emailAddress;
}
public String getSecondaryEmailAddress() {
return secondaryEmailAddress;
}
public void setSecondaryEmailAddress(String secondaryEmailAddress) {
this.secondaryEmailAddress = secondaryEmailAddress;
}
public void setActive(boolean active) {
this.active = active;
}

View File

@@ -95,6 +95,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Simon Paradies
* @author Geoffrey Deremetz
* @author Krzysztof Krason
* @author Yanming Zhou
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:application-context.xml")
@@ -2970,6 +2971,24 @@ class UserRepositoryTests {
.isNotNull();
}
@Test // DATAJPA-3462
void supportsProjectionsWithNativeQueriesAndUnderscoresColumnNameToCamelCaseProperty() {
User user = new User();
user.setEmailAddress("primary@something");
user.setSecondaryEmailAddress("secondary@something");
em.persist(user);
UserRepository.EmailOnly result = repository.findEmailOnlyByNativeQuery(user.getId());
String secondaryEmailAddress = result.getSecondaryEmailAddress();
assertThat(secondaryEmailAddress) //
.isEqualTo(user.getSecondaryEmailAddress()) //
.as("ensuring secondary email is actually not null") //
.isNotNull();
}
@Test // DATAJPA-1235
void handlesColonsFollowedByIntegerInStringLiteral() {

View File

@@ -62,6 +62,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Simon Paradies
* @author Diego Krupitza
* @author Geoffrey Deremetz
* @author Yanming Zhou
*/
public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User>,
UserRepositoryCustom, ListQuerydslPredicateExecutor<User> {
@@ -555,7 +556,7 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
NameOnly findByNativeQuery(Integer id);
// DATAJPA-1248
@Query(value = "SELECT emailaddress FROM SD_User WHERE id = ?1", nativeQuery = true)
@Query(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", nativeQuery = true)
EmailOnly findEmailOnlyByNativeQuery(Integer id);
// DATAJPA-1235
@@ -721,6 +722,8 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
interface EmailOnly {
String getEmailAddress();
String getSecondaryEmailAddress();
}
interface IdOnly {