diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index 28ec8cf31..16ef4cff0 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -307,15 +307,18 @@ public class JpaQueryCreator extends AbstractQueryCreator falsePath = getTypedPath(root, part); return builder.isFalse(falsePath); case SIMPLE_PROPERTY: + case NEGATING_SIMPLE_PROPERTY: + ParameterMetadata expression = provider.next(part); Expression path = getTypedPath(root, part); - return expression.isIsNullParameter() ? path.isNull() - : builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())); - case NEGATING_SIMPLE_PROPERTY: - ParameterMetadata negatedExpression = provider.next(part); - Expression negatedPath = getTypedPath(root, part); - return negatedExpression.isIsNullParameter() ? negatedPath.isNotNull() - : builder.notEqual(upperIfIgnoreCase(negatedPath), upperIfIgnoreCase(negatedExpression.getExpression())); + + if (expression.isIsNullParameter()) { + return type.equals(SIMPLE_PROPERTY) ? path.isNull() : path.isNotNull(); + } else { + return type.equals(SIMPLE_PROPERTY) + ? builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())) + : builder.notEqual(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())); + } case IS_EMPTY: case IS_NOT_EMPTY: diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java index 213e641a5..3c858dd81 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java @@ -208,7 +208,10 @@ class ParameterMetadataProvider { EscapeCharacter escape) { this.expression = expression; - this.type = value == null && Type.SIMPLE_PROPERTY.equals(part.getType()) ? Type.IS_NULL : part.getType(); + this.type = value == null + && (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType())) + ? Type.IS_NULL + : part.getType(); this.ignoreCase = IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase()); this.noWildcards = part.getProperty().getLeafProperty().isCollection(); this.escape = escape; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 259420a09..fdf3330ca 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -58,6 +58,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Krzysztof Krason * @author Greg Turnquist * @author Mark Paluch + * @author Christoph Strobl * @see QueryLookupStrategy */ @ExtendWith(SpringExtension.class) @@ -386,4 +387,22 @@ class UserRepositoryFinderTests { assertThat(dtos).flatExtracting(UserRepository.NameOnly::getLastname) // .containsExactly("Matthews", "Beauford", "Matthews"); } + + @Test // GH-3675 + void findBySimplePropertyUsingMixedNullNonNullArgument() { + + List result = userRepository.findUserByLastname(null); + assertThat(result).isEmpty(); + result = userRepository.findUserByLastname(carter.getLastname()); + assertThat(result).containsExactly(carter); + } + + @Test // GH-3675 + void findByNegatingSimplePropertyUsingMixedNullNonNullArgument() { + + List result = userRepository.findByLastnameNot(null); + assertThat(result).isNotEmpty(); + result = userRepository.findUserByLastname(carter.getLastname()); + assertThat(result).containsExactly(carter); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index ddd71dbfa..4b34c8b70 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -37,6 +37,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -59,6 +61,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; * @author Michael Cramer * @author Jens Schauder * @author Krzysztof Krason + * @author Christoph Strobl */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -100,20 +103,21 @@ class PartTreeJpaQueryIntegrationTests { testIgnoreCase("findByIdAllIgnoringCase", 3); } - @Test // DATAJPA-121 - @Disabled // HHH-15432 - void recreatesQueryIfNullValueIsGiven() throws Exception { + @ParameterizedTest // DATAJPA-121, GH-3675 + @ValueSource(strings = { "Firstname", "FirstnameNot" }) + void recreatesQueryIfNullValueIsGiven(String criteria) throws Exception { - JpaQueryMethod queryMethod = getQueryMethod("findByFirstname", String.class, Pageable.class); + JpaQueryMethod queryMethod = getQueryMethod("findBy%s".formatted(criteria), String.class, Pageable.class); PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager); Query query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { "Matthews", PageRequest.of(0, 1) })); - - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("firstname=:param0"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))) + .contains("firstname %s :".formatted(criteria.endsWith("Not") ? "<>" : "=")); query = jpaQuery.createQuery(getAccessor(queryMethod, new Object[] { null, PageRequest.of(0, 1) })); - assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))).endsWith("firstname is null"); + assertThat(HibernateUtils.getHibernateQuery(query.unwrap(HIBERNATE_NATIVE_QUERY))) + .endsWithIgnoringCase("firstname %s NULL".formatted(criteria.endsWith("Not") ? "IS NOT" : "IS")); } @Test // DATAJPA-920 @@ -277,6 +281,8 @@ class PartTreeJpaQueryIntegrationTests { Page findByFirstname(String firstname, Pageable pageable); + Page findByFirstnameNot(String firstname, Pageable pageable); + User findByIdIgnoringCase(Integer id); User findByIdAllIgnoringCase(Integer id); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 01526cd0b..e8eb6ae75 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -48,6 +48,7 @@ import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.querydsl.ListQuerydslPredicateExecutor; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.springframework.lang.Nullable; import org.springframework.transaction.annotation.Transactional; /** @@ -75,6 +76,8 @@ public interface UserRepository extends JpaRepository, JpaSpecifi @QueryHints({ @QueryHint(name = "foo", value = "bar") }) List findByLastname(String lastname); + List findUserByLastname(@Nullable String lastname); + /** * Redeclaration of {@link CrudRepository#findById(java.lang.Object)} to change transaction configuration. */ @@ -177,7 +180,7 @@ public interface UserRepository extends JpaRepository, JpaSpecifi List findByLastnameNotLike(String lastname); - List findByLastnameNot(String lastname); + List findByLastnameNot(@Nullable String lastname); List findByManagerLastname(String name); diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index 8657d42ae..f69bb7929 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -60,7 +60,7 @@ The following table describes the keywords supported for JPA and what a method c |`EndingWith`|`findByFirstnameEndingWith`|`… where x.firstname like ?1` (parameter bound with prepended `%`) |`Containing`|`findByFirstnameContaining`|`… where x.firstname like ?1` (parameter bound wrapped in `%`) |`OrderBy`|`findByAgeOrderByLastnameDesc`|`… where x.age = ?1 order by x.lastname desc` -|`Not`|`findByLastnameNot`|`… where x.lastname <> ?1` +|`Not`|`findByLastnameNot`|`… where x.lastname <> ?1` (or `… where x.lastname IS NOT NULL` if the argument is `null`) |`In`|`findByAgeIn(Collection ages)`|`… where x.age in ?1` |`NotIn`|`findByAgeNotIn(Collection ages)`|`… where x.age not in ?1` |`True`|`findByActiveTrue()`|`… where x.active = true`