diff --git a/pom.xml b/pom.xml
index 04c71fe23e..4d64df6e37 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
pom
Spring Data JPA Parent
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index c54fa73c20..095e8bd9c1 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -5,12 +5,12 @@
org.springframework.data
spring-data-envers
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
org.springframework.data
spring-data-jpa-parent
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml
index 954a3a249b..ceacf5342e 100644
--- a/spring-data-jpa-distribution/pom.xml
+++ b/spring-data-jpa-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
../pom.xml
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index d26ca6b94b..03561d26d7 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-jpa
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
@@ -16,7 +16,7 @@
org.springframework.data
spring-data-jpa-parent
- 4.1.0-SNAPSHOT
+ 4.1.x-GH-4110-SNAPSHOT
../pom.xml
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 9361a50a9f..1be903171a 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
@@ -481,7 +481,10 @@ public JpqlQueryBuilder.Predicate build() {
case NOT_CONTAINING:
if (property.getLeafProperty().isCollection()) {
- where = JpqlQueryBuilder.where(entity, property);
+
+ if (!property.hasNext()) {
+ where = JpqlQueryBuilder.where(entity, property);
+ }
return type.equals(NOT_CONTAINING) ? where.notMemberOf(placeholder(provider.next(part)))
: where.memberOf(placeholder(provider.next(part)));
@@ -522,7 +525,10 @@ public JpqlQueryBuilder.Predicate build() {
throw new IllegalArgumentException("IsEmpty / IsNotEmpty can only be used on collection properties");
}
- where = JpqlQueryBuilder.where(entity, property);
+ if (!property.hasNext()) {
+ where = JpqlQueryBuilder.where(entity, property);
+ }
+
return type.equals(IS_NOT_EMPTY) ? where.isNotEmpty() : where.isEmpty();
case WITHIN:
case NEAR:
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java
index e1aa748554..4355ee69e0 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlUtils.java
@@ -17,6 +17,7 @@
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Bindable;
+import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.ManagedType;
import jakarta.persistence.metamodel.Metamodel;
@@ -85,22 +86,41 @@ public JpqlQueryBuilder.PathExpression toExpressionRecursively(Metamodel metamod
// if it's a leaf, return the join
if (isLeafProperty) {
+
+ // except its a collection type on the root
+ if (from instanceof EntityType> && property.isCollection()) {
+ Attribute, ?> nextAttribute = resolveAttribute(metamodel, from, property);
+ if(nextAttribute != null && nextAttribute.isAssociation()) {
+ return new JpqlQueryBuilder.PathAndOrigin(property, source, false);
+ }
+ }
return new JpqlQueryBuilder.PathAndOrigin(property, joinSource, true);
}
PropertyPath nextProperty = Objects.requireNonNull(property.next(), "An element of the property path is null");
- ManagedType> managedTypeForModel = getManagedTypeForModel(from);
- Attribute, ?> nextAttribute = getModelForPath(metamodel, property, managedTypeForModel, from);
+ Attribute, ?> nextAttribute = resolveAttribute(metamodel, from, property);
if (nextAttribute == null) {
throw new IllegalStateException("Binding property is null");
}
+ // this is a reference to a collection property (eg. for an is empty check)
+ if (nextAttribute.isCollection() && !nextProperty.hasNext()) {
+ return new JpqlQueryBuilder.PathAndOrigin(nextProperty, joinSource, false);
+ }
+
return toExpressionRecursively(metamodel, joinSource, (Bindable>) nextAttribute, nextProperty, isForSelection,
requiresOuterJoin);
}
+ private static @Nullable Attribute, ?> resolveAttribute(Metamodel metamodel, Bindable> from,
+ PropertyPath property) {
+
+ ManagedType> managedType = getManagedTypeForModel(from);
+ return getModelForPath(metamodel, property, managedType, from);
+ }
+
private static @Nullable Attribute, ?> getModelForPath(@Nullable Metamodel metamodel, PropertyPath path,
@Nullable ManagedType> managedType, @Nullable Bindable> fallback) {
@@ -137,8 +157,7 @@ record BindablePathResolver(Metamodel metamodel,
}
private @Nullable Attribute, ?> resolveAttribute(PropertyPath propertyPath) {
- ManagedType> managedType = getManagedTypeForModel(bindable);
- return getModelForPath(metamodel, propertyPath, managedType, bindable);
+ return JpqlExpressionFactory.resolveAttribute(metamodel, bindable, propertyPath);
}
@Override
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java
index 9ad2fe8f3c..5d6367a71c 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkUserRepositoryFinderTests.java
@@ -40,4 +40,8 @@ void executesInKeywordForPageCorrectly() {}
@Override
void shouldProjectWithKeysetScrolling() {}
+ @Disabled
+ @Override
+ void executesQueryWithContainingOnCollectionViaJoin() {}
+
}
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 2c73a64803..7074d9d7c2 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
@@ -167,6 +167,62 @@ void executesNotInQueryCorrectly() {
assertThat(result).containsExactly(oliver);
}
+ @Test // GH-4110
+ void executesQueryWithEmptyOnCollection() {
+
+ dave.addColleague(oliver);
+ userRepository.save(dave);
+ userRepository.save(oliver);
+
+ assertThat(userRepository.findByColleaguesRolesIsEmpty()).containsExactly(dave, carter);
+ assertThat(userRepository.findByColleaguesRolesIsNotEmpty()).containsExactlyInAnyOrder(oliver);
+ }
+
+ @Test // GH-4110
+ void executesQueryWithEmptyOnCollectionViaMultipleJoins() {
+
+ carter.setManager(dave);
+ dave.addColleague(oliver);
+ dave.addColleague(carter);
+ userRepository.save(dave);
+ userRepository.save(carter);
+ userRepository.save(oliver);
+
+ userRepository.save(oliver);
+
+ assertThat(userRepository.findByManagerColleaguesRolesIsNotEmpty()).containsExactly(carter);
+ }
+
+ @Test // GH-4110
+ void executesQueryWithContainingOnCollectionViaJoin() {
+
+ dave.addColleague(oliver);
+ oliver.addRole(singer);
+ userRepository.save(dave);
+ userRepository.save(oliver);
+
+ assertThat(userRepository.findByColleaguesRolesContaining(singer)).containsExactlyInAnyOrder(dave, oliver);
+ assertThat(userRepository.findByColleaguesRolesNotContaining(drummer)).containsExactlyInAnyOrder(dave, carter, oliver);
+ }
+
+ @Test // GH-4110
+ void executesQueryWithMultipleCollectionPredicates() {
+
+ dave.addColleague(oliver);
+ dave.getAttributes().add("test");
+ userRepository.save(dave);
+ userRepository.save(oliver);
+
+ assertThat(userRepository.findByColleaguesRolesIsEmptyAndAttributesIsNotEmpty())
+ .containsExactlyInAnyOrder(dave);
+ }
+
+ @Test // GH-4110
+ void executesQueryWithEmptyOnCollectionWithNoColleagues() {
+
+ assertThat(userRepository.findByColleaguesRolesIsEmpty()).containsExactly(dave, carter, oliver);
+ }
+
@Test // DATAJPA-92
void findsByLastnameIgnoringCase() {
diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java
index 70add8e717..81bb79c2a7 100644
--- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java
+++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java
@@ -248,6 +248,83 @@ void shouldRenderJoinsWithSamePathSegmentCorrectly() {
}
+ @Test // GH-4110
+ void referencesCollectionViaJoin() {
+
+ TestMetaModel model = TestMetaModel.hibernateModel(Race.class, Groups.class, Group.class, GroupId.class,
+ Person.class);
+ Entity entity = entity(Race.class);
+
+ EntityType entityType = model.entity(Race.class);
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
+ PropertyPath.from("lineup.groups", Race.class));
+ String jpql = JpqlQueryBuilder.selectFrom(entity).entity().where(JpqlQueryBuilder.where(pas).isNotEmpty()).render();
+
+ assertThat(jpql).isEqualTo(
+ "SELECT r FROM JpqlQueryBuilderUnitTests$Race r LEFT JOIN r.lineup l WHERE l.groups IS NOT EMPTY");
+ }
+
+ @Test // GH-4110
+ void referencesCollectionViaMultipleJoins() {
+
+ TestMetaModel model = TestMetaModel.hibernateModel(TestUser.class, TestRole.class);
+ Entity entity = entity(TestUser.class);
+
+ EntityType entityType = model.entity(TestUser.class);
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
+ PropertyPath.from("manager.colleagues.roles", TestUser.class));
+ String jpql = JpqlQueryBuilder.selectFrom(entity).entity().where(JpqlQueryBuilder.where(pas).isEmpty()).render();
+
+ assertThat(jpql).isEqualTo(
+ "SELECT t FROM JpqlQueryBuilderUnitTests$TestUser t LEFT JOIN t.manager m LEFT JOIN m.colleagues c WHERE c.roles IS EMPTY");
+ }
+
+ @Test // GH-4110
+ void referencesCollectionViaJoinWithMemberOf() {
+
+ TestMetaModel model = TestMetaModel.hibernateModel(TestUser.class, TestRole.class);
+ Entity entity = entity(TestUser.class);
+
+ EntityType entityType = model.entity(TestUser.class);
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
+ PropertyPath.from("colleagues.roles", TestUser.class));
+ String jpql = JpqlQueryBuilder.selectFrom(entity).entity()
+ .where(JpqlQueryBuilder.where(pas).memberOf(JpqlQueryBuilder.parameter("?1"))).render();
+
+ assertThat(jpql).isEqualTo(
+ "SELECT t FROM JpqlQueryBuilderUnitTests$TestUser t LEFT JOIN t.colleagues c WHERE ?1 MEMBER OF c.roles");
+ }
+
+ @Test // GH-4110
+ void directCollectionPropertyDoesNotCreateJoin() {
+
+ TestMetaModel model = TestMetaModel.hibernateModel(TestUser.class, TestRole.class);
+ Entity entity = entity(TestUser.class);
+
+ EntityType entityType = model.entity(TestUser.class);
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
+ PropertyPath.from("roles", TestUser.class));
+ String jpql = JpqlQueryBuilder.selectFrom(entity).entity().where(JpqlQueryBuilder.where(pas).isEmpty()).render();
+
+ assertThat(jpql).isEqualTo("SELECT t FROM JpqlQueryBuilderUnitTests$TestUser t WHERE t.roles IS EMPTY");
+ }
+
+ @Test // GH-4110
+ void collectionWithAdditionalPathSegments() {
+
+ TestMetaModel model = TestMetaModel.hibernateModel(TestUser.class, TestRole.class);
+ Entity entity = entity(TestUser.class);
+
+ EntityType entityType = model.entity(TestUser.class);
+ JpqlQueryBuilder.PathExpression pas = JpqlUtils.toExpressionRecursively(model, entity, entityType,
+ PropertyPath.from("colleagues.roles.name", TestUser.class));
+ String jpql = JpqlQueryBuilder.selectFrom(entity).entity()
+ .where(JpqlQueryBuilder.where(pas).eq(literal("ADMIN"))).render();
+
+ assertThat(jpql).isEqualTo(
+ "SELECT t FROM JpqlQueryBuilderUnitTests$TestUser t LEFT JOIN t.colleagues c LEFT JOIN c.roles r WHERE r.name = 'ADMIN'");
+ }
+
static ContextualAssert contextual(RenderContext context) {
return new ContextualAssert(context);
}
@@ -348,6 +425,13 @@ static class Groups {
}
+ @jakarta.persistence.Entity
+ static class Race {
+
+ @Id long id;
+ @OneToMany Set lineup;
+ }
+
@jakarta.persistence.Entity
static class Group {
@@ -362,4 +446,20 @@ static class GroupId {
}
+ @jakarta.persistence.Entity
+ static class TestUser {
+
+ @Id long id;
+ @ManyToOne TestUser manager;
+ @OneToMany Set colleagues = new HashSet<>();
+ @OneToMany Set roles = new HashSet<>();
+ }
+
+ @jakarta.persistence.Entity
+ static class TestRole {
+
+ @Id long id;
+ String name;
+ }
+
}
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 004488e471..1cdc072fb2 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
@@ -146,6 +146,18 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
List findByFirstnameNotIn(Collection firstnames);
+ List findByColleaguesRolesIsEmpty();
+
+ List findByColleaguesRolesIsNotEmpty();
+
+ List findByManagerColleaguesRolesIsNotEmpty();
+
+ List findByColleaguesRolesContaining(Role role);
+
+ List findByColleaguesRolesNotContaining(Role role);
+
+ List findByColleaguesRolesIsEmptyAndAttributesIsNotEmpty();
+
// DATAJPA-292
@Query("select u from User u where u.firstname like ?1%")
List findByFirstnameLike(String firstname);