Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data JPA Parent</name>
Expand Down
4 changes: 2 additions & 2 deletions spring-data-envers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>

<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-jpa-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jpa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>

<name>Spring Data JPA</name>
<description>Spring Data module for JPA repositories.</description>
Expand All @@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4110-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ void executesInKeywordForPageCorrectly() {}
@Override
void shouldProjectWithKeysetScrolling() {}

@Disabled
@Override
void executesQueryWithContainingOnCollectionViaJoin() {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Race> 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<TestUser> 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<TestUser> 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<TestUser> 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<TestUser> 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);
}
Expand Down Expand Up @@ -348,6 +425,13 @@ static class Groups {

}

@jakarta.persistence.Entity
static class Race {

@Id long id;
@OneToMany Set<Groups> lineup;
}

@jakarta.persistence.Entity
static class Group {

Expand All @@ -362,4 +446,20 @@ static class GroupId {

}

@jakarta.persistence.Entity
static class TestUser {

@Id long id;
@ManyToOne TestUser manager;
@OneToMany Set<TestUser> colleagues = new HashSet<>();
@OneToMany Set<TestRole> roles = new HashSet<>();
}

@jakarta.persistence.Entity
static class TestRole {

@Id long id;
String name;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,18 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S

List<User> findByFirstnameNotIn(Collection<String> firstnames);

List<User> findByColleaguesRolesIsEmpty();

List<User> findByColleaguesRolesIsNotEmpty();

List<User> findByManagerColleaguesRolesIsNotEmpty();

List<User> findByColleaguesRolesContaining(Role role);

List<User> findByColleaguesRolesNotContaining(Role role);

List<User> findByColleaguesRolesIsEmptyAndAttributesIsNotEmpty();

// DATAJPA-292
@Query("select u from User u where u.firstname like ?1%")
List<User> findByFirstnameLike(String firstname);
Expand Down
Loading