From f205dc16b2a6fbdd0fe3b1c97f132f3ec1ae1064 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 2 Dec 2015 07:19:42 +0100 Subject: [PATCH] DATAJPA-829 - Support for Contains keyword on collection expressions. The if a collection expression is concluded with a Contains keyword, we now translate that into a "member of"-expression on the criteria query. This allows to check whether a collection property contains a singular value. List findByRolesContaining(Role role); This will return all users that have the given role. --- .../jpa/repository/query/JpaQueryCreator.java | 84 +++++++++++-------- .../data/jpa/domain/sample/User.java | 8 +- .../repository/UserRepositoryFinderTests.java | 58 +++++++++---- .../jpa/repository/sample/UserRepository.java | 11 +++ 4 files changed, 108 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java index b51128e49..877798a54 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2012 the original author or authors. + * Copyright 2008-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.List; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; @@ -147,27 +148,11 @@ public class JpaQueryCreator extends AbstractQueryCreator, return new PredicateBuilder(part, root).build(); } - /** - * Returns a path to a {@link Comparable}. - * - * @param root - * @param part - * @return - */ - @SuppressWarnings({ "rawtypes" }) - private Expression getComparablePath(Root root, Part part) { - - return getTypedPath(root, part); - } - - private Expression getTypedPath(Root root, Part part) { - return toExpressionRecursively(root, part.getProperty()); - } - /** * Simple builder to contain logic to create JPA {@link Predicate}s from {@link Part}s. * * @author Phil Webb + * @author Oliver Gierke */ @SuppressWarnings({ "unchecked", "rawtypes" }) private class PredicateBuilder { @@ -197,7 +182,6 @@ public class JpaQueryCreator extends AbstractQueryCreator, public Predicate build() { PropertyPath property = part.getProperty(); - Expression path = toExpressionRecursively(root, property); Type type = part.getType(); switch (type) { @@ -207,31 +191,42 @@ public class JpaQueryCreator extends AbstractQueryCreator, return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression()); case AFTER: case GREATER_THAN: - return builder.greaterThan(getComparablePath(root, part), provider.next(part, Comparable.class) - .getExpression()); + return builder.greaterThan(getComparablePath(root, part), + provider.next(part, Comparable.class).getExpression()); case GREATER_THAN_EQUAL: - return builder.greaterThanOrEqualTo(getComparablePath(root, part), provider.next(part, Comparable.class) - .getExpression()); + return builder.greaterThanOrEqualTo(getComparablePath(root, part), + provider.next(part, Comparable.class).getExpression()); case BEFORE: case LESS_THAN: return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression()); case LESS_THAN_EQUAL: - return builder.lessThanOrEqualTo(getComparablePath(root, part), provider.next(part, Comparable.class) - .getExpression()); + return builder.lessThanOrEqualTo(getComparablePath(root, part), + provider.next(part, Comparable.class).getExpression()); case IS_NULL: - return path.isNull(); + return getTypedPath(root, part).isNull(); case IS_NOT_NULL: - return path.isNotNull(); + return getTypedPath(root, part).isNotNull(); case NOT_IN: - return path.in(provider.next(part, Collection.class).getExpression()).not(); + return getTypedPath(root, part).in(provider.next(part, Collection.class).getExpression()).not(); case IN: - return path.in(provider.next(part, Collection.class).getExpression()); + return getTypedPath(root, part).in(provider.next(part, Collection.class).getExpression()); case STARTING_WITH: case ENDING_WITH: case CONTAINING: + case NOT_CONTAINING: + + if (property.isCollection()) { + + Expression> propertyExpression = traversePath(root, property); + Expression parameterExpression = provider.next(part).getExpression(); + + // Can't just call .not() in case of negation as EclipseLink chokes on that. + return type.equals(NOT_CONTAINING) ? builder.isNotMember(parameterExpression, propertyExpression) + : builder.isMember(parameterExpression, propertyExpression); + } + case LIKE: case NOT_LIKE: - case NOT_CONTAINING: Expression stringPath = getTypedPath(root, part); Expression propertyExpression = upperIfIgnoreCase(stringPath); Expression parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression()); @@ -245,10 +240,12 @@ public class JpaQueryCreator extends AbstractQueryCreator, return builder.isFalse(falsePath); case SIMPLE_PROPERTY: ParameterMetadata expression = provider.next(part); - return expression.isIsNullParameter() ? path.isNull() : builder.equal(upperIfIgnoreCase(path), - upperIfIgnoreCase(expression.getExpression())); + Expression path = getTypedPath(root, part); + return expression.isIsNullParameter() ? path.isNull() + : builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression())); case NEGATING_SIMPLE_PROPERTY: - return builder.notEqual(upperIfIgnoreCase(path), upperIfIgnoreCase(provider.next(part).getExpression())); + return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)), + upperIfIgnoreCase(provider.next(part).getExpression())); default: throw new IllegalArgumentException("Unsupported keyword " + type); } @@ -287,5 +284,26 @@ public class JpaQueryCreator extends AbstractQueryCreator, private boolean canUpperCase(Expression expression) { return String.class.equals(expression.getJavaType()); } + + /** + * Returns a path to a {@link Comparable}. + * + * @param root + * @param part + * @return + */ + private Expression getComparablePath(Root root, Part part) { + return getTypedPath(root, part); + } + + private Expression getTypedPath(Root root, Part part) { + return toExpressionRecursively(root, part.getProperty()); + } + + private Expression traversePath(Path root, PropertyPath path) { + + Path result = root.get(path.getSegment()); + return (Expression) (path.hasNext() ? traversePath(result, path.next()) : result); + } } } diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/src/test/java/org/springframework/data/jpa/domain/sample/User.java index 78fb72ab2..d1d88f913 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jpa.domain.sample; +import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Set; @@ -99,19 +100,20 @@ public class User { } /** - * Creates a new instance of {@code User} with preinitialized values for firstname, lastname and email address. + * Creates a new instance of {@code User} with preinitialized values for firstname, lastname, email address and roles. * * @param firstname * @param lastname * @param emailAddress + * @param roles */ - public User(String firstname, String lastname, String emailAddress) { + public User(String firstname, String lastname, String emailAddress, Role... roles) { this.firstname = firstname; this.lastname = lastname; this.emailAddress = emailAddress; this.active = true; - this.roles = new HashSet(); + this.roles = new HashSet(Arrays.asList(roles)); this.colleagues = new HashSet(); this.attributes = new HashSet(); this.createdAt = new Date(); diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 0062cf902..9422e465e 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -30,7 +30,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.sample.RoleRepository; import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.test.context.ContextConfiguration; @@ -49,22 +51,21 @@ import org.springframework.transaction.annotation.Transactional; public class UserRepositoryFinderTests { @Autowired UserRepository userRepository; + @Autowired RoleRepository roleRepository; User dave, carter, oliver; + Role drummer, guitarist, singer; @Before public void setUp() { - // This one matches both criterias - dave = new User("Dave", "Matthews", "dave@dmband.com"); - userRepository.save(dave); + drummer = roleRepository.save(new Role("DRUMMER")); + guitarist = roleRepository.save(new Role("GUITARIST")); + singer = roleRepository.save(new Role("SINGER")); - // This one matches only the second one - carter = new User("Carter", "Beauford", "carter@dmband.com"); - userRepository.save(carter); - - oliver = new User("Oliver August", "Matthews", "oliver@dmband.com"); - userRepository.save(oliver); + dave = userRepository.save(new User("Dave", "Matthews", "dave@dmband.com", singer)); + carter = userRepository.save(new User("Carter", "Beauford", "carter@dmband.com", singer, drummer)); + oliver = userRepository.save(new User("Oliver August", "Matthews", "oliver@dmband.com")); } /** @@ -171,16 +172,18 @@ public class UserRepositoryFinderTests { */ @Test public void respectsPageableOrderOnQueryGenerateFromMethodName() throws Exception { - Page ascending = userRepository.findByLastnameIgnoringCase( - new PageRequest(0, 10, new Sort(ASC, "firstname")), "Matthews"); - Page descending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10, - new Sort(DESC, "firstname")), "Matthews"); + Page ascending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10, new Sort(ASC, "firstname")), + "Matthews"); + Page descending = userRepository + .findByLastnameIgnoringCase(new PageRequest(0, 10, new Sort(DESC, "firstname")), "Matthews"); assertThat(ascending.getTotalElements(), is(2L)); assertThat(descending.getTotalElements(), is(2L)); - assertThat(ascending.getContent().get(0).getFirstname(), is(not(equalTo(descending.getContent().get(0) - .getFirstname())))); - assertThat(ascending.getContent().get(0).getFirstname(), is(equalTo(descending.getContent().get(1).getFirstname()))); - assertThat(ascending.getContent().get(1).getFirstname(), is(equalTo(descending.getContent().get(0).getFirstname()))); + assertThat(ascending.getContent().get(0).getFirstname(), + is(not(equalTo(descending.getContent().get(0).getFirstname())))); + assertThat(ascending.getContent().get(0).getFirstname(), + is(equalTo(descending.getContent().get(1).getFirstname()))); + assertThat(ascending.getContent().get(1).getFirstname(), + is(equalTo(descending.getContent().get(0).getFirstname()))); } /** @@ -202,4 +205,25 @@ public class UserRepositoryFinderTests { public void executesMethodWithNotContainingOnStringCorrectly() { assertThat(userRepository.findByLastnameNotContaining("u"), containsInAnyOrder(dave, oliver)); } + + /** + * @see DATAJPA-829 + */ + @Test + public void translatesContainsToMemberOf() { + + List singers = userRepository.findByRolesContaining(singer); + + assertThat(singers, hasSize(2)); + assertThat(singers, hasItems(dave, carter)); + assertThat(userRepository.findByRolesContaining(drummer), contains(carter)); + } + + /** + * @see DATAJPA-829 + */ + @Test + public void translatesNotContainsToNotMemberOf() { + assertThat(userRepository.findByRolesNotContaining(drummer), hasItems(dave, oliver)); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index e800cdcf8..a3913dc01 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -28,6 +28,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.sample.Role; import org.springframework.data.jpa.domain.sample.SpecialUser; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.JpaRepository; @@ -580,4 +581,14 @@ public interface UserRepository * @see DATAJPA-830 */ List findByLastnameNotContaining(String part); + + /** + * DATAJPA-829 + */ + List findByRolesContaining(Role role); + + /** + * DATAJPA-829 + */ + List findByRolesNotContaining(Role role); }