Skip to content

Commit 6ec173b

Browse files
thomasdarimontodrotbohm
authored andcommitted
DATAJPA-218 - Support extracting parameters from a bean parameter.
Added prototypic support for query by example queries to SimpleJpaRepositories. Clients can use an Example object to wrap an existing prototype entity instance that will be used to derive a query from. Related tickets: DATACMNS-810. Original pull request: #164.
1 parent a1509f9 commit 6ec173b

File tree

5 files changed

+231
-1
lines changed

5 files changed

+231
-1
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain;
17+
18+
import java.util.Arrays;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.HashSet;
22+
import java.util.Set;
23+
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* A wrapper around a prototype object that can be used in <quote>Query by Example<quote> queries
28+
*
29+
* @author Thomas Darimont
30+
* @param <T>
31+
*/
32+
public class Example<T> {
33+
34+
private final T prototype;
35+
private final Set<String> ignoredAttributes;
36+
37+
/**
38+
* Creates a new {@link Example} with the given {@code prototype}.
39+
*
40+
* @param prototype must not be {@literal null}
41+
*/
42+
public Example(T prototype) {
43+
this(prototype, Collections.<String> emptySet());
44+
}
45+
46+
/**
47+
* Creates a new {@link Example} with the given {@code prototype} ignoring the given attributes.
48+
*
49+
* @param prototype prototype must not be {@literal null}
50+
* @param attributeNames prototype must not be {@literal null}
51+
*/
52+
public Example(T prototype, Set<String> attributeNames) {
53+
54+
Assert.notNull(prototype, "Prototype must not be null!");
55+
Assert.notNull(attributeNames, "attributeNames must not be null!");
56+
57+
this.prototype = prototype;
58+
this.ignoredAttributes = attributeNames;
59+
}
60+
61+
public T getPrototype() {
62+
return prototype;
63+
}
64+
65+
public Set<String> getIgnoredAttributes() {
66+
return Collections.unmodifiableSet(ignoredAttributes);
67+
}
68+
69+
public boolean isAttributeIgnored(String attributePath) {
70+
return ignoredAttributes.contains(attributePath);
71+
}
72+
73+
public static <T> Example<T> exampleOf(T prototype) {
74+
return new Example<T>(prototype);
75+
}
76+
77+
public static <T> Builder<T> newExample(T prototype) {
78+
return new Builder<T>(prototype);
79+
}
80+
81+
/**
82+
* A {@link Builder} for {@link Example}s.
83+
*
84+
* @author Thomas Darimont
85+
* @param <T>
86+
*/
87+
public static class Builder<T> {
88+
89+
private final T prototype;
90+
private Set<String> ignoredAttributeNames;
91+
92+
/**
93+
* @param prototype
94+
*/
95+
public Builder(T prototype) {
96+
97+
Assert.notNull(prototype, "Prototype must not be null!");
98+
99+
this.prototype = prototype;
100+
}
101+
102+
/**
103+
* Allows to specify attribute names that should be ignored.
104+
*
105+
* @param attributeNames
106+
* @return
107+
*/
108+
public Builder<T> ignoring(String... attributeNames) {
109+
110+
Assert.notNull(attributeNames, "attributeNames must not be null!");
111+
112+
return ignoring(Arrays.asList(attributeNames));
113+
}
114+
115+
/**
116+
* Allows to specify attribute names that should be ignored.
117+
*
118+
* @param attributeNames
119+
* @return
120+
*/
121+
public Builder<T> ignoring(Collection<String> attributeNames) {
122+
123+
Assert.notNull(attributeNames, "attributeNames must not be null!");
124+
125+
this.ignoredAttributeNames = new HashSet<String>(attributeNames);
126+
return this;
127+
}
128+
129+
/**
130+
* Constructs the actual {@link Example} instance.
131+
*
132+
* @return
133+
*/
134+
public Example<T> build() {
135+
return new Example<T>(prototype, ignoredAttributeNames);
136+
}
137+
}
138+
}

src/main/java/org/springframework/data/jpa/repository/JpaRepository.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import javax.persistence.EntityManager;
2222

2323
import org.springframework.data.domain.Sort;
24+
import org.springframework.data.jpa.domain.Example;
2425
import org.springframework.data.repository.NoRepositoryBean;
2526
import org.springframework.data.repository.PagingAndSortingRepository;
2627

@@ -78,7 +79,7 @@ public interface JpaRepository<T, ID extends Serializable> extends PagingAndSort
7879
void deleteInBatch(Iterable<T> entities);
7980

8081
/**
81-
* Deletes all entites in a batch call.
82+
* Deletes all entities in a batch call.
8283
*/
8384
void deleteAllInBatch();
8485

@@ -90,4 +91,15 @@ public interface JpaRepository<T, ID extends Serializable> extends PagingAndSort
9091
* @see EntityManager#getReference(Class, Object)
9192
*/
9293
T getOne(ID id);
94+
95+
/**
96+
* Returns all instances of the type specified by the given {@link Example}.
97+
*
98+
* This method is deliberately <b>not<b> named {@code findByExample} to not interfere
99+
* with existing repository methods that rely on query derivation.
100+
*
101+
* @param example must not be {@literal null}.
102+
* @return
103+
*/
104+
List<T> findWithExample(Example<T> example);
93105
}

src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.Serializable;
2121
import java.util.ArrayList;
22+
import java.util.Collection;
2223
import java.util.Collections;
2324
import java.util.HashMap;
2425
import java.util.List;
@@ -37,12 +38,16 @@
3738
import javax.persistence.criteria.Path;
3839
import javax.persistence.criteria.Predicate;
3940
import javax.persistence.criteria.Root;
41+
import javax.persistence.metamodel.Attribute;
4042

