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<User> findByRolesContaining(Role role); This will return all users that have the given role.
This commit is contained in:
@@ -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<CriteriaQuery<Object>,
|
||||
return new PredicateBuilder(part, root).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a path to a {@link Comparable}.
|
||||
*
|
||||
* @param root
|
||||
* @param part
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
private Expression<? extends Comparable> getComparablePath(Root<?> root, Part part) {
|
||||
|
||||
return getTypedPath(root, part);
|
||||
}
|
||||
|
||||
private <T> Expression<T> 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<CriteriaQuery<Object>,
|
||||
public Predicate build() {
|
||||
|
||||
PropertyPath property = part.getProperty();
|
||||
Expression<Object> path = toExpressionRecursively(root, property);
|
||||
Type type = part.getType();
|
||||
|
||||
switch (type) {
|
||||
@@ -207,31 +191,42 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<Object>,
|
||||
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<Collection<Object>> propertyExpression = traversePath(root, property);
|
||||
Expression<Object> 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<String> stringPath = getTypedPath(root, part);
|
||||
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
|
||||
Expression<String> parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
|
||||
@@ -245,10 +240,12 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<Object>,
|
||||
return builder.isFalse(falsePath);
|
||||
case SIMPLE_PROPERTY:
|
||||
ParameterMetadata<Object> expression = provider.next(part);
|
||||
return expression.isIsNullParameter() ? path.isNull() : builder.equal(upperIfIgnoreCase(path),
|
||||
upperIfIgnoreCase(expression.getExpression()));
|
||||
Expression<Object> 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<CriteriaQuery<Object>,
|
||||
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<? extends Comparable> getComparablePath(Root<?> root, Part part) {
|
||||
return getTypedPath(root, part);
|
||||
}
|
||||
|
||||
private <T> Expression<T> getTypedPath(Root<?> root, Part part) {
|
||||
return toExpressionRecursively(root, part.getProperty());
|
||||
}
|
||||
|
||||
private <T> Expression<T> traversePath(Path<?> root, PropertyPath path) {
|
||||
|
||||
Path<Object> result = root.get(path.getSegment());
|
||||
return (Expression<T>) (path.hasNext() ? traversePath(result, path.next()) : result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -95,19 +96,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<Role>();
|
||||
this.roles = new HashSet<Role>(Arrays.asList(roles));
|
||||
this.colleagues = new HashSet<User>();
|
||||
this.attributes = new HashSet<String>();
|
||||
this.createdAt = new Date();
|
||||
|
||||
@@ -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<User> ascending = userRepository.findByLastnameIgnoringCase(
|
||||
new PageRequest(0, 10, new Sort(ASC, "firstname")), "Matthews");
|
||||
Page<User> descending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10,
|
||||
new Sort(DESC, "firstname")), "Matthews");
|
||||
Page<User> ascending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10, new Sort(ASC, "firstname")),
|
||||
"Matthews");
|
||||
Page<User> 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<User> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 extends JpaRepository<User, Integer>, JpaSpecifi
|
||||
* @see DATAJPA-830
|
||||
*/
|
||||
List<User> findByLastnameNotContaining(String part);
|
||||
|
||||
/**
|
||||
* DATAJPA-829
|
||||
*/
|
||||
List<User> findByRolesContaining(Role role);
|
||||
|
||||
/**
|
||||
* DATAJPA-829
|
||||
*/
|
||||
List<User> findByRolesNotContaining(Role role);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user