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 000000000..2e205e337 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -0,0 +1,248 @@ +/* + * 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.Example.NullHandler; +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 + * @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, "Root must not be null!"); + + List predicates = getPredicates("", cb, root, root.getModel(), example.getSampleObject(), example, + new PathNode("root", null, example.getSampleObject())); + + if (predicates.isEmpty()) { + return cb.isTrue(cb.literal(false)); + } + + 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, Example example, 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 (example.isIgnoredPath(currentPath)) { + continue; + } + + Object attributeValue = example.getValueTransformerForPath(currentPath).convert( + beanWrapper.getPropertyValue(attribute.getName())); + + if (attributeValue == null) { + + if (example.getNullHandler().equals(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, example, 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(example.getSampleType()), node)); + } + + predicates.addAll(getPredicates(currentPath, cb, ((From) from).join(attribute.getName()), + (ManagedType) attribute.getType(), attributeValue, example, node)); + + continue; + } + + if (attribute.getJavaType().equals(String.class)) { + + Expression expression = from.get(attribute); + if (example.isIgnoreCaseForPath(currentPath)) { + expression = cb.lower(expression); + attributeValue = attributeValue.toString().toLowerCase(); + } + + switch (example.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 " + + example.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/domain/Example.java b/src/main/java/org/springframework/data/jpa/domain/Example.java deleted file mode 100644 index e3cc4d507..000000000 --- a/src/main/java/org/springframework/data/jpa/domain/Example.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jpa.domain; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import org.springframework.util.Assert; - -/** - * A wrapper around a prototype object that can be used in Query by Example queries - * - * @author Thomas Darimont - * @param - */ -public class Example { - - private final T prototype; - private final Set ignoredAttributes; - - /** - * Creates a new {@link Example} with the given {@code prototype}. - * - * @param prototype must not be {@literal null} - */ - public Example(T prototype) { - this(prototype, Collections. emptySet()); - } - - /** - * Creates a new {@link Example} with the given {@code prototype} ignoring the given attributes. - * - * @param prototype prototype must not be {@literal null} - * @param attributeNames prototype must not be {@literal null} - */ - public Example(T prototype, Set attributeNames) { - - Assert.notNull(prototype, "Prototype must not be null!"); - Assert.notNull(attributeNames, "attributeNames must not be null!"); - - this.prototype = prototype; - this.ignoredAttributes = attributeNames; - } - - public T getPrototype() { - return prototype; - } - - public Set getIgnoredAttributes() { - return Collections.unmodifiableSet(ignoredAttributes); - } - - public boolean isAttributeIgnored(String attributePath) { - return ignoredAttributes.contains(attributePath); - } - - public static Example exampleOf(T prototype) { - return new Example(prototype); - } - - public static Builder newExample(T prototype) { - return new Builder(prototype); - } - - /** - * A {@link Builder} for {@link Example}s. - * - * @author Thomas Darimont - * @param - */ - public static class Builder { - - private final T prototype; - private Set ignoredAttributeNames; - - /** - * @param prototype - */ - public Builder(T prototype) { - - Assert.notNull(prototype, "Prototype must not be null!"); - - this.prototype = prototype; - } - - /** - * Allows to specify attribute names that should be ignored. - * - * @param attributeNames - * @return - */ - public Builder ignoring(String... attributeNames) { - - Assert.notNull(attributeNames, "attributeNames must not be null!"); - - return ignoring(Arrays.asList(attributeNames)); - } - - /** - * Allows to specify attribute names that should be ignored. - * - * @param attributeNames - * @return - */ - public Builder ignoring(Collection attributeNames) { - - Assert.notNull(attributeNames, "attributeNames must not be null!"); - - this.ignoredAttributeNames = new HashSet(attributeNames); - return this; - } - - /** - * Constructs the actual {@link Example} instance. - * - * @return - */ - public Example build() { - return new Example(prototype, ignoredAttributeNames); - } - } -} diff --git a/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index 35c28e8ec..901dd92d6 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,13 +117,14 @@ 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(); @@ -163,6 +164,7 @@ public enum PersistenceProvider implements QueryExtractor,ProxyIdAccessor { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new EclipseLinkScrollableResultsIterator(jpaQuery); } + }, /** @@ -201,6 +203,7 @@ public enum PersistenceProvider implements QueryExtractor,ProxyIdAccessor { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new OpenJpaResultStreamingIterator(jpaQuery); } + }, /** @@ -243,6 +246,7 @@ public enum PersistenceProvider implements QueryExtractor,ProxyIdAccessor { public Object getIdentifierFrom(Object entity) { return null; } + }; /** @@ -384,8 +388,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 9132b8a45..885afd867 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,8 +20,10 @@ 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.jpa.domain.Example; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @@ -29,6 +31,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; * JPA specific extension of {@link org.springframework.data.repository.Repository}. * * @author Oliver Gierke + * @author Christoph Strobl */ @NoRepositoryBean public interface JpaRepository extends PagingAndSortingRepository { @@ -91,15 +94,34 @@ public interface JpaRepository extends PagingAndSort * @see EntityManager#getReference(Class, Object) */ T getOne(ID id); - + /** * Returns all instances of the type specified by the given {@link Example}. * - * This method is deliberately not named {@code findByExample} to not interfere - * with existing repository methods that rely on query derivation. - * * @param example must not be {@literal null}. * @return + * @since 1.10 */ - List findWithExample(Example example); + List findAllByExample(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 + */ + List findAllByExample(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 b8ec62d3f..d81cf30e2 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 0191dfb7a..6d4b2f8e6 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -19,7 +19,6 @@ import static org.springframework.data.jpa.repository.query.QueryUtils.*; import java.io.Serializable; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -38,16 +37,14 @@ import javax.persistence.criteria.ParameterExpression; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import javax.persistence.metamodel.Attribute; -import org.springframework.beans.BeanWrapper; -import org.springframework.beans.BeanWrapperImpl; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.domain.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.jpa.domain.Example; +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; @@ -59,7 +56,6 @@ import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer @@ -420,33 +416,27 @@ public class SimpleJpaRepository implements JpaRepos /* (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example) */ - public List findWithExample(Example example) { + @Override + public List findAllByExample(Example example) { + return findAll(new ExampleSpecification(example)); + } - Assert.notNull(example, "Example must not be null!"); + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) + */ + @Override + public List findAllByExample(Example example, Sort sort) { + return findAll(new ExampleSpecification(example), sort); + } - CriteriaBuilder builder = em.getCriteriaBuilder(); - CriteriaQuery query = builder.createQuery(getDomainClass()); - Root root = query.from(getDomainClass()); - - BeanWrapper bean = new BeanWrapperImpl(example.getPrototype()); - - List predicates = new ArrayList(); - for (Attribute attribute : em.getMetamodel().managedType(getDomainClass()).getAttributes()) { - - Object value = bean.getPropertyValue(attribute.getName()); - - // TODO support different matching modes configured on the provided Example - if (value == null // - || (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) - || (value instanceof Map && CollectionUtils.isEmpty((Map) value)) - || example.isAttributeIgnored(attribute.getName())) { - continue; - } - - predicates.add(builder.equal(root.get(attribute.getName()), value)); - } - - return em.createQuery(query.where(predicates.toArray(new Predicate[predicates.size()]))).getResultList(); + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) + */ + @Override + public Page findAllByExample(Example example, Pageable pageable) { + return findAll(new ExampleSpecification(example), pageable); } /* @@ -698,4 +688,37 @@ public class SimpleJpaRepository implements JpaRepos 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 000000000..d97eb7a9c --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java @@ -0,0 +1,271 @@ +/* + * 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 + */ +@RunWith(MockitoJUnitRunner.class) +public class QueryByExamplePredicateBuilderUnitTests { + + @Mock CriteriaBuilder cb; + @Mock Root root; + @Mock EntityType personEntityType; + @Mock Expression expressionMock; + @Mock Predicate falsePredicate; + @Mock Predicate dummyPredicate; + @Mock Predicate listPredicate; + @Mock Path dummyPath; + + Set> personEntityAttribtues; + + SingularAttribute personIdAttribute; + SingularAttribute personFirstnameAttribute; + SingularAttribute personAgeAttribute; + SingularAttribute personFatherAttribute; + SingularAttribute personSkillAttribute; + SingularAttribute 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(falsePredicate); + when(cb.and(Matchers. anyVararg())).thenReturn(listPredicate); + } + + /** + * @see DATAJPA-218 + */ + @Test(expected = IllegalArgumentException.class) + public void getPredicateShouldThrowExceptionOnNullRoot() { + QueryByExamplePredicateBuilder.getPredicate(null, cb, exampleOf(new Person())); + } + + /** + * @see DATAJPA-218 + */ + @Test(expected = IllegalArgumentException.class) + public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() { + QueryByExamplePredicateBuilder.getPredicate(root, null, exampleOf(new Person())); + } + + /** + * @see DATAJPA-218 + */ + @Test(expected = IllegalArgumentException.class) + public void getPredicateShouldThrowExceptionOnNullExample() { + QueryByExamplePredicateBuilder.getPredicate(root, null, null); + } + + /** + * @see DATAJPA-218 + */ + @Test + public void emptyCriteriaListShouldResultFalsePredicate() { + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(new Person())), equalTo(falsePredicate)); + } + + /** + * @see DATAJPA-218 + */ + @Test + public void singleElementCriteriaShouldJustReturnIt() { + + Person p = new Person(); + p.firstname = "foo"; + + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(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, exampleOf(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/provider/PersistenceProviderIntegrationTests.java b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java index c72792c09..543ebbc20 100644 --- a/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderIntegrationTests.java @@ -30,8 +30,6 @@ 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 7d99804e9..a92559d5c 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -16,11 +16,10 @@ 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; @@ -42,6 +41,7 @@ import javax.persistence.criteria.Predicate; 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; @@ -49,20 +49,24 @@ import org.junit.runner.RunWith; 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.Example.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; import org.springframework.data.domain.Sort.Order; -import org.springframework.data.jpa.domain.Example; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; 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; @@ -1907,42 +1911,343 @@ public class UserRepositoryTests { assertThat(users, hasSize(2)); } - + /** - * @see DATAJPA-218 + * @see DATAJPA-218 */ @Test - public void queryByExample() { - + public void findAllByExample() { + flushTestUsers(); - + User prototype = new User(); prototype.setAge(28); prototype.setCreatedAt(null); - - List users = repository.findWithExample(Example.exampleOf(prototype)); - + + List users = repository.findAllByExample(exampleOf(prototype)); + assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); } - + /** - * @see DATAJPA-218 + * @see DATAJPA-218 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void findAllByNullExample() { + repository.findAllByExample(null); + } + + /** + * @see DATAJPA-218 */ @Test - public void queryByExampleWithExcludedAttributes() { - + public void findAllByExampleWithExcludedAttributes() { + flushTestUsers(); - + User prototype = new User(); prototype.setAge(28); - - List users = repository.findWithExample(Example.newExample(prototype).ignoring("createdAt").build()); - + + List users = repository.findAllByExample(newExampleOf(prototype).ignore("createdAt").get()); + 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); + + List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + + 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)); + + List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + + 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 = newExampleOf(prototype).matchStringsStartingWith().ignore("age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).matchStringsEndingWith().ignore("age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).withStringMatcher(StringMatcher.REGEX).ignore("age", "createdAt") + .get(); + + repository.findAllByExample(example); + } + + /** + * @see DATAJPA-218 + */ + @Test + public void findAllByExampleWithIgnoreCase() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setFirstname("oLiVer"); + + Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).includeNullValues() + .ignore("id", "binaryData", "lastname", "emailAddress", "age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt") + .withPropertySpecifier(PropertySpecifier.newPropertySpecifier("firstname").matchStringStartingWith().get()) + .get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + List users = repository.findAllByExample(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 = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + Page users = repository.findAllByExample(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 = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + repository.findAllByExample(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 = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); @@ -1953,4 +2258,5 @@ public class UserRepositoryTests { assertThat(result.getTotalElements(), is(2L)); return result; } + }