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:
Oliver Gierke
2015-12-02 07:19:42 +01:00
parent e0a9f287ee
commit f205dc16b2
4 changed files with 108 additions and 53 deletions

View File

@@ -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);
}
}
}

View File

@@ -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<Role>();
this.roles = new HashSet<Role>(Arrays.asList(roles));
this.colleagues = new HashSet<User>();
this.attributes = new HashSet<String>();
this.createdAt = new Date();

View File

@@ -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));
}
}

View File

@@ -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<User> findByLastnameNotContaining(String part);
/**
* DATAJPA-829
*/
List<User> findByRolesContaining(Role role);
/**
* DATAJPA-829
*/
List<User> findByRolesNotContaining(Role role);
}