diff --git a/.gitignore b/.gitignore
index 5cce85cc34..6d0b68c764 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
target/
+.idea/
.settings/
+*.iml
.project
.classpath
.springBeans
diff --git a/pom.xml b/pom.xml
index 4a4c26e2ce..5039c0e98d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,8 +5,7 @@
org.springframework.data
spring-data-jpa
- 1.10.0.BUILD-SNAPSHOT
-
+ 1.10.0.DATAJPA-218-SNAPSHOT
Spring Data JPA
Spring Data module for JPA repositories.
http://projects.spring.io/spring-data-jpa
@@ -26,7 +25,7 @@
1.8.0.10
2.0.0
2.4.0
- 1.12.0.BUILD-SNAPSHOT
+ 1.12.0.DATACMNS-810-SNAPSHOT
reuseReports
diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc
index e5ca7b6dfe..6e02d38ff1 100644
--- a/src/main/asciidoc/index.adoc
+++ b/src/main/asciidoc/index.adoc
@@ -1,5 +1,5 @@
= Spring Data JPA - Reference Documentation
-Oliver Gierke; Thomas Darimont; Christoph Strobl
+Oliver Gierke; Thomas Darimont; Christoph Strobl; Mark Paluch
:revnumber: {version}
:revdate: {localdate}
:toc:
@@ -7,7 +7,7 @@ Oliver Gierke; Thomas Darimont; Christoph Strobl
:spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc
:spring-framework-docs: http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html
-(C) 2008-2015 The original authors.
+(C) 2008-2016 The original authors.
NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
diff --git a/src/main/asciidoc/jpa.adoc b/src/main/asciidoc/jpa.adoc
index db43024c4b..f126a66719 100644
--- a/src/main/asciidoc/jpa.adoc
+++ b/src/main/asciidoc/jpa.adoc
@@ -604,6 +604,9 @@ List customers = customerRepository.findAll(
As you can see, `Specifications` offers some glue-code methods to chain and combine `Specification` instances. Thus extending your data access layer is just a matter of creating new `Specification` implementations and combining them with ones already existing.
====
+include::{spring-data-commons-docs}/query-by-example.adoc[]
+include::query-by-example.adoc[]
+
[[transactions]]
== Transactionality
CRUD methods on repository instances are transactional by default. For reading operations the transaction configuration `readOnly` flag is set to true, all others are configured with a plain `@Transactional` so that default transaction configuration applies. For details see JavaDoc of `CrudRepository`. If you need to tweak transaction configuration for one of the methods declared in a repository simply redeclare the method in your repository interface as follows:
diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc
new file mode 100644
index 0000000000..92654f16e3
--- /dev/null
+++ b/src/main/asciidoc/query-by-example.adoc
@@ -0,0 +1,68 @@
+[[query.by.example.execution]]
+== Executing Example
+
+In Spring Data JPA you can use Query by Example with Repositories.
+
+.Query by Example using a Repository
+====
+[source, java]
+----
+public interface PersonRepository extends JpaRepository {
+
+}
+
+public class PersonService {
+
+ @Autowired PersonRepository personRepository;
+
+ public List findPeople(Person probe) {
+ return personRepository.findAll(Example.of(probe));
+ }
+}
+----
+====
+
+An `Example` containing an untyped `ExampleSpec` uses the Repository type. Typed `ExampleSpec` use their type for creating JPA queries.
+
+NOTE: Only SingularAttribute properties can be used for property matching.
+
+
+Property specifier accepts property names (e.g. "firstname" and "lastname"). You can navigate by chaining properties together with dots ("address.city"). You can tune it with matching options and case sensitivity.
+
+[cols="1,2", options="header"]
+.`StringMatcher` options
+|===
+| Matching
+| Logical result
+
+| `DEFAULT` (case-sensitive)
+| `firstname = ?0`
+
+| `DEFAULT` (case-insensitive)
+| `LOWER(firstname) = LOWER(?0)`
+
+| `EXACT` (case-sensitive)
+| `firstname = ?0`
+
+| `EXACT` (case-insensitive)
+| `LOWER(firstname) = LOWER(?0)`
+
+| `STARTING` (case-sensitive)
+| `firstname like ?0 + '%'`
+
+| `STARTING` (case-insensitive)
+| `LOWER(firstname) like LOWER(?0) + '%'`
+
+| `ENDING` (case-sensitive)
+| `firstname like '%' + ?0`
+
+| `ENDING` (case-insensitive)
+| `LOWER(firstname) like '%' + LOWER(?0)`
+
+| `CONTAINING` (case-sensitive)
+| `firstname like '%' + ?0 + '%'`
+
+| `CONTAINING` (case-insensitive)
+| `LOWER(firstname) like '%' + LOWER(?0) + '%'`
+
+|===
diff --git a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java
new file mode 100644
index 0000000000..72cee28f22
--- /dev/null
+++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2016 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.convert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.Path;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import javax.persistence.metamodel.Attribute;
+import javax.persistence.metamodel.Attribute.PersistentAttributeType;
+import javax.persistence.metamodel.ManagedType;
+import javax.persistence.metamodel.SingularAttribute;
+
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleSpec;
+import org.springframework.data.repository.core.support.ExampleSpecAccessor;
+import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link QueryByExamplePredicateBuilder} creates a single {@link CriteriaBuilder#and(Predicate...)} combined
+ * {@link Predicate} for a given {@link Example}.
+ * The builder includes any {@link SingularAttribute} of the {@link Example#getProbe()} applying {@link String} and
+ * {@literal null} matching strategies configured on the {@link Example}. Ignored paths are no matter of their actual
+ * value not considered.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 1.10
+ */
+public class QueryByExamplePredicateBuilder {
+
+ private static final Set ASSOCIATION_TYPES;
+
+ static {
+ ASSOCIATION_TYPES = new HashSet(Arrays.asList(PersistentAttributeType.MANY_TO_MANY,
+ PersistentAttributeType.MANY_TO_ONE, PersistentAttributeType.ONE_TO_MANY, PersistentAttributeType.ONE_TO_ONE));
+ }
+
+ /**
+ * Extract the {@link Predicate} representing the {@link Example}.
+ *
+ * @param root must not be {@literal null}.
+ * @param cb must not be {@literal null}.
+ * @param example must not be {@literal null}.
+ * @return never {@literal null}.
+ */
+ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Example example) {
+
+ Assert.notNull(root, "Root must not be null!");
+ Assert.notNull(cb, "CriteriaBuilder must not be null!");
+ Assert.notNull(example, "Example must not be null!");
+
+ List predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(),
+ example.getProbeType(), new ExampleSpecAccessor(example.getExampleSpec()),
+ new PathNode("root", null, example.getProbe()));
+
+ if (predicates.isEmpty()) {
+ return cb.isTrue(cb.literal(true));
+ }
+
+ if (predicates.size() == 1) {
+ return predicates.iterator().next();
+ }
+
+ return cb.and(predicates.toArray(new Predicate[predicates.size()]));
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ static List getPredicates(String path, CriteriaBuilder cb, Path> from, ManagedType> type, Object value,
+ Class> probeType, ExampleSpecAccessor exampleAccessor, PathNode currentNode) {
+
+ List predicates = new ArrayList();
+ DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(value);
+
+ for (SingularAttribute attribute : type.getSingularAttributes()) {
+
+ String currentPath = !StringUtils.hasText(path) ? attribute.getName() : path + "." + attribute.getName();
+
+ if (exampleAccessor.isIgnoredPath(currentPath)) {
+ continue;
+ }
+
+ Object attributeValue = exampleAccessor.getValueTransformerForPath(currentPath)
+ .convert(beanWrapper.getPropertyValue(attribute.getName()));
+
+ if (attributeValue == null) {
+
+ if (exampleAccessor.getNullHandler().equals(ExampleSpec.NullHandler.INCLUDE)) {
+ predicates.add(cb.isNull(from.get(attribute)));
+ }
+ continue;
+ }
+
+ if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) {
+
+ predicates.addAll(getPredicates(currentPath, cb, from.get(attribute.getName()),
+ (ManagedType>) attribute.getType(), attributeValue, probeType, exampleAccessor, currentNode));
+ continue;
+ }
+
+ if (isAssociation(attribute)) {
+
+ if (!(from instanceof From)) {
+ throw new JpaSystemException(new IllegalArgumentException(
+ String.format("Unexpected path type for %s. Found % where From.class was expected.", currentPath, from)));
+ }
+
+ PathNode node = currentNode.add(attribute.getName(), attributeValue);
+ if (node.spansCycle()) {
+ throw new InvalidDataAccessApiUsageException(
+ String.format("Path '%s' from root %s must not span a cyclic property reference!\r\n%s", currentPath,
+ ClassUtils.getShortName(probeType), node));
+ }
+
+ predicates.addAll(getPredicates(currentPath, cb, ((From, ?>) from).join(attribute.getName()),
+ (ManagedType>) attribute.getType(), attributeValue, probeType, exampleAccessor, node));
+
+ continue;
+ }
+
+ if (attribute.getJavaType().equals(String.class)) {
+
+ Expression expression = from.get(attribute);
+ if (exampleAccessor.isIgnoreCaseForPath(currentPath)) {
+ expression = cb.lower(expression);
+ attributeValue = attributeValue.toString().toLowerCase();
+ }
+
+ switch (exampleAccessor.getStringMatcherForPath(currentPath)) {
+
+ case DEFAULT:
+ case EXACT:
+ predicates.add(cb.equal(expression, attributeValue));
+ break;
+ case CONTAINING:
+ predicates.add(cb.like(expression, "%" + attributeValue + "%"));
+ break;
+ case STARTING:
+ predicates.add(cb.like(expression, attributeValue + "%"));
+ break;
+ case ENDING:
+ predicates.add(cb.like(expression, "%" + attributeValue));
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported StringMatcher " + exampleAccessor.getStringMatcherForPath(currentPath));
+ }
+ } else {
+ predicates.add(cb.equal(from.get(attribute), attributeValue));
+ }
+ }
+
+ return predicates;
+ }
+
+ private static boolean isAssociation(Attribute, ?> attribute) {
+ return ASSOCIATION_TYPES.contains(attribute.getPersistentAttributeType());
+ }
+
+ /**
+ * {@link PathNode} is used to dynamically grow a directed graph structure that allows to detect cycles within its
+ * direct predecessor nodes by comparing parent node values using {@link System#identityHashCode(Object)}.
+ *
+ * @author Christoph Strobl
+ */
+ private static class PathNode {
+
+ String name;
+ PathNode parent;
+ List siblings = new ArrayList();;
+ Object value;
+
+ public PathNode(String edge, PathNode parent, Object value) {
+
+ this.name = edge;
+ this.parent = parent;
+ this.value = value;
+ }
+
+ PathNode add(String attribute, Object value) {
+
+ PathNode node = new PathNode(attribute, this, value);
+ siblings.add(node);
+ return node;
+ }
+
+ boolean spansCycle() {
+
+ if (value == null) {
+ return false;
+ }
+
+ String identityHex = ObjectUtils.getIdentityHexString(value);
+ PathNode tmp = parent;
+
+ while (tmp != null) {
+
+ if (ObjectUtils.getIdentityHexString(tmp.value).equals(identityHex)) {
+ return true;
+ }
+ tmp = tmp.parent;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+
+ StringBuilder sb = new StringBuilder();
+ if (parent != null) {
+ sb.append(parent.toString());
+ sb.append(" -");
+ sb.append(name);
+ sb.append("-> ");
+ }
+
+ sb.append("[{ ");
+ sb.append(ObjectUtils.nullSafeToString(value));
+ sb.append(" }]");
+ return sb.toString();
+ }
+ }
+}
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..299616d00a 100644
--- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java
+++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2013 the original author or authors.
+ * Copyright 2008-2016 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.
@@ -20,17 +20,22 @@
import javax.persistence.EntityManager;
+import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
/**
* JPA specific extension of {@link org.springframework.data.repository.Repository}.
- *
+ *
* @author Oliver Gierke
+ * @author Christoph Strobl
+ * @author Mark Paluch
*/
@NoRepositoryBean
-public interface JpaRepository extends PagingAndSortingRepository {
+public interface JpaRepository
+ extends PagingAndSortingRepository, QueryByExampleExecutor {
/*
* (non-Javadoc)
@@ -78,7 +83,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 +95,17 @@ public interface JpaRepository extends PagingAndSort
* @see EntityManager#getReference(Class, Object)
*/
T getOne(ID id);
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
+ */
+ @Override
+ List findAll(Example example);
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
+ */
+ @Override
+ List findAll(Example example, Sort sort);
+
}
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..e33b3d11c3 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2015 the original author or authors.
+ * Copyright 2008-2016 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.
@@ -39,10 +39,13 @@
import javax.persistence.criteria.Root;
import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.data.domain.Example;
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.domain.TypedExampleSpec;
+import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph;
@@ -62,13 +65,14 @@
* @author Oliver Gierke
* @author Eberhard Wolff
* @author Thomas Darimont
+ * @author Mark Paluch
* @param the type of the entity to handle
* @param the type of the entity's identifier
*/
@Repository
@Transactional(readOnly = true)
-public class SimpleJpaRepository implements JpaRepository,
- JpaSpecificationExecutor {
+public class SimpleJpaRepository
+ implements JpaRepository, JpaSpecificationExecutor {
private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
@@ -368,7 +372,7 @@ public Page findAll(Pageable pageable) {
return new PageImpl(findAll());
}
- return findAll(null, pageable);
+ return findAll((Specification) null, pageable);
}
/*
@@ -411,6 +415,68 @@ public List findAll(Specification spec, Sort sort) {
return getQuery(spec, sort).getResultList();
}
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public S findOne(Example example) {
+ try {
+ return getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getSingleResult();
+ } catch (NoResultException e) {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#count(org.springframework.data.domain.Example)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public long count(Example example) {
+ return executeCountQuery(getCountQuery(new ExampleSpecification(example), getResultType(example)));
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#exists(org.springframework.data.domain.Example)
+ */
+ @Override
+ public boolean exists(Example example) {
+ return !getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList()
+ .isEmpty();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
+ */
+ @Override
+ public List findAll(Example example) {
+ return getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
+ */
+ @Override
+ public List findAll(Example example, Sort sort) {
+ return getQuery(new ExampleSpecification(example), getResultType(example), sort).getResultList();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable)
+ */
+ @Override
+ public Page findAll(Example example, Pageable pageable) {
+
+ ExampleSpecification spec = new ExampleSpecification(example);
+ TypedQuery query = getQuery(new ExampleSpecification(example), getResultType(example), pageable);
+ return pageable == null ? new PageImpl(query.getResultList())
+ : readPage(query, getResultType(example), pageable, spec);
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.repository.CrudRepository#count()
@@ -489,26 +555,41 @@ public void flush() {
/**
* Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and
* {@link Specification}.
- *
+ *
* @param query must not be {@literal null}.
* @param spec can be {@literal null}.
* @param pageable can be {@literal null}.
* @return
*/
protected Page readPage(TypedQuery query, Pageable pageable, Specification spec) {
+ return readPage(query, getDomainClass(), pageable, spec);
+ }
+
+ /**
+ * Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and
+ * {@link Specification}.
+ *
+ * @param query must not be {@literal null}.
+ * @param domainClass must not be {@literal null}.
+ * @param spec can be {@literal null}.
+ * @param pageable can be {@literal null}.
+ * @return
+ */
+ protected Page readPage(TypedQuery query, Class domainClass, Pageable pageable,
+ Specification spec) {
query.setFirstResult(pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
- Long total = executeCountQuery(getCountQuery(spec));
- List content = total > pageable.getOffset() ? query.getResultList() : Collections. emptyList();
+ Long total = executeCountQuery(getCountQuery(spec, domainClass));
+ List content = total > pageable.getOffset() ? query.getResultList() : Collections. emptyList();
- return new PageImpl(content, pageable, total);
+ return new PageImpl(content, pageable, total);
}
/**
* Creates a new {@link TypedQuery} from the given {@link Specification}.
- *
+ *
* @param spec can be {@literal null}.
* @param pageable can be {@literal null}.
* @return
@@ -516,7 +597,21 @@ protected Page readPage(TypedQuery query, Pageable pageable, Specification
protected TypedQuery getQuery(Specification spec, Pageable pageable) {
Sort sort = pageable == null ? null : pageable.getSort();
- return getQuery(spec, sort);
+ return getQuery(spec, getDomainClass(), sort);
+ }
+
+ /**
+ * Creates a new {@link TypedQuery} from the given {@link Specification}.
+ *
+ * @param spec can be {@literal null}.
+ * @param domainClass must not be {@literal null}.
+ * @param pageable can be {@literal null}.
+ * @return
+ */
+ protected TypedQuery getQuery(Specification spec, Class domainClass, Pageable pageable) {
+
+ Sort sort = pageable == null ? null : pageable.getSort();
+ return getQuery(spec, domainClass, sort);
}
/**
@@ -527,11 +622,23 @@ protected TypedQuery getQuery(Specification spec, Pageable pageable) {
* @return
*/
protected TypedQuery getQuery(Specification spec, Sort sort) {
+ return getQuery(spec, getDomainClass(), sort);
+ }
+
+ /**
+ * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}.
+ *
+ * @param spec can be {@literal null}.
+ * @param domainClass must not be {@literal null}.
+ * @param sort can be {@literal null}.
+ * @return
+ */
+ protected TypedQuery getQuery(Specification spec, Class domainClass, Sort sort) {
CriteriaBuilder builder = em.getCriteriaBuilder();
- CriteriaQuery query = builder.createQuery(getDomainClass());
+ CriteriaQuery query = builder.createQuery(domainClass);
- Root root = applySpecificationToCriteria(spec, query);
+ Root root = applySpecificationToCriteria(spec, domainClass, query);
query.select(root);
if (sort != null) {
@@ -548,11 +655,22 @@ protected TypedQuery getQuery(Specification spec, Sort sort) {
* @return
*/
protected TypedQuery getCountQuery(Specification spec) {
+ return getCountQuery(spec, getDomainClass());
+ }
+
+ /**
+ * Creates a new count query for the given {@link Specification}.
+ *
+ * @param spec can be {@literal null}.
+ * @param domainClass must not be {@literal null}.
+ * @return
+ */
+ protected TypedQuery getCountQuery(Specification spec, Class domainClass) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(Long.class);
- Root root = applySpecificationToCriteria(spec, query);
+ Root root = applySpecificationToCriteria(spec, domainClass, query);
if (query.isDistinct()) {
query.select(builder.countDistinct(root));
@@ -571,9 +689,24 @@ protected TypedQuery getCountQuery(Specification spec) {
* @return
*/
private Root applySpecificationToCriteria(Specification spec, CriteriaQuery query) {
+ return applySpecificationToCriteria(spec, getDomainClass(), query);
+
+ }
+
+ /**
+ * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
+ *
+ * @param spec can be {@literal null}.
+ * @param domainClass must not be {@literal null}.
+ * @param query must not be {@literal null}.
+ * @return
+ */
+ private Root applySpecificationToCriteria(Specification spec, Class domainClass,
+ CriteriaQuery query) {
Assert.notNull(query);
- Root root = query.from(getDomainClass());
+ Assert.notNull(domainClass);
+ Root root = query.from(domainClass);
if (spec == null) {
return root;
@@ -589,14 +722,14 @@ private Root applySpecificationToCriteria(Specification spec, Criteria
return root;
}
- private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) {
+ private TypedQuery applyRepositoryMethodMetadata(TypedQuery query) {
if (metadata == null) {
return query;
}
LockModeType type = metadata.getLockModeType();
- TypedQuery toReturn = type == null ? query : query.setLockMode(type);
+ TypedQuery toReturn = type == null ? query : query.setLockMode(type);
applyQueryHints(toReturn);
@@ -610,6 +743,15 @@ private void applyQueryHints(Query query) {
}
}
+
+ private Class getResultType(Example example) {
+
+ if(example.getExampleSpec() instanceof TypedExampleSpec>){
+ return example.getResultType();
+ }
+ return (Class) getDomainClass();
+ }
+
/**
* Executes a count query and transparently sums up all values returned.
*
@@ -660,4 +802,37 @@ public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuild
return path.in(parameter);
}
}
+
+ /**
+ * {@link Specification} that gives access to the {@link Predicate} instance representing the values contained in the
+ * {@link Example}.
+ *
+ * @author Christoph Strobl
+ * @since 1.10
+ * @param
+ */
+ private static class ExampleSpecification implements Specification {
+
+ private final Example example;
+
+ /**
+ * Creates new {@link ExampleSpecification}.
+ *
+ * @param example
+ */
+ public ExampleSpecification(Example example) {
+
+ Assert.notNull(example, "Example must not be null!");
+ this.example = example;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.jpa.domain.Specification#toPredicate(javax.persistence.criteria.Root, javax.persistence.criteria.CriteriaQuery, javax.persistence.criteria.CriteriaBuilder)
+ */
+ @Override
+ public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) {
+ return QueryByExamplePredicateBuilder.getPredicate(root, cb, example);
+ }
+ }
}
diff --git a/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java
new file mode 100644
index 0000000000..3ddf152da5
--- /dev/null
+++ b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2016 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.convert;
+
+import static org.hamcrest.core.IsEqual.*;
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.data.domain.Example.*;
+
+import java.lang.reflect.Member;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import javax.persistence.Id;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.Expression;
+import javax.persistence.criteria.Path;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import javax.persistence.metamodel.Attribute.PersistentAttributeType;
+import javax.persistence.metamodel.EntityType;
+import javax.persistence.metamodel.ManagedType;
+import javax.persistence.metamodel.SingularAttribute;
+import javax.persistence.metamodel.Type;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class QueryByExamplePredicateBuilderUnitTests {
+
+ @Mock CriteriaBuilder cb;
+ @Mock Root root;
+ @Mock EntityType personEntityType;
+ @Mock Expression expressionMock;
+ @Mock Predicate truePredicate;
+ @Mock Predicate dummyPredicate;
+ @Mock Predicate listPredicate;
+ @Mock Path dummyPath;
+
+ Set> personEntityAttribtues;
+
+ SingularAttribute super Person, Long> personIdAttribute;
+ SingularAttribute super Person, String> personFirstnameAttribute;
+ SingularAttribute super Person, Long> personAgeAttribute;
+ SingularAttribute super Person, Person> personFatherAttribute;
+ SingularAttribute super Person, Skill> personSkillAttribute;
+ SingularAttribute super Person, Address> personAddressAttribute;
+
+ @Before
+ public void setUp() {
+
+ personIdAttribute = new SingluarAttributeStub("id", PersistentAttributeType.BASIC, Long.class);
+ personFirstnameAttribute = new SingluarAttributeStub("firstname", PersistentAttributeType.BASIC,
+ String.class);
+ personAgeAttribute = new SingluarAttributeStub("age", PersistentAttributeType.BASIC, Long.class);
+ personFatherAttribute = new SingluarAttributeStub("father", PersistentAttributeType.MANY_TO_ONE,
+ Person.class);
+ personSkillAttribute = new SingluarAttributeStub("skill", PersistentAttributeType.MANY_TO_ONE,
+ Skill.class);
+ personAddressAttribute = new SingluarAttributeStub("address", PersistentAttributeType.EMBEDDED,
+ Address.class);
+
+ personEntityAttribtues = new LinkedHashSet>();
+ personEntityAttribtues.add(personIdAttribute);
+ personEntityAttribtues.add(personFirstnameAttribute);
+ personEntityAttribtues.add(personAgeAttribute);
+ personEntityAttribtues.add(personFatherAttribute);
+ personEntityAttribtues.add(personAddressAttribute);
+ personEntityAttribtues.add(personSkillAttribute);
+
+ when(root.get(any(SingularAttribute.class))).thenReturn(dummyPath);
+ when(root.getModel()).thenReturn(personEntityType);
+ when(personEntityType.getSingularAttributes()).thenReturn(personEntityAttribtues);
+
+ when(cb.equal(any(Expression.class), any(String.class))).thenReturn(dummyPredicate);
+ when(cb.equal(any(Expression.class), any(Long.class))).thenReturn(dummyPredicate);
+ when(cb.like(any(Expression.class), any(String.class))).thenReturn(dummyPredicate);
+
+ when(cb.literal(any(Boolean.class))).thenReturn(expressionMock);
+ when(cb.isTrue(eq(expressionMock))).thenReturn(truePredicate);
+ when(cb.and(Matchers. anyVararg())).thenReturn(listPredicate);
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void getPredicateShouldThrowExceptionOnNullRoot() {
+ QueryByExamplePredicateBuilder.getPredicate(null, cb, of(new Person()));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() {
+ QueryByExamplePredicateBuilder.getPredicate(root, null, of(new Person()));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void getPredicateShouldThrowExceptionOnNullExample() {
+ QueryByExamplePredicateBuilder.getPredicate(root, null, null);
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void emptyCriteriaListShouldResultTruePredicate() {
+ assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(truePredicate));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void singleElementCriteriaShouldJustReturnIt() {
+
+ Person p = new Person();
+ p.firstname = "foo";
+
+ assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)), equalTo(dummyPredicate));
+ verify(cb, times(1)).equal(any(Expression.class), eq("foo"));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void multiPredicateCriteriaShouldReturnCombinedOnes() {
+
+ Person p = new Person();
+ p.firstname = "foo";
+ p.age = 2L;
+
+ assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)), equalTo(listPredicate));
+
+ verify(cb, times(1)).equal(any(Expression.class), eq("foo"));
+ verify(cb, times(1)).equal(any(Expression.class), eq(2L));
+ }
+
+ static class Person {
+
+ @Id Long id;
+ String firstname;
+ Long age;
+
+ Person father;
+ Address address;
+ Skill skill;
+ }
+
+ static class Address {
+
+ String city;
+ String country;
+ }
+
+ static class Skill {
+
+ @Id Long id;
+ String name;
+ }
+
+ static class SingluarAttributeStub implements SingularAttribute {
+
+ private String name;
+ private PersistentAttributeType attributeType;
+ private Class type;
+
+ public SingluarAttributeStub(String name,
+ javax.persistence.metamodel.Attribute.PersistentAttributeType attributeType, Class type) {
+ this.name = name;
+ this.attributeType = attributeType;
+ this.type = type;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public javax.persistence.metamodel.Attribute.PersistentAttributeType getPersistentAttributeType() {
+ return attributeType;
+ }
+
+ @Override
+ public ManagedType getDeclaringType() {
+ return null;
+ }
+
+ @Override
+ public Class getJavaType() {
+ return type;
+ }
+
+ @Override
+ public Member getJavaMember() {
+ return null;
+ }
+
+ @Override
+ public boolean isAssociation() {
+ return !attributeType.equals(PersistentAttributeType.BASIC)
+ && !attributeType.equals(PersistentAttributeType.EMBEDDED);
+ }
+
+ @Override
+ public boolean isCollection() {
+ return false;
+ }
+
+ @Override
+ public javax.persistence.metamodel.Bindable.BindableType getBindableType() {
+ return BindableType.SINGULAR_ATTRIBUTE;
+ }
+
+ @Override
+ public Class getBindableJavaType() {
+ return type;
+ }
+
+ @Override
+ public boolean isId() {
+ return ObjectUtils.nullSafeEquals(name, "id");
+ }
+
+ @Override
+ public boolean isVersion() {
+ return false;
+ }
+
+ @Override
+ public boolean isOptional() {
+ return false;
+ }
+
+ @Override
+ public Type getType() {
+ return null;
+ }
+
+ }
+}
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 d1d88f9139..a1968272a7 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
@@ -381,6 +381,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..691ab45e13 100644
--- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
+++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2014 the original author or authors.
+ * Copyright 2008-2016 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.
@@ -16,9 +16,12 @@
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.Example.*;
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;
@@ -40,6 +43,7 @@
import javax.persistence.criteria.Root;
import org.hamcrest.Matchers;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -47,6 +51,11 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleSpec;
+import org.springframework.data.domain.ExampleSpec.GenericPropertyMatcher;
+import org.springframework.data.domain.ExampleSpec.StringMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
@@ -60,6 +69,7 @@
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.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension.SampleSecurityContextHolder;
import org.springframework.data.jpa.repository.sample.UserRepository;
import org.springframework.test.context.ContextConfiguration;
@@ -77,6 +87,7 @@
* @author Oliver Gierke
* @author Kevin Raymond
* @author Thomas Darimont
+ * @author Mark Paluch
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
@@ -586,7 +597,7 @@ public void returnsSamePageIfNoSpecGiven() throws Exception {
Pageable pageable = new PageRequest(0, 1);
flushTestUsers();
- assertThat(repository.findAll(null, pageable), is(repository.findAll(pageable)));
+ assertThat(repository.findAll((Specification) null, pageable), is(repository.findAll(pageable)));
}
@Test
@@ -1271,6 +1282,68 @@ public void saveAndFlushShouldSupportReturningSubTypesOfRepositoryEntity() {
assertThat(user.getEmailAddress(), is(savedUser.getEmailAddress()));
}
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByUntypedExampleShouldReturnSubTypesOfRepositoryEntity() {
+
+ flushTestUsers();
+
+ SpecialUser user = new SpecialUser();
+ user.setFirstname("Thomas");
+ user.setEmailAddress("thomas@example.org");
+
+ repository.saveAndFlush(user);
+
+ List result = repository
+ .findAll(Example.of(new User(), ExampleSpec.untyped().withIgnorePaths("age", "createdAt", "dateOfBirth")));
+
+ assertThat(result, hasSize(5));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByTypedUserExampleShouldReturnSubTypesOfRepositoryEntity() {
+
+ flushTestUsers();
+
+ SpecialUser user = new SpecialUser();
+ user.setFirstname("Thomas");
+ user.setEmailAddress("thomas@example.org");
+
+ repository.saveAndFlush(user);
+
+ Example example = Example.of(new User(),
+ ExampleSpec.typed(User.class).withIgnorePaths("age", "createdAt", "dateOfBirth"));
+ List result = repository.findAll(example);
+
+ assertThat(result, hasSize(5));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByTypedSpecialUserExampleShouldReturnSubTypesOfRepositoryEntity() {
+
+ flushTestUsers();
+
+ SpecialUser user = new SpecialUser();
+ user.setFirstname("Thomas");
+ user.setEmailAddress("thomas@example.org");
+
+ repository.saveAndFlush(user);
+
+ Example example = Example.of(new User(),
+ ExampleSpec.typed(SpecialUser.class).withIgnorePaths("age", "createdAt", "dateOfBirth"));
+ List result = repository.findAll(example);
+
+ assertThat(result, hasSize(1));
+ }
+
/**
* @see DATAJPA-491
*/
@@ -1905,6 +1978,411 @@ public void accept(User user) {
assertThat(users, hasSize(2));
}
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExample() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+ prototype.setCreatedAt(null);
+
+ List users = repository.findAll(of(prototype));
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithEmptyProbe() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setCreatedAt(null);
+
+ List users = repository
+ .findAll(of(prototype, ExampleSpec.untyped().withIgnorePaths("age", "createdAt", "active")));
+
+ assertThat(users, hasSize(4));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = InvalidDataAccessApiUsageException.class)
+ public void findAllByNullExample() {
+ repository.findAll((Example) null);
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithExcludedAttributes() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt"));
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithAssociation() {
+
+ flushTestUsers();
+
+ firstUser.setManager(secondUser);
+ thirdUser.setManager(firstUser);
+ repository.save(Arrays.asList(firstUser, thirdUser));
+
+ User manager = new User();
+ manager.setLastname("Arrasz");
+ manager.setAge(secondUser.getAge());
+ manager.setCreatedAt(null);
+
+ User prototype = new User();
+ prototype.setCreatedAt(null);
+ prototype.setManager(manager);
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("age"));
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithEmbedded() {
+
+ flushTestUsers();
+
+ firstUser.setAddress(new Address("germany", "dresden", "", ""));
+ repository.save(firstUser);
+
+ User prototype = new User();
+ prototype.setCreatedAt(null);
+ prototype.setAddress(new Address("germany", null, null, null));
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("age"));
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithStartingStringMatcher() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("Ol");
+
+ Example example = Example.of(prototype,
+ ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.STARTING).withIgnorePaths("age", "createdAt"));
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithEndingStringMatcher() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("ver");
+
+ Example example = Example.of(prototype,
+ ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.ENDING).withIgnorePaths("age", "createdAt"));
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = InvalidDataAccessApiUsageException.class)
+ public void findAllByExampleWithRegexStringMatcher() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("^Oliver$");
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.REGEX));
+ repository.findAll(example);
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithIgnoreCase() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("oLiVer");
+
+ Example example = Example.of(prototype,
+ ExampleSpec.typed(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt"));
+
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithStringMatcherAndIgnoreCase() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("oLiV");
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class)
+ .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().withIgnorePaths("age", "createdAt"));
+
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithIncludeNull() {
+
+ // something is wrong with OpenJPA - I do not know what
+ Assume.assumeThat(PersistenceProvider.fromEntityManager(em), not(equalTo(PersistenceProvider.OPEN_JPA)));
+
+ flushTestUsers();
+
+ firstUser.setAddress(new Address("andor", "caemlyn", "", ""));
+
+ User fifthUser = new User();
+ fifthUser.setEmailAddress("foo@bar.com");
+ fifthUser.setActive(firstUser.isActive());
+ fifthUser.setAge(firstUser.getAge());
+ fifthUser.setFirstname(firstUser.getFirstname());
+ fifthUser.setLastname(firstUser.getLastname());
+
+ repository.save(Arrays.asList(firstUser, fifthUser));
+
+ User prototype = new User();
+ prototype.setFirstname(firstUser.getFirstname());
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIncludeNullValues()
+ .withIgnorePaths("id", "binaryData", "lastname", "emailAddress", "age", "createdAt"));
+
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(fifthUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithPropertySpecifier() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setFirstname("oLi");
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase()
+ .withIgnorePaths("age", "createdAt").withMatcher("firstname", new GenericPropertyMatcher().startsWith()));
+
+ List users = repository.findAll(example);
+
+ assertThat(users, hasSize(1));
+ assertThat(users.get(0), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithSort() {
+
+ flushTestUsers();
+
+ User user1 = new User("Oliver", "Srping", "o@s.de");
+ user1.setAge(30);
+
+ repository.save(user1);
+
+ User prototype = new User();
+ prototype.setFirstname("oLi");
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase()
+ .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase());
+
+ List users = repository.findAll(example, new Sort(DESC, "age"));
+
+ assertThat(users, hasSize(2));
+ assertThat(users.get(0), is(user1));
+ assertThat(users.get(1), is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findAllByExampleWithPageable() {
+
+ flushTestUsers();
+
+ for (int i = 0; i < 99; i++) {
+ User user1 = new User("Oliver-" + i, "Srping", "o" + i + "@s.de");
+ user1.setAge(30 + i);
+
+ repository.save(user1);
+ }
+
+ User prototype = new User();
+ prototype.setFirstname("oLi");
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase()
+ .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase());
+
+ Page users = repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age")));
+
+ assertThat(users.getSize(), is(10));
+ assertThat(users.hasNext(), is(true));
+ assertThat(users.getTotalElements(), is(100L));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = InvalidDataAccessApiUsageException.class)
+ public void findAllByExampleShouldNotAllowCycles() {
+
+ flushTestUsers();
+
+ User user1 = new User();
+ user1.setFirstname("user1");
+
+ user1.setManager(user1);
+
+ Example example = Example.of(user1, ExampleSpec.typed(User.class).withIgnoreCase()
+ .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase());
+
+ repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age")));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test(expected = InvalidDataAccessApiUsageException.class)
+ public void findAllByExampleShouldNotAllowCyclesOverSeveralInstances() {
+
+ flushTestUsers();
+
+ User user1 = new User();
+ user1.setFirstname("user1");
+
+ User user2 = new User();
+ user2.setFirstname("user2");
+
+ user1.setManager(user2);
+ user2.setManager(user1);
+
+ Example example = Example.of(user1, ExampleSpec.typed(User.class).withIgnoreCase()
+ .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase());
+
+ repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age")));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void findOneByExampleWithExcludedAttributes() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt"));
+ User users = repository.findOne(example);
+
+ assertThat(users, is(firstUser));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void countByExampleWithExcludedAttributes() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt"));
+ long count = repository.count(example);
+
+ assertThat(count, is(1L));
+ }
+
+ /**
+ * @see DATAJPA-218
+ */
+ @Test
+ public void existsByExampleWithExcludedAttributes() {
+
+ flushTestUsers();
+
+ User prototype = new User();
+ prototype.setAge(28);
+
+ Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt"));
+ boolean exists = repository.exists(example);
+
+ assertThat(exists, is(true));
+ }
+
private Page executeSpecWithSort(Sort sort) {
flushTestUsers();