diff --git a/.gitignore b/.gitignore index 5cce85cc3..6d0b68c76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ target/ +.idea/ .settings/ +*.iml .project .classpath .springBeans diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index e5ca7b6df..6e02d38ff 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 6b294d1ec..13315ba54 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 000000000..92654f16e --- /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 index 2e205e337..72cee28f2 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -34,7 +34,8 @@ import javax.persistence.metamodel.SingularAttribute; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.NullHandler; +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; @@ -48,8 +49,9 @@ import org.springframework.util.StringUtils; * 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 { @@ -63,7 +65,7 @@ public class QueryByExamplePredicateBuilder { /** * 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}. @@ -73,13 +75,14 @@ public class QueryByExamplePredicateBuilder { Assert.notNull(root, "Root must not be null!"); Assert.notNull(cb, "CriteriaBuilder must not be null!"); - Assert.notNull(example, "Root must not be null!"); + Assert.notNull(example, "Example must not be null!"); - List predicates = getPredicates("", cb, root, root.getModel(), example.getSampleObject(), example, - new PathNode("root", null, example.getSampleObject())); + 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(false)); + return cb.isTrue(cb.literal(true)); } if (predicates.size() == 1) { @@ -90,8 +93,8 @@ public class QueryByExamplePredicateBuilder { } @SuppressWarnings({ "rawtypes", "unchecked" }) - static List getPredicates(String path, CriteriaBuilder cb, Path from, ManagedType type, - Object value, Example example, PathNode currentNode) { + 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); @@ -100,16 +103,16 @@ public class QueryByExamplePredicateBuilder { String currentPath = !StringUtils.hasText(path) ? attribute.getName() : path + "." + attribute.getName(); - if (example.isIgnoredPath(currentPath)) { + if (exampleAccessor.isIgnoredPath(currentPath)) { continue; } - Object attributeValue = example.getValueTransformerForPath(currentPath).convert( - beanWrapper.getPropertyValue(attribute.getName())); + Object attributeValue = exampleAccessor.getValueTransformerForPath(currentPath) + .convert(beanWrapper.getPropertyValue(attribute.getName())); if (attributeValue == null) { - if (example.getNullHandler().equals(NullHandler.INCLUDE)) { + if (exampleAccessor.getNullHandler().equals(ExampleSpec.NullHandler.INCLUDE)) { predicates.add(cb.isNull(from.get(attribute))); } continue; @@ -118,26 +121,26 @@ public class QueryByExamplePredicateBuilder { if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) { predicates.addAll(getPredicates(currentPath, cb, from.get(attribute.getName()), - (ManagedType) attribute.getType(), attributeValue, example, currentNode)); + (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))); + 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(example.getSampleType()), node)); + 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, example, node)); + (ManagedType) attribute.getType(), attributeValue, probeType, exampleAccessor, node)); continue; } @@ -145,12 +148,12 @@ public class QueryByExamplePredicateBuilder { if (attribute.getJavaType().equals(String.class)) { Expression expression = from.get(attribute); - if (example.isIgnoreCaseForPath(currentPath)) { + if (exampleAccessor.isIgnoreCaseForPath(currentPath)) { expression = cb.lower(expression); attributeValue = attributeValue.toString().toLowerCase(); } - switch (example.getStringMatcherForPath(currentPath)) { + switch (exampleAccessor.getStringMatcherForPath(currentPath)) { case DEFAULT: case EXACT: @@ -166,8 +169,8 @@ public class QueryByExamplePredicateBuilder { predicates.add(cb.like(expression, "%" + attributeValue)); break; default: - throw new IllegalArgumentException("Unsupported StringMatcher " - + example.getStringMatcherForPath(currentPath)); + throw new IllegalArgumentException( + "Unsupported StringMatcher " + exampleAccessor.getStringMatcherForPath(currentPath)); } } else { predicates.add(cb.equal(from.get(attribute), attributeValue)); @@ -184,7 +187,7 @@ public class QueryByExamplePredicateBuilder { /** * {@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 { diff --git a/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 901dd92d6..35c28e8ec 100644 --- a/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -52,7 +52,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * @author Oliver Gierke * @author Thomas Darimont */ -public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { +public enum PersistenceProvider implements QueryExtractor,ProxyIdAccessor { /** * Hibernate persistence provider. @@ -117,14 +117,13 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new HibernateScrollableResultsIterator(jpaQuery); } - }, /** * EclipseLink persistence provider. */ - ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), Collections - .singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) { + ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE), + Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) { public String extractQueryString(Query query) { return ((JpaQuery) query).getDatabaseQuery().getJPQLString(); @@ -164,7 +163,6 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new EclipseLinkScrollableResultsIterator(jpaQuery); } - }, /** @@ -203,7 +201,6 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new OpenJpaResultStreamingIterator(jpaQuery); } - }, /** @@ -246,7 +243,6 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { public Object getIdentifierFrom(Object entity) { return null; } - }; /** @@ -388,8 +384,8 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { } public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { - throw new UnsupportedOperationException("Streaming results is not implement for this PersistenceProvider: " - + name()); + throw new UnsupportedOperationException( + "Streaming results is not implement for this PersistenceProvider: " + name()); } /** 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 885afd867..299616d00 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -21,20 +21,21 @@ import java.util.List; import javax.persistence.EntityManager; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; 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) @@ -95,33 +96,16 @@ public interface JpaRepository extends PagingAndSort */ T getOne(ID id); - /** - * Returns all instances of the type specified by the given {@link Example}. - * - * @param example must not be {@literal null}. - * @return - * @since 1.10 + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ - List findAllByExample(Example example); + @Override + List findAll(Example example); - /** - * Returns all instances of the type specified by the given {@link Example}. - * - * @param example must not be {@literal null}. - * @param sort can be {@literal null}. - * @return all entities sorted by the given options - * @since 1.10 + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ - List findAllByExample(Example example, Sort sort); + @Override + List findAll(Example example, Sort sort); - /** - * Returns a {@link Page} of entities meeting the paging restriction specified by the given {@link Example} limited to - * criteria provided in the {@code Pageable} object. - * - * @param example must not be {@literal null}. - * @param pageable can be {@literal null}. - * @return a {@link Page} of entities - * @since 1.10 - */ - Page findAllByExample(Example example, Pageable pageable); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index d81cf30e2..b8ec62d3f 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -189,8 +189,8 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { Assert.notNull(query, "Query must not be null!"); Assert.notNull(method, "JpaQueryMethod must not be null!"); - Map hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(), getQueryMethod() - .getEntityInformation().getJavaType()); + Map hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(), + getQueryMethod().getEntityInformation().getJavaType()); for (Map.Entry hint : hints.entrySet()) { query.setHint(hint.getKey(), hint.getValue()); 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 6d4b2f8e6..e33b3d11c 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. @@ -44,6 +44,7 @@ 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; @@ -64,13 +65,14 @@ import org.springframework.util.Assert; * @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!"; @@ -370,7 +372,7 @@ public class SimpleJpaRepository implements JpaRepos return new PageImpl(findAll()); } - return findAll(null, pageable); + return findAll((Specification) null, pageable); } /* @@ -414,29 +416,65 @@ public class SimpleJpaRepository implements JpaRepos } /* (non-Javadoc) - * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example) + * @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 List findAllByExample(Example example) { - return findAll(new ExampleSpecification(example)); + public boolean exists(Example example) { + return !getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList() + .isEmpty(); } /* * (non-Javadoc) - * @see org.springframework.data.jpa.repository.JpaRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ @Override - public List findAllByExample(Example example, Sort sort) { - return findAll(new ExampleSpecification(example), sort); + public List findAll(Example example) { + return getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList(); } /* * (non-Javadoc) - * @see org.springframework.data.jpa.repository.JpaRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ @Override - public Page findAllByExample(Example example, Pageable pageable) { - return findAll(new ExampleSpecification(example), pageable); + 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); } /* @@ -517,26 +555,41 @@ public class SimpleJpaRepository implements JpaRepos /** * 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 @@ -544,7 +597,21 @@ public class SimpleJpaRepository implements JpaRepos 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); } /** @@ -555,11 +622,23 @@ public class SimpleJpaRepository implements JpaRepos * @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) { @@ -576,11 +655,22 @@ public class SimpleJpaRepository implements JpaRepos * @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)); @@ -599,9 +689,24 @@ public class SimpleJpaRepository implements JpaRepos * @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; @@ -617,14 +722,14 @@ public class SimpleJpaRepository implements JpaRepos 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); @@ -638,6 +743,15 @@ public class SimpleJpaRepository implements JpaRepos } } + + 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. * @@ -692,7 +806,7 @@ public class SimpleJpaRepository implements JpaRepos /** * {@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 @@ -703,7 +817,7 @@ public class SimpleJpaRepository implements JpaRepos /** * Creates new {@link ExampleSpecification}. - * + * * @param example */ public ExampleSpecification(Example 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 index d97eb7a9c..3ddf152da 100644 --- a/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java @@ -47,6 +47,7 @@ import org.springframework.util.ObjectUtils; /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class QueryByExamplePredicateBuilderUnitTests { @@ -55,7 +56,7 @@ public class QueryByExamplePredicateBuilderUnitTests { @Mock Root root; @Mock EntityType personEntityType; @Mock Expression expressionMock; - @Mock Predicate falsePredicate; + @Mock Predicate truePredicate; @Mock Predicate dummyPredicate; @Mock Predicate listPredicate; @Mock Path dummyPath; @@ -100,7 +101,7 @@ public class QueryByExamplePredicateBuilderUnitTests { 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(falsePredicate); + when(cb.isTrue(eq(expressionMock))).thenReturn(truePredicate); when(cb.and(Matchers. anyVararg())).thenReturn(listPredicate); } @@ -109,7 +110,7 @@ public class QueryByExamplePredicateBuilderUnitTests { */ @Test(expected = IllegalArgumentException.class) public void getPredicateShouldThrowExceptionOnNullRoot() { - QueryByExamplePredicateBuilder.getPredicate(null, cb, exampleOf(new Person())); + QueryByExamplePredicateBuilder.getPredicate(null, cb, of(new Person())); } /** @@ -117,7 +118,7 @@ public class QueryByExamplePredicateBuilderUnitTests { */ @Test(expected = IllegalArgumentException.class) public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() { - QueryByExamplePredicateBuilder.getPredicate(root, null, exampleOf(new Person())); + QueryByExamplePredicateBuilder.getPredicate(root, null, of(new Person())); } /** @@ -132,8 +133,8 @@ public class QueryByExamplePredicateBuilderUnitTests { * @see DATAJPA-218 */ @Test - public void emptyCriteriaListShouldResultFalsePredicate() { - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(new Person())), equalTo(falsePredicate)); + public void emptyCriteriaListShouldResultTruePredicate() { + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(truePredicate)); } /** @@ -145,7 +146,7 @@ public class QueryByExamplePredicateBuilderUnitTests { Person p = new Person(); p.firstname = "foo"; - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(p)), equalTo(dummyPredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)), equalTo(dummyPredicate)); verify(cb, times(1)).equal(any(Expression.class), eq("foo")); } @@ -159,7 +160,7 @@ public class QueryByExamplePredicateBuilderUnitTests { p.firstname = "foo"; p.age = 2L; - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(p)), equalTo(listPredicate)); + 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)); diff --git a/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java index 543ebbc20..c72792c09 100644 --- a/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java @@ -30,6 +30,8 @@ import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.ImportResource; import org.springframework.data.jpa.domain.sample.Category; import org.springframework.data.jpa.domain.sample.Product; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.provider.ProxyIdAccessor; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.sample.CategoryRepository; import org.springframework.data.jpa.repository.sample.ProductRepository; 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 a92559d5c..691ab45e1 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,10 +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; @@ -51,12 +53,13 @@ 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.Example.StringMatcher; +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; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.PropertySpecifier; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -84,6 +87,7 @@ import com.google.common.base.Optional; * @author Oliver Gierke * @author Kevin Raymond * @author Thomas Darimont + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:application-context.xml") @@ -593,7 +597,7 @@ public class UserRepositoryTests { 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 @@ -1278,6 +1282,68 @@ public class UserRepositoryTests { 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 */ @@ -1924,18 +1990,35 @@ public class UserRepositoryTests { prototype.setAge(28); prototype.setCreatedAt(null); - List users = repository.findAllByExample(exampleOf(prototype)); + 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.findAllByExample(null); + repository.findAll((Example) null); } /** @@ -1949,7 +2032,8 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setAge(28); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("createdAt").get()); + 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)); @@ -1976,7 +2060,8 @@ public class UserRepositoryTests { prototype.setCreatedAt(null); prototype.setManager(manager); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + 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)); @@ -1997,7 +2082,8 @@ public class UserRepositoryTests { prototype.setCreatedAt(null); prototype.setAddress(new Address("germany", null, null, null)); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + 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)); @@ -2014,9 +2100,9 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("Ol"); - Example example = newExampleOf(prototype).matchStringsStartingWith().ignore("age", "createdAt").get(); - - List users = repository.findAllByExample(example); + 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)); @@ -2033,9 +2119,9 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("ver"); - Example example = newExampleOf(prototype).matchStringsEndingWith().ignore("age", "createdAt").get(); - - List users = repository.findAllByExample(example); + 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)); @@ -2052,10 +2138,8 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("^Oliver$"); - Example example = newExampleOf(prototype).withStringMatcher(StringMatcher.REGEX).ignore("age", "createdAt") - .get(); - - repository.findAllByExample(example); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.REGEX)); + repository.findAll(example); } /** @@ -2069,9 +2153,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("oLiVer"); - Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt").get(); + Example example = Example.of(prototype, + ExampleSpec.typed(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt")); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2088,10 +2173,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("oLiV"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = Example.of(prototype, ExampleSpec.typed(User.class) + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().withIgnorePaths("age", "createdAt")); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2122,10 +2207,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname(firstUser.getFirstname()); - Example example = newExampleOf(prototype).includeNullValues() - .ignore("id", "binaryData", "lastname", "emailAddress", "age", "createdAt").get(); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIncludeNullValues() + .withIgnorePaths("id", "binaryData", "lastname", "emailAddress", "age", "createdAt")); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(fifthUser)); @@ -2142,11 +2227,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt") - .withPropertySpecifier(PropertySpecifier.newPropertySpecifier("firstname").matchStringStartingWith().get()) - .get(); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withMatcher("firstname", new GenericPropertyMatcher().startsWith())); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2168,10 +2252,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase()); - List users = repository.findAllByExample(example, new Sort(DESC, "age")); + List users = repository.findAll(example, new Sort(DESC, "age")); assertThat(users, hasSize(2)); assertThat(users.get(0), is(user1)); @@ -2196,10 +2280,10 @@ public class UserRepositoryTests { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase()); - Page users = repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + Page users = repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age"))); assertThat(users.getSize(), is(10)); assertThat(users.hasNext(), is(true)); @@ -2219,10 +2303,10 @@ public class UserRepositoryTests { user1.setManager(user1); - Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = Example.of(user1, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase()); - repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age"))); } /** @@ -2242,10 +2326,61 @@ public class UserRepositoryTests { user1.setManager(user2); user2.setManager(user1); - Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = Example.of(user1, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withStringMatcher(StringMatcher.STARTING).withIgnoreCase()); - repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + 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) { @@ -2258,5 +2393,4 @@ public class UserRepositoryTests { assertThat(result.getTotalElements(), is(2L)); return result; } - }