diff --git a/pom.xml b/pom.xml
index 3d4491cc9d..26507eda7e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-jpa
- 1.9.0.BUILD-SNAPSHOT
+ 1.9.0.DATAJPA-218-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
diff --git a/src/main/java/org/springframework/data/jpa/domain/Example.java b/src/main/java/org/springframework/data/jpa/domain/Example.java
new file mode 100644
index 0000000000..e3cc4d5073
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/domain/Example.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jpa.domain;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.util.Assert;
+
+/**
+ * A wrapper around a prototype object that can be used in Query by Example queries
+ *
+ * @author Thomas Darimont
+ * @param
+ */
+public class Example {
+
+ private final T prototype;
+ private final Set ignoredAttributes;
+
+ /**
+ * Creates a new {@link Example} with the given {@code prototype}.
+ *
+ * @param prototype must not be {@literal null}
+ */
+ public Example(T prototype) {
+ this(prototype, Collections. emptySet());
+ }
+
+ /**
+ * Creates a new {@link Example} with the given {@code prototype} ignoring the given attributes.
+ *
+ * @param prototype prototype must not be {@literal null}
+ * @param attributeNames prototype must not be {@literal null}
+ */
+ public Example(T prototype, Set attributeNames) {
+
+ Assert.notNull(prototype, "Prototype must not be null!");
+ Assert.notNull(attributeNames, "attributeNames must not be null!");
+
+ this.prototype = prototype;
+ this.ignoredAttributes = attributeNames;
+ }
+
+ public T getPrototype() {
+ return prototype;
+ }
+
+ public Set getIgnoredAttributes() {
+ return Collections.unmodifiableSet(ignoredAttributes);
+ }
+
+ public boolean isAttributeIgnored(String attributePath) {
+ return ignoredAttributes.contains(attributePath);
+ }
+
+ public static Example exampleOf(T prototype) {
+ return new Example(prototype);
+ }
+
+ public static Builder newExample(T prototype) {
+ return new Builder(prototype);
+ }
+
+ /**
+ * A {@link Builder} for {@link Example}s.
+ *
+ * @author Thomas Darimont
+ * @param
+ */
+ public static class Builder {
+
+ private final T prototype;
+ private Set ignoredAttributeNames;
+
+ /**
+ * @param prototype
+ */
+ public Builder(T prototype) {
+
+ Assert.notNull(prototype, "Prototype must not be null!");
+
+ this.prototype = prototype;
+ }
+
+ /**
+ * Allows to specify attribute names that should be ignored.
+ *
+ * @param attributeNames
+ * @return
+ */
+ public Builder ignoring(String... attributeNames) {
+
+ Assert.notNull(attributeNames, "attributeNames must not be null!");
+
+ return ignoring(Arrays.asList(attributeNames));
+ }
+
+ /**
+ * Allows to specify attribute names that should be ignored.
+ *
+ * @param attributeNames
+ * @return
+ */
+ public Builder ignoring(Collection attributeNames) {
+
+ Assert.notNull(attributeNames, "attributeNames must not be null!");
+
+ this.ignoredAttributeNames = new HashSet(attributeNames);
+ return this;
+ }
+
+ /**
+ * Constructs the actual {@link Example} instance.
+ *
+ * @return
+ */
+ public Example build() {
+ return new Example(prototype, ignoredAttributeNames);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java
index c7d0b5f1cd..9132b8a45d 100644
--- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java
+++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java
@@ -21,6 +21,7 @@
import javax.persistence.EntityManager;
import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Example;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
@@ -78,7 +79,7 @@ public interface JpaRepository extends PagingAndSort
void deleteInBatch(Iterable entities);
/**
- * Deletes all entites in a batch call.
+ * Deletes all entities in a batch call.
*/
void deleteAllInBatch();
@@ -90,4 +91,15 @@ public interface JpaRepository extends PagingAndSort
* @see EntityManager#getReference(Class, Object)
*/
T getOne(ID id);
+
+ /**
+ * Returns all instances of the type specified by the given {@link Example}.
+ *
+ * This method is deliberately not named {@code findByExample} to not interfere
+ * with existing repository methods that rely on query derivation.
+ *
+ * @param example must not be {@literal null}.
+ * @return
+ */
+ List findWithExample(Example example);
}
diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
index 5376c13c1e..0191dfb7ac 100644
--- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
+++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java
@@ -19,6 +19,7 @@
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -37,12 +38,16 @@
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
+import javax.persistence.metamodel.Attribute;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Example;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
@@ -54,6 +59,7 @@
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
/**
* Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer
@@ -411,6 +417,38 @@ public List findAll(Specification spec, Sort sort) {
return getQuery(spec, sort).getResultList();
}
+ /* (non-Javadoc)
+ * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example)
+ */
+ public List findWithExample(Example example) {
+
+ Assert.notNull(example, "Example must not be null!");
+
+ CriteriaBuilder builder = em.getCriteriaBuilder();
+ CriteriaQuery query = builder.createQuery(getDomainClass());
+ Root root = query.from(getDomainClass());
+
+ BeanWrapper bean = new BeanWrapperImpl(example.getPrototype());
+
+ List predicates = new ArrayList();
+ for (Attribute, ?> attribute : em.getMetamodel().managedType(getDomainClass()).getAttributes()) {
+
+ Object value = bean.getPropertyValue(attribute.getName());
+
+ // TODO support different matching modes configured on the provided Example
+ if (value == null //
+ || (value instanceof Collection && CollectionUtils.isEmpty((Collection>) value))
+ || (value instanceof Map && CollectionUtils.isEmpty((Map, ?>) value))
+ || example.isAttributeIgnored(attribute.getName())) {
+ continue;
+ }
+
+ predicates.add(builder.equal(root.get(attribute.getName()), value));
+ }
+
+ return em.createQuery(query.where(predicates.toArray(new Predicate[predicates.size()]))).getResultList();
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#count()
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 962972c468..87f8baf2d4 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
@@ -377,6 +377,10 @@ public Date getDateOfBirth() {
public void setDateOfBirth(Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
+
+ public void setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ }
/*
* (non-Javadoc)
diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
index e4f644203c..7d99804e9f 100644
--- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
@@ -16,9 +16,11 @@
package org.springframework.data.jpa.repository;
import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
import static org.springframework.data.domain.Sort.Direction.*;
import static org.springframework.data.jpa.domain.Specifications.*;
+import static org.springframework.data.jpa.domain.Specifications.not;
import static org.springframework.data.jpa.domain.sample.UserSpecifications.*;
import java.util.ArrayList;
@@ -55,6 +57,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.jpa.domain.Example;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.sample.Address;
import org.springframework.data.jpa.domain.sample.Role;
@@ -1904,6 +1907,41 @@ public void accept(User user) {
assertThat(users, hasSize(2));
}
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void queryByExample() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+ prototype.setCreatedAt(null);
+
+ List users = repository.findWithExample(Example.exampleOf(prototype));
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void queryByExampleWithExcludedAttributes() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+
+ List users = repository.findWithExample(Example.newExample(prototype).ignoring("createdAt").build());
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
private Page executeSpecWithSort(Sort sort) {