43+
import org.springframework.beans.BeanWrapper;
44+
import org.springframework.beans.BeanWrapperImpl;
4145
import org.springframework.dao.EmptyResultDataAccessException;
4246
import org.springframework.data.domain.Page;
4347
import org.springframework.data.domain.PageImpl;
4448
import org.springframework.data.domain.Pageable;
4549
import org.springframework.data.domain.Sort;
50+
import org.springframework.data.jpa.domain.Example;
4651
import org.springframework.data.jpa.domain.Specification;
4752
import org.springframework.data.jpa.provider.PersistenceProvider;
4853
import org.springframework.data.jpa.repository.EntityGraph;
@@ -54,6 +59,7 @@
5459
import org.springframework.stereotype.Repository;
5560
import org.springframework.transaction.annotation.Transactional;
5661
import org.springframework.util.Assert;
62+
import org.springframework.util.CollectionUtils;
5763

5864
/**
5965
* Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer
@@ -411,6 +417,38 @@ public List<T> findAll(Specification<T> spec, Sort sort) {
411417
return getQuery(spec, sort).getResultList();
412418
}
413419

420+
/* (non-Javadoc)
421+
* @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example)
422+
*/
423+
public List<T> findWithExample(Example<T> example) {
424+
425+
Assert.notNull(example, "Example must not be null!");
426+
427+
CriteriaBuilder builder = em.getCriteriaBuilder();
428+
CriteriaQuery<T> query = builder.createQuery(getDomainClass());
429+
Root<T> root = query.from(getDomainClass());
430+
431+
BeanWrapper bean = new BeanWrapperImpl(example.getPrototype());
432+
433+
List<Predicate> predicates = new ArrayList<Predicate>();
434+
for (Attribute<?, ?> attribute : em.getMetamodel().managedType(getDomainClass()).getAttributes()) {
435+
436+
Object value = bean.getPropertyValue(attribute.getName());
437+
438+
// TODO support different matching modes configured on the provided Example
439+
if (value == null //
440+
|| (value instanceof Collection && CollectionUtils.isEmpty((Collection<?>) value))
441+
|| (value instanceof Map && CollectionUtils.isEmpty((Map<?, ?>) value))
442+
|| example.isAttributeIgnored(attribute.getName())) {
443+
continue;
444+
}
445+
446+
predicates.add(builder.equal(root.get(attribute.getName()), value));
447+
}
448+
449+
return em.createQuery(query.where(predicates.toArray(new Predicate[predicates.size()]))).getResultList();
450+
}
451+
414452
/*
415453
* (non-Javadoc)
416454
* @see org.springframework.data.repository.CrudRepository#count()

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ public Date getDateOfBirth() {
381381
public void setDateOfBirth(Date dateOfBirth) {
382382
this.dateOfBirth = dateOfBirth;
383383
}
384+
385+
public void setCreatedAt(Date createdAt) {
386+
this.createdAt = createdAt;
387+
}
384388

385389
/*
386390
* (non-Javadoc)

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package org.springframework.data.jpa.repository;
1717

1818
import static org.hamcrest.Matchers.*;
19+
import static org.hamcrest.Matchers.not;
1920
import static org.junit.Assert.*;
2021
import static org.springframework.data.domain.Sort.Direction.*;
2122
import static org.springframework.data.jpa.domain.Specifications.*;
23+
import static org.springframework.data.jpa.domain.Specifications.not;
2224
import static org.springframework.data.jpa.domain.sample.UserSpecifications.*;
2325

2426
import java.util.ArrayList;
@@ -55,6 +57,7 @@
5557
import org.springframework.data.domain.Sort;
5658
import org.springframework.data.domain.Sort.Direction;
5759
import org.springframework.data.domain.Sort.Order;
60+
import org.springframework.data.jpa.domain.Example;
5861
import org.springframework.data.jpa.domain.Specification;
5962
import org.springframework.data.jpa.domain.sample.Address;
6063
import org.springframework.data.jpa.domain.sample.Role;
@@ -1904,6 +1907,41 @@ public void accept(User user) {
19041907

19051908
assertThat(users, hasSize(2));
19061909
}
1910+
1911+
/**
1912+
* @see DATAJPA-218
1913+
*/
1914+
@Test
1915+
public void queryByExample() {
1916+
1917+
flushTestUsers();
1918+
1919+
User prototype = new User();
1920+
prototype.setAge(28);
1921+
prototype.setCreatedAt(null);
1922+
1923+
List<User> users = repository.findWithExample(Example.exampleOf(prototype));
1924+
1925+
assertThat(users, hasSize(1));
1926+
assertThat(users.get(0), is(firstUser));
1927+
}
1928+
1929+
/**
1930+
* @see DATAJPA-218
1931+
*/
1932+
@Test
1933+
public void queryByExampleWithExcludedAttributes() {
1934+
1935+
flushTestUsers();
1936+
1937+
User prototype = new User();
1938+
prototype.setAge(28);
1939+
1940+
List<User> users = repository.findWithExample(Example.newExample(prototype).ignoring("createdAt").build());
1941+
1942+
assertThat(users, hasSize(1));
1943+
assertThat(users.get(0), is(firstUser));
1944+
}
19071945

19081946
private Page<User> executeSpecWithSort(Sort sort) {
19091947

0 commit comments

Comments
 (0)