Skip to content

Commit ae7a3c9

Browse files
committed
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.
1 parent e0a9f28 commit ae7a3c9

File tree

4 files changed

+107
-53
lines changed

4 files changed

+107
-53
lines changed

src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java

+50-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2008-2012 the original author or authors.
2+
* Copyright 2008-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import javax.persistence.criteria.CriteriaBuilder;
2626
import javax.persistence.criteria.CriteriaQuery;
2727
import javax.persistence.criteria.Expression;
28+
import javax.persistence.criteria.Path;
2829
import javax.persistence.criteria.Predicate;
2930
import javax.persistence.criteria.Root;
3031

@@ -147,27 +148,11 @@ private Predicate toPredicate(Part part, Root<?> root) {
147148
return new PredicateBuilder(part, root).build();
148149
}
149150

150-
/**
151-
* Returns a path to a {@link Comparable}.
152-
*
153-
* @param root
154-
* @param part
155-
* @return
156-
*/
157-
@SuppressWarnings({ "rawtypes" })
158-
private Expression<? extends Comparable> getComparablePath(Root<?> root, Part part) {
159-
160-
return getTypedPath(root, part);
161-
}
162-
163-
private <T> Expression<T> getTypedPath(Root<?> root, Part part) {
164-
return toExpressionRecursively(root, part.getProperty());
165-
}
166-
167151
/**
168152
* Simple builder to contain logic to create JPA {@link Predicate}s from {@link Part}s.
169153
*
170154
* @author Phil Webb
155+
* @author Oliver Gierke
171156
*/
172157
@SuppressWarnings({ "unchecked", "rawtypes" })
173158
private class PredicateBuilder {
@@ -197,7 +182,6 @@ public PredicateBuilder(Part part, Root<?> root) {
197182
public Predicate build() {
198183

199184
PropertyPath property = part.getProperty();
200-
Expression<Object> path = toExpressionRecursively(root, property);
201185
Type type = part.getType();
202186

203187
switch (type) {
@@ -207,31 +191,41 @@ public Predicate build() {
207191
return builder.between(getComparablePath(root, part), first.getExpression(), second.getExpression());
208192
case AFTER:
209193
case GREATER_THAN:
210-
return builder.greaterThan(getComparablePath(root, part), provider.next(part, Comparable.class)
211-
.getExpression());
194+
return builder.greaterThan(getComparablePath(root, part),
195+
provider.next(part, Comparable.class).getExpression());
212196
case GREATER_THAN_EQUAL:
213-
return builder.greaterThanOrEqualTo(getComparablePath(root, part), provider.next(part, Comparable.class)
214-
.getExpression());
197+
return builder.greaterThanOrEqualTo(getComparablePath(root, part),
198+
provider.next(part, Comparable.class).getExpression());
215199
case BEFORE:
216200
case LESS_THAN:
217201
return builder.lessThan(getComparablePath(root, part), provider.next(part, Comparable.class).getExpression());
218202
case LESS_THAN_EQUAL:
219-
return builder.lessThanOrEqualTo(getComparablePath(root, part), provider.next(part, Comparable.class)
220-
.getExpression());
203+
return builder.lessThanOrEqualTo(getComparablePath(root, part),
204+
provider.next(part, Comparable.class).getExpression());
221205
case IS_NULL:
222-
return path.isNull();
206+
return getTypedPath(root, part).isNull();
223207
case IS_NOT_NULL:
224-
return path.isNotNull();
208+
return getTypedPath(root, part).isNotNull();
225209
case NOT_IN:
226-
return path.in(provider.next(part, Collection.class).getExpression()).not();
210+
return getTypedPath(root, part).in(provider.next(part, Collection.class).getExpression()).not();
227211
case IN:
228-
return path.in(provider.next(part, Collection.class).getExpression());
212+
return getTypedPath(root, part).in(provider.next(part, Collection.class).getExpression());
229213
case STARTING_WITH:
230214
case ENDING_WITH:
231215
case CONTAINING:
216+
case NOT_CONTAINING:
217+
218+
if (property.isCollection()) {
219+
220+
Expression<Collection<Object>> propertyExpression = traversePath(root, property);
221+
Expression<Object> parameterExpression = provider.next(part).getExpression();
222+
223+
Predicate isMember = builder.isMember(parameterExpression, propertyExpression);
224+
return type.equals(NOT_CONTAINING) ? isMember.not() : isMember;
225+
}
226+
232227
case LIKE:
233228
case NOT_LIKE:
234-
case NOT_CONTAINING:
235229
Expression<String> stringPath = getTypedPath(root, part);
236230
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
237231
Expression<String> parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
@@ -245,10 +239,12 @@ public Predicate build() {
245239
return builder.isFalse(falsePath);
246240
case SIMPLE_PROPERTY:
247241
ParameterMetadata<Object> expression = provider.next(part);
248-
return expression.isIsNullParameter() ? path.isNull() : builder.equal(upperIfIgnoreCase(path),
249-
upperIfIgnoreCase(expression.getExpression()));
242+
Expression<Object> path = getTypedPath(root, part);
243+
return expression.isIsNullParameter() ? path.isNull()
244+
: builder.equal(upperIfIgnoreCase(path), upperIfIgnoreCase(expression.getExpression()));
250245
case NEGATING_SIMPLE_PROPERTY:
251-
return builder.notEqual(upperIfIgnoreCase(path), upperIfIgnoreCase(provider.next(part).getExpression()));
246+
return builder.notEqual(upperIfIgnoreCase(getTypedPath(root, part)),
247+
upperIfIgnoreCase(provider.next(part).getExpression()));
252248
default:
253249
throw new IllegalArgumentException("Unsupported keyword " + type);
254250
}
@@ -287,5 +283,26 @@ private <T> Expression<T> upperIfIgnoreCase(Expression<? extends T> expression)
287283
private boolean canUpperCase(Expression<?> expression) {
288284
return String.class.equals(expression.getJavaType());
289285
}
286+
287+
/**
288+
* Returns a path to a {@link Comparable}.
289+
*
290+
* @param root
291+
* @param part
292+
* @return
293+
*/
294+
private Expression<? extends Comparable> getComparablePath(Root<?> root, Part part) {
295+
return getTypedPath(root, part);
296+
}
297+
298+
private <T> Expression<T> getTypedPath(Root<?> root, Part part) {
299+
return toExpressionRecursively(root, part.getProperty());
300+
}
301+
302+
private <T> Expression<T> traversePath(Path<?> root, PropertyPath path) {
303+
304+
Path<Object> result = root.get(path.getSegment());
305+
return (Expression<T>) (path.hasNext() ? traversePath(result, path.next()) : result);
306+
}
290307
}
291308
}

src/test/java/org/springframework/data/jpa/domain/sample/User.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.domain.sample;
1717

18+
import java.util.Arrays;
1819
import java.util.Date;
1920
import java.util.HashSet;
2021
import java.util.Set;
@@ -99,19 +100,20 @@ public User() {
99100
}
100101

101102
/**
102-
* Creates a new instance of {@code User} with preinitialized values for firstname, lastname and email address.
103+
* Creates a new instance of {@code User} with preinitialized values for firstname, lastname, email address and roles.
103104
*
104105
* @param firstname
105106
* @param lastname
106107
* @param emailAddress
108+
* @param roles
107109
*/
108-
public User(String firstname, String lastname, String emailAddress) {
110+
public User(String firstname, String lastname, String emailAddress, Role... roles) {
109111

110112
this.firstname = firstname;
111113
this.lastname = lastname;
112114
this.emailAddress = emailAddress;
113115
this.active = true;
114-
this.roles = new HashSet<Role>();
116+
this.roles = new HashSet<Role>(Arrays.asList(roles));
115117
this.colleagues = new HashSet<User>();
116118
this.attributes = new HashSet<String>();
117119
this.createdAt = new Date();

src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java

+41-17
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import org.springframework.data.domain.PageRequest;
3131
import org.springframework.data.domain.Slice;
3232
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.jpa.domain.sample.Role;
3334
import org.springframework.data.jpa.domain.sample.User;
35+
import org.springframework.data.jpa.repository.sample.RoleRepository;
3436
import org.springframework.data.jpa.repository.sample.UserRepository;
3537
import org.springframework.data.repository.query.QueryLookupStrategy;
3638
import org.springframework.test.context.ContextConfiguration;
@@ -49,22 +51,21 @@
4951
public class UserRepositoryFinderTests {
5052

5153
@Autowired UserRepository userRepository;
54+
@Autowired RoleRepository roleRepository;
5255

5356
User dave, carter, oliver;
57+
Role drummer, guitarist, singer;
5458

5559
@Before
5660
public void setUp() {
5761

58-
// This one matches both criterias
59-
dave = new User("Dave", "Matthews", "[email protected]");
60-
userRepository.save(dave);
62+
drummer = roleRepository.save(new Role("DRUMMER"));
63+
guitarist = roleRepository.save(new Role("GUITARIST"));
64+
singer = roleRepository.save(new Role("SINGER"));
6165

62-
// This one matches only the second one
63-
carter = new User("Carter", "Beauford", "[email protected]");
64-
userRepository.save(carter);
65-
66-
oliver = new User("Oliver August", "Matthews", "[email protected]");
67-
userRepository.save(oliver);
66+
dave = userRepository.save(new User("Dave", "Matthews", "[email protected]", singer));
67+
carter = userRepository.save(new User("Carter", "Beauford", "[email protected]", singer, drummer));
68+
oliver = userRepository.save(new User("Oliver August", "Matthews", "[email protected]"));
6869
}
6970

7071
/**
@@ -171,16 +172,18 @@ public void findByLastnameAndFirstnameAllIgnoringCase() throws Exception {
171172
*/
172173
@Test
173174
public void respectsPageableOrderOnQueryGenerateFromMethodName() throws Exception {
174-
Page<User> ascending = userRepository.findByLastnameIgnoringCase(
175-
new PageRequest(0, 10, new Sort(ASC, "firstname")), "Matthews");
176-
Page<User> descending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10,
177-
new Sort(DESC, "firstname")), "Matthews");
175+
Page<User> ascending = userRepository.findByLastnameIgnoringCase(new PageRequest(0, 10, new Sort(ASC, "firstname")),
176+
"Matthews");
177+
Page<User> descending = userRepository
178+
.findByLastnameIgnoringCase(new PageRequest(0, 10, new Sort(DESC, "firstname")), "Matthews");
178179
assertThat(ascending.getTotalElements(), is(2L));
179180
assertThat(descending.getTotalElements(), is(2L));
180-
assertThat(ascending.getContent().get(0).getFirstname(), is(not(equalTo(descending.getContent().get(0)
181-
.getFirstname()))));
182-
assertThat(ascending.getContent().get(0).getFirstname(), is(equalTo(descending.getContent().get(1).getFirstname())));
183-
assertThat(ascending.getContent().get(1).getFirstname(), is(equalTo(descending.getContent().get(0).getFirstname())));
181+
assertThat(ascending.getContent().get(0).getFirstname(),
182+
is(not(equalTo(descending.getContent().get(0).getFirstname()))));
183+
assertThat(ascending.getContent().get(0).getFirstname(),
184+
is(equalTo(descending.getContent().get(1).getFirstname())));
185+
assertThat(ascending.getContent().get(1).getFirstname(),
186+
is(equalTo(descending.getContent().get(0).getFirstname())));
184187
}
185188

186189
/**
@@ -202,4 +205,25 @@ public void executesQueryToSlice() {
202205
public void executesMethodWithNotContainingOnStringCorrectly() {
203206
assertThat(userRepository.findByLastnameNotContaining("u"), containsInAnyOrder(dave, oliver));
204207
}
208+
209+
/**
210+
* @see DATAJPA-829
211+
*/
212+
@Test
213+
public void translatesContainsToMemberOf() {
214+
215+
List<User> singers = userRepository.findByRolesContaining(singer);
216+
217+
assertThat(singers, hasSize(2));
218+
assertThat(singers, hasItems(dave, carter));
219+
assertThat(userRepository.findByRolesContaining(drummer), contains(carter));
220+
}
221+
222+
/**
223+
* @see DATAJPA-829
224+
*/
225+
@Test
226+
public void translatesNotContainsToNotMemberOf() {
227+
assertThat(userRepository.findByRolesNotContaining(drummer), hasItems(dave, oliver));
228+
}
205229
}

src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.domain.Pageable;
2929
import org.springframework.data.domain.Slice;
3030
import org.springframework.data.domain.Sort;
31+
import org.springframework.data.jpa.domain.sample.Role;
3132
import org.springframework.data.jpa.domain.sample.SpecialUser;
3233
import org.springframework.data.jpa.domain.sample.User;
3334
import org.springframework.data.jpa.repository.JpaRepository;
@@ -580,4 +581,14 @@ List<User> findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity
580581
* @see DATAJPA-830
581582
*/
582583
List<User> findByLastnameNotContaining(String part);
584+
585+
/**
586+
* DATAJPA-829
587+
*/
588+
List<User> findByRolesContaining(Role role);
589+
590+
/**
591+
* DATAJPA-829
592+
*/
593+
List<User> findByRolesNotContaining(Role role);
583594
}

0 commit comments

Comments
 (0)