From 09d01a0c7d80eacf69739d0a8f4821a25d063e34 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 1 Jul 2015 22:03:58 +0200 Subject: [PATCH 01/11] DATAJPA-218 - Support extracting parameters from a bean parameter. Prepare issue branch. --- pom.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 From 00bf1f7a5d8f85759516a0872cc5cfbcccdf9f86 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 2 Jul 2015 00:18:17 +0200 Subject: [PATCH 02/11] DATAJPA-218 - Support extracting parameters from a bean parameter. Added prototypic support for query by example queries to SimpleJpaRepositories. Clients can use an Example Object to wrap an existing prototype entity instance that will be used to derive a query from. Would be great if we could unify this effort with the infrastructure from DATAMONGO-1245. --- .../data/jpa/domain/Example.java | 138 ++++++++++++++++++ .../data/jpa/repository/JpaRepository.java | 14 +- .../support/SimpleJpaRepository.java | 38 +++++ .../data/jpa/domain/sample/User.java | 4 + .../jpa/repository/UserRepositoryTests.java | 38 +++++ 5 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/jpa/domain/Example.java diff --git a/src/main/java/org/springframework/data/jpa/domain/Example.java b/src/main/java/org/springframework/data/jpa/domain/Example.java new file mode 100644 index 0000000000..e3cc4d5073 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/domain/Example.java @@ -0,0 +1,138 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.domain; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * A wrapper around a prototype object that can be used in Query by Example queries + * + * @author Thomas Darimont + * @param + */ +public class Example { + + private final T prototype; + private final Set ignoredAttributes; + + /** + * Creates a new {@link Example} with the given {@code prototype}. + * + * @param prototype must not be {@literal null} + */ + public Example(T prototype) { + this(prototype, Collections. emptySet()); + } + + /** + * Creates a new {@link Example} with the given {@code prototype} ignoring the given attributes. + * + * @param prototype prototype must not be {@literal null} + * @param attributeNames prototype must not be {@literal null} + */ + public Example(T prototype, Set attributeNames) { + + Assert.notNull(prototype, "Prototype must not be null!"); + Assert.notNull(attributeNames, "attributeNames must not be null!"); + + this.prototype = prototype; + this.ignoredAttributes = attributeNames; + } + + public T getPrototype() { + return prototype; + } + + public Set getIgnoredAttributes() { + return Collections.unmodifiableSet(ignoredAttributes); + } + + public boolean isAttributeIgnored(String attributePath) { + return ignoredAttributes.contains(attributePath); + } + + public static Example exampleOf(T prototype) { + return new Example(prototype); + } + + public static Builder newExample(T prototype) { + return new Builder(prototype); + } + + /** + * A {@link Builder} for {@link Example}s. + * + * @author Thomas Darimont + * @param + */ + public static class Builder { + + private final T prototype; + private Set ignoredAttributeNames; + + /** + * @param prototype + */ + public Builder(T prototype) { + + Assert.notNull(prototype, "Prototype must not be null!"); + + this.prototype = prototype; + } + + /** + * Allows to specify attribute names that should be ignored. + * + * @param attributeNames + * @return + */ + public Builder ignoring(String... attributeNames) { + + Assert.notNull(attributeNames, "attributeNames must not be null!"); + + return ignoring(Arrays.asList(attributeNames)); + } + + /** + * Allows to specify attribute names that should be ignored. + * + * @param attributeNames + * @return + */ + public Builder ignoring(Collection attributeNames) { + + Assert.notNull(attributeNames, "attributeNames must not be null!"); + + this.ignoredAttributeNames = new HashSet(attributeNames); + return this; + } + + /** + * Constructs the actual {@link Example} instance. + * + * @return + */ + public Example build() { + return new Example(prototype, ignoredAttributeNames); + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java index c7d0b5f1cd..9132b8a45d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -21,6 +21,7 @@ import javax.persistence.EntityManager; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Example; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @@ -78,7 +79,7 @@ public interface JpaRepository extends PagingAndSort void deleteInBatch(Iterable entities); /** - * Deletes all entites in a batch call. + * Deletes all entities in a batch call. */ void deleteAllInBatch(); @@ -90,4 +91,15 @@ public interface JpaRepository extends PagingAndSort * @see EntityManager#getReference(Class, Object) */ T getOne(ID id); + + /** + * Returns all instances of the type specified by the given {@link Example}. + * + * This method is deliberately not named {@code findByExample} to not interfere + * with existing repository methods that rely on query derivation. + * + * @param example must not be {@literal null}. + * @return + */ + List findWithExample(Example example); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 5376c13c1e..0191dfb7ac 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -37,12 +38,16 @@ import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.persistence.metamodel.Attribute; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Example; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; @@ -54,6 +59,7 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer @@ -411,6 +417,38 @@ public List findAll(Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); } + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example) + */ + public List findWithExample(Example example) { + + Assert.notNull(example, "Example must not be null!"); + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createQuery(getDomainClass()); + Root root = query.from(getDomainClass()); + + BeanWrapper bean = new BeanWrapperImpl(example.getPrototype()); + + List predicates = new ArrayList(); + for (Attribute attribute : em.getMetamodel().managedType(getDomainClass()).getAttributes()) { + + Object value = bean.getPropertyValue(attribute.getName()); + + // TODO support different matching modes configured on the provided Example + if (value == null // + || (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) + || (value instanceof Map && CollectionUtils.isEmpty((Map) value)) + || example.isAttributeIgnored(attribute.getName())) { + continue; + } + + predicates.add(builder.equal(root.get(attribute.getName()), value)); + } + + return em.createQuery(query.where(predicates.toArray(new Predicate[predicates.size()]))).getResultList(); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#count() diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/src/test/java/org/springframework/data/jpa/domain/sample/User.java index 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..7d99804e9f 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -16,9 +16,11 @@ package org.springframework.data.jpa.repository; import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.*; import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.jpa.domain.Specifications.*; +import static org.springframework.data.jpa.domain.Specifications.not; import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; import java.util.ArrayList; @@ -55,6 +57,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.jpa.domain.Example; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; @@ -1904,6 +1907,41 @@ public void accept(User user) { assertThat(users, hasSize(2)); } + + /** + * @see DATAJPA-218 + */ + @Test + public void queryByExample() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setAge(28); + prototype.setCreatedAt(null); + + List users = repository.findWithExample(Example.exampleOf(prototype)); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(firstUser)); + } + + /** + * @see DATAJPA-218 + */ + @Test + public void queryByExampleWithExcludedAttributes() { + + flushTestUsers(); + + User prototype = new User(); + prototype.setAge(28); + + List users = repository.findWithExample(Example.newExample(prototype).ignoring("createdAt").build()); + + assertThat(users, hasSize(1)); + assertThat(users.get(0), is(firstUser)); + } private Page executeSpecWithSort(Sort sort) { From c2e3395e8d5b38caf3ed0774c13a2ad5064c8f44 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 19 Jan 2016 19:46:06 +0100 Subject: [PATCH 03/11] DATAJPA-218 - Add Predicate based QBE implementation. We convert a given Example to a set of and combinded Predicates using CriteriaBuilder. Cycles within associations are not allowed and result in an InvalidDataAccessApiUsageException. At this time only SingularAttributes are taken into concern. --- .../springframework/data/domain/Example.java | 456 ++++++++++++++++++ .../data/domain/PropertySpecifier.java | 216 +++++++++ .../data/domain/PropertyValueTransformer.java | 25 + .../QueryByExamplePredicateBuilder.java | 248 ++++++++++ .../data/jpa/domain/Example.java | 138 ------ .../jpa/provider/PersistenceProvider.java | 14 +- .../data/jpa/repository/JpaRepository.java | 36 +- .../repository/query/AbstractJpaQuery.java | 4 +- .../support/SimpleJpaRepository.java | 85 ++-- ...eryByExamplePredicateBuilderUnitTests.java | 271 +++++++++++ .../PersistenceProviderIntegrationTests.java | 2 - .../jpa/repository/UserRepositoryTests.java | 342 ++++++++++++- 12 files changed, 1633 insertions(+), 204 deletions(-) create mode 100644 src/main/java/org/springframework/data/domain/Example.java create mode 100644 src/main/java/org/springframework/data/domain/PropertySpecifier.java create mode 100644 src/main/java/org/springframework/data/domain/PropertyValueTransformer.java create mode 100644 src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java delete mode 100644 src/main/java/org/springframework/data/jpa/domain/Example.java create mode 100644 src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java new file mode 100644 index 0000000000..2efc46173a --- /dev/null +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -0,0 +1,456 @@ +/* + * 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.domain; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.data.domain.Example.NullHandler; +import org.springframework.data.domain.PropertySpecifier.NoOpPropertyValueTransformer; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Support for query by example (QBE). + * + * @author Christoph Strobl + * @param + */ +public class Example { + + private final T probe; + + private NullHandler nullHandler = NullHandler.IGNORE; + private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; + private PropertySpecifiers propertySpecifiers = new PropertySpecifiers(); + private Set ignoredPaths = new LinkedHashSet(); + + private boolean ignoreCase = false; + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe The example to use. Must not be {@literal null}. + */ + public Example(S probe) { + + Assert.notNull(probe, "Probe must not be null!"); + this.probe = probe; + } + + /** + * Get the example used. + * + * @return never {@literal null}. + */ + public T getProbe() { + return probe; + } + + /** + * Get defined null handling. + * + * @return never {@literal null} + */ + public NullHandler getNullHandler() { + return nullHandler; + } + + /** + * Get defined {@link StringMatcher}. + * + * @return never {@literal null}. + */ + public StringMatcher getDefaultStringMatcher() { + return defaultStringMatcher; + } + + /** + * @return {@literal true} if {@link String} should be matched with ignore case option. + */ + public boolean isIngnoreCaseEnabled() { + return this.ignoreCase; + } + + /** + * @param path + * @return + */ + public boolean isIgnoredPath(String path) { + return this.ignoredPaths.contains(path); + } + + /** + * @return + */ + public Set getIgnoredPaths() { + return Collections.unmodifiableSet(ignoredPaths); + } + + /** + * @return + */ + public Collection getPropertySpecifiers() { + return propertySpecifiers.getSpecifiers(); + } + + /** + * @param path Dot-Path to property. + * @return {@literal true} in case {@link PropertySpecifier} defined for given path. + */ + public boolean hasPropertySpecifier(String path) { + return propertySpecifiers.hasSpecifierForPath(path); + } + + /** + * @param path Dot-Path to property. + * @return {@literal null} when no {@link PropertySpecifier} defined for path. + */ + public PropertySpecifier getPropertySpecifier(String path) { + return propertySpecifiers.getForPath(path); + } + + /** + * @return true if at least one {@link PropertySpecifier} defined. + */ + public boolean hasPropertySpecifiers() { + return this.propertySpecifiers.hasValues(); + } + + /** + * Get the {@link StringMatcher} for a given path or return the default one if none defined. + * + * @param path + * @return never {@literal null}. + */ + public StringMatcher getStringMatcherForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return getDefaultStringMatcher(); + } + + PropertySpecifier specifier = getPropertySpecifier(path); + return specifier.getStringMatcher() != null ? specifier.getStringMatcher() : getDefaultStringMatcher(); + } + + /** + * Get the ignore case flag for a given path or return the default one if none defined. + * + * @param path + * @return never {@literal null}. + */ + public boolean isIgnoreCaseForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return isIngnoreCaseEnabled(); + } + + PropertySpecifier specifier = getPropertySpecifier(path); + return specifier.getIgnoreCase() != null ? specifier.getIgnoreCase().booleanValue() : isIngnoreCaseEnabled(); + } + + /** + * Get the ignore case flag for a given path or return {@link NoOpPropertyValueTransformer} if none defined. + * + * @param path + * @return + */ + public PropertyValueTransformer getValueTransformerForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return NoOpPropertyValueTransformer.INSTANCE; + } + + return getPropertySpecifier(path).getPropertyValueTransformer(); + } + + /** + * Get the actual type for the example used. + * + * @return + */ + @SuppressWarnings("unchecked") + public Class getProbeType() { + return (Class) ClassUtils.getUserClass(probe.getClass()); + } + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe must not be {@literal null}. + * @return + */ + public static Example exampleOf(T probe) { + return new Example(probe); + } + + /** + * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. + * + * @param probe must not be {@literal null}. + * @return + */ + public static Example exampleOf(T probe, String... ignoredProperties) { + return new Builder(probe).ignore(ignoredProperties).get(); + } + + /** + * Create new {@link Builder} for specifying {@link Example}. + * + * @param probe must not be {@literal null}. + * @return + * @see Builder + */ + public static Builder newExampleOf(S probe) { + return new Builder(probe); + } + + /** + * Builder for specifying desired behavior of {@link Example}. + * + * @author Christoph Strobl + * @param + */ + public static class Builder { + + private Example example; + + Builder(T probe) { + example = new Example(probe); + } + + /** + * Sets {@link NullHandler} used for {@link Example}. + * + * @param nullHandling + * @return + * @see Builder#nullHandling(NullHandler) + */ + public Builder with(NullHandler nullHandling) { + return handleNullValues(nullHandling); + } + + /** + * Sets default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher + * @return + * @see Builder#matchStrings(StringMatcher) + */ + public Builder with(StringMatcher stringMatcher) { + return matchStrings(stringMatcher); + } + + /** + * Adds {@link PropertySpecifier} used for {@link Example}. + * + * @param specifier + * @return + * @see Builder#specify(PropertySpecifier...) + */ + public Builder with(PropertySpecifier... specifiers) { + return specify(specifiers); + } + + /** + * Sets {@link NullHandler} used for {@link Example}. + * + * @param nullHandling Defaulted to {@link NullHandler#INCLUDE} in case of {@literal null}. + * @return + */ + public Builder handleNullValues(NullHandler nullHandling) { + + example.nullHandler = nullHandling == null ? NullHandler.IGNORE : nullHandling; + return this; + } + + public Builder includeNullValues() { + return handleNullValues(NullHandler.INCLUDE); + } + + /** + * Sets the default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. + * @return + */ + public Builder matchStrings(StringMatcher stringMatcher) { + return matchStrings(stringMatcher, example.ignoreCase); + } + + /** + * Sets the default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. + * @param ignoreCase + * @return + */ + public Builder matchStrings(StringMatcher stringMatching, boolean ignoreCase) { + + example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; + example.ignoreCase = ignoreCase; + return this; + } + + /** + * @return + */ + public Builder matchStringsWithIgnoreCase() { + example.ignoreCase = true; + return this; + } + + public Builder matchStringsStartingWith() { + return matchStrings(StringMatcher.STARTING); + } + + public Builder matchStringsEndingWith() { + return matchStrings(StringMatcher.ENDING); + } + + public Builder matchStringsContaining() { + return matchStrings(StringMatcher.CONTAINING); + } + + /** + * Define specific property handling. + * + * @param specifiers + * @return + */ + public Builder specify(PropertySpecifier... specifiers) { + + for (PropertySpecifier specifier : specifiers) { + example.propertySpecifiers.add(specifier); + } + return this; + } + + /** + * Ignore given properties. + * + * @param ignoredProperties + * @return + */ + public Builder ignore(String... ignoredProperties) { + + for (String ignoredProperty : ignoredProperties) { + example.ignoredPaths.add(ignoredProperty); + } + return this; + } + + /** + * @return {@link Example} as defined. + */ + public Example get() { + return this.example; + } + } + + /** + * Null handling for creating criterion out of an {@link Example}. + * + * @author Christoph Strobl + */ + public static enum NullHandler { + + INCLUDE, IGNORE + } + + /** + * Match modes for treatment of {@link String} values. + * + * @author Christoph Strobl + */ + public static enum StringMatcher { + + /** + * Store specific default. + */ + DEFAULT(null), + /** + * Matches the exact string + */ + EXACT(Type.SIMPLE_PROPERTY), + /** + * Matches string starting with pattern + */ + STARTING(Type.STARTING_WITH), + /** + * Matches string ending with pattern + */ + ENDING(Type.ENDING_WITH), + /** + * Matches string containing pattern + */ + CONTAINING(Type.CONTAINING), + /** + * Treats strings as regular expression patterns + */ + REGEX(Type.REGEX); + + private Type type; + + private StringMatcher(Type type) { + this.type = type; + } + + /** + * Get the according {@link Part.Type}. + * + * @return {@literal null} for {@link StringMatcher#DEFAULT}. + */ + public Type getPartType() { + return type; + } + + } + + static class PropertySpecifiers { + + private Map propertySpecifiers = new LinkedHashMap(); + + public void add(PropertySpecifier specifier) { + + Assert.notNull(specifier, "PropertySpecifier must not be null!"); + propertySpecifiers.put(specifier.getPath(), specifier); + } + + public boolean hasSpecifierForPath(String path) { + return propertySpecifiers.containsKey(path); + } + + public PropertySpecifier getForPath(String path) { + return propertySpecifiers.get(path); + } + + public boolean hasValues() { + return !propertySpecifiers.isEmpty(); + } + + public Collection getSpecifiers() { + return propertySpecifiers.values(); + } + } + +} diff --git a/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/src/main/java/org/springframework/data/domain/PropertySpecifier.java new file mode 100644 index 0000000000..da5981be09 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/PropertySpecifier.java @@ -0,0 +1,216 @@ +/* + * 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.domain; + +import org.springframework.data.domain.Example.StringMatcher; +import org.springframework.util.Assert; + +/** + * Define specific property handling for a Dot-Path. + * + * @author Christoph Strobl + */ +public class PropertySpecifier { + + private final String path; + + private StringMatcher stringMatcher; + private Boolean ignoreCase; + + private PropertyValueTransformer valueTransformer; + + /** + * Creates new {@link PropertySpecifier} for given path. + * + * @param path Dot-Path to the property. Must not be {@literal null}. + */ + PropertySpecifier(String path) { + + Assert.hasText(path, "Path must not be null/empty!"); + this.path = path; + } + + /** + * Get the properties Dot-Path. + * + * @return never {@literal null}. + */ + public String getPath() { + return path; + } + + /** + * Get the {@link StringMatcher}. + * + * @return can be {@literal null}. + */ + public StringMatcher getStringMatcher() { + return stringMatcher; + } + + /** + * @return {literal true} in case {@link StringMatcher} defined. + */ + public boolean hasStringMatcher() { + return this.stringMatcher != null; + } + + /** + * @return {@literal null} if not set. + */ + public Boolean getIgnoreCase() { + return ignoreCase; + } + + /** + * Get the property transformer to be applied. + * + * @return never {@literal null}. + */ + public PropertyValueTransformer getPropertyValueTransformer() { + return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; + } + + /** + * Transforms a given source using the {@link PropertyValueTransformer}. + * + * @param source + * @return + */ + public Object transformValue(Object source) { + return getPropertyValueTransformer().convert(source); + } + + /** + * Creates new case ignoring {@link PropertySpecifier} for given path. + * + * @param propertyPath must not be {@literal null}. + * @return + */ + public static PropertySpecifier ignoreCase(String propertyPath) { + return new Builder(propertyPath).ignoreCase().get(); + } + + /** + * Create new {@link Builder} for specifying {@link PropertySpecifier}. + * + * @param propertyPath must not be {@literal null}. + * @return + */ + public static Builder newPropertySpecifier(String propertyPath) { + return new Builder(propertyPath); + } + + /** + * Builder for specifying desired behavior of {@link PropertySpecifier}. + * + * @author Christoph Strobl + */ + public static class Builder { + + private PropertySpecifier specifier; + + Builder(String path) { + specifier = new PropertySpecifier(path); + } + + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @return + * @see Builder#stringMatcher(StringMatcher) + */ + public Builder with(StringMatcher stringMatcher) { + return stringMatcher(stringMatcher); + } + + /** + * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. + * + * @param valueTransformer + * @return + * @see Builder#valueTransformer(PropertyValueTransformer) + */ + public Builder with(PropertyValueTransformer valueTransformer) { + return valueTransformer(valueTransformer); + } + + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @return + */ + public Builder stringMatcher(StringMatcher stringMatcher) { + return stringMatcher(stringMatcher, specifier.ignoreCase); + } + + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @param ignoreCase + * @return + */ + public Builder stringMatcher(StringMatcher stringMatcher, Boolean ignoreCase) { + + specifier.stringMatcher = stringMatcher; + specifier.ignoreCase = ignoreCase; + return this; + } + + /** + * @return + */ + public Builder ignoreCase() { + specifier.ignoreCase = Boolean.TRUE; + return this; + } + + /** + * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. + * + * @param valueTransformer + * @return + */ + public Builder valueTransformer(PropertyValueTransformer valueTransformer) { + specifier.valueTransformer = valueTransformer; + return this; + } + + /** + * @return {@link PropertySpecifier} as defined. + */ + public PropertySpecifier get() { + return this.specifier; + } + } + + /** + * @author Christoph Strobl + */ + static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { + + INSTANCE; + + @Override + public Object convert(Object source) { + return source; + } + + } +} diff --git a/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java b/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java new file mode 100644 index 0000000000..30d779a1bd --- /dev/null +++ b/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java @@ -0,0 +1,25 @@ +/* + * 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.domain; + +import org.springframework.core.convert.converter.Converter; + +/** + * @author Christoph Strobl + */ +public interface PropertyValueTransformer extends Converter { + // TODO: should we use the converter interface directly or not at all? +} 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..506a935dde --- /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.getProbe(), example, + new PathNode("root", null, example.getProbe())); + + 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.getProbeType()), 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 e3cc4d5073..0000000000 --- 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 35c28e8ec6..901dd92d6f 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 @@ * @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 Collection potentiallyConvertEmptyCollection(Collection collect 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 Collection potentiallyConvertEmptyCollection(Collection collect public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new EclipseLinkScrollableResultsIterator(jpaQuery); } + }, /** @@ -201,6 +203,7 @@ public Object getIdentifierFrom(Object entity) { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new OpenJpaResultStreamingIterator(jpaQuery); } + }, /** @@ -243,6 +246,7 @@ public boolean shouldUseAccessorFor(Object entity) { public Object getIdentifierFrom(Object entity) { return null; } + }; /** @@ -384,8 +388,8 @@ public Collection potentiallyConvertEmptyCollection(Collection collect } 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 9132b8a45d..885afd8674 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 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 @@ * 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 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 */ - List findWithExample(Example example); + 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 b8ec62d3f3..d81cf30e2c 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 @@ private Query applyEntityGraphConfiguration(Query query, JpaQueryMethod method) 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 0191dfb7ac..6d4b2f8e6c 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 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.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.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 List findAll(Specification spec, Sort sort) { /* (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example) */ - public List findWithExample(Example example) { - - Assert.notNull(example, "Example must not be null!"); - - CriteriaBuilder builder = em.getCriteriaBuilder(); - CriteriaQuery query = builder.createQuery(getDomainClass()); - Root root = query.from(getDomainClass()); - - BeanWrapper bean = new BeanWrapperImpl(example.getPrototype()); - - List predicates = new ArrayList(); - for (Attribute attribute : em.getMetamodel().managedType(getDomainClass()).getAttributes()) { - - Object value = bean.getPropertyValue(attribute.getName()); - - // TODO support different matching modes configured on the provided Example - if (value == null // - || (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) - || (value instanceof Map && CollectionUtils.isEmpty((Map) value)) - || example.isAttributeIgnored(attribute.getName())) { - continue; - } + @Override + public List findAllByExample(Example example) { + return findAll(new ExampleSpecification(example)); + } - predicates.add(builder.equal(root.get(attribute.getName()), value)); - } + /* + * (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); + } - 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 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..d97eb7a9c7 --- /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 c72792c091..543ebbc200 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.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 7d99804e9f..cb568caa74 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.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.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,341 @@ public void accept(User user) { 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).with(StringMatcher.STARTING).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).with(StringMatcher.ENDING).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).with(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") + .with(PropertySpecifier.newPropertySpecifier("firstname").stringMatcher(StringMatcher.STARTING).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).with(StringMatcher.STARTING).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).with(StringMatcher.STARTING).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).with(StringMatcher.STARTING).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).with(StringMatcher.STARTING).matchStringsWithIgnoreCase() + .ignore("age", "createdAt").get(); + + repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); @@ -1953,4 +2256,5 @@ private Page executeSpecWithSort(Sort sort) { assertThat(result.getTotalElements(), is(2L)); return result; } + } From 7b26959bee896dd4b8637b35e95090cdebb3cbca Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 28 Jan 2016 14:20:51 +0100 Subject: [PATCH 04/11] DATAJPA-218 - Switch to types used in DATACMNS-810. --- .../springframework/data/domain/Example.java | 456 ------------------ .../data/domain/PropertySpecifier.java | 216 --------- .../data/domain/PropertyValueTransformer.java | 25 - .../QueryByExamplePredicateBuilder.java | 6 +- .../jpa/repository/UserRepositoryTests.java | 18 +- 5 files changed, 13 insertions(+), 708 deletions(-) delete mode 100644 src/main/java/org/springframework/data/domain/Example.java delete mode 100644 src/main/java/org/springframework/data/domain/PropertySpecifier.java delete mode 100644 src/main/java/org/springframework/data/domain/PropertyValueTransformer.java diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java deleted file mode 100644 index 2efc46173a..0000000000 --- a/src/main/java/org/springframework/data/domain/Example.java +++ /dev/null @@ -1,456 +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.domain; - -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.data.domain.Example.NullHandler; -import org.springframework.data.domain.PropertySpecifier.NoOpPropertyValueTransformer; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.data.repository.query.parser.Part.Type; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * Support for query by example (QBE). - * - * @author Christoph Strobl - * @param - */ -public class Example { - - private final T probe; - - private NullHandler nullHandler = NullHandler.IGNORE; - private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; - private PropertySpecifiers propertySpecifiers = new PropertySpecifiers(); - private Set ignoredPaths = new LinkedHashSet(); - - private boolean ignoreCase = false; - - /** - * Create a new {@link Example} including all non-null properties by default. - * - * @param probe The example to use. Must not be {@literal null}. - */ - public Example(S probe) { - - Assert.notNull(probe, "Probe must not be null!"); - this.probe = probe; - } - - /** - * Get the example used. - * - * @return never {@literal null}. - */ - public T getProbe() { - return probe; - } - - /** - * Get defined null handling. - * - * @return never {@literal null} - */ - public NullHandler getNullHandler() { - return nullHandler; - } - - /** - * Get defined {@link StringMatcher}. - * - * @return never {@literal null}. - */ - public StringMatcher getDefaultStringMatcher() { - return defaultStringMatcher; - } - - /** - * @return {@literal true} if {@link String} should be matched with ignore case option. - */ - public boolean isIngnoreCaseEnabled() { - return this.ignoreCase; - } - - /** - * @param path - * @return - */ - public boolean isIgnoredPath(String path) { - return this.ignoredPaths.contains(path); - } - - /** - * @return - */ - public Set getIgnoredPaths() { - return Collections.unmodifiableSet(ignoredPaths); - } - - /** - * @return - */ - public Collection getPropertySpecifiers() { - return propertySpecifiers.getSpecifiers(); - } - - /** - * @param path Dot-Path to property. - * @return {@literal true} in case {@link PropertySpecifier} defined for given path. - */ - public boolean hasPropertySpecifier(String path) { - return propertySpecifiers.hasSpecifierForPath(path); - } - - /** - * @param path Dot-Path to property. - * @return {@literal null} when no {@link PropertySpecifier} defined for path. - */ - public PropertySpecifier getPropertySpecifier(String path) { - return propertySpecifiers.getForPath(path); - } - - /** - * @return true if at least one {@link PropertySpecifier} defined. - */ - public boolean hasPropertySpecifiers() { - return this.propertySpecifiers.hasValues(); - } - - /** - * Get the {@link StringMatcher} for a given path or return the default one if none defined. - * - * @param path - * @return never {@literal null}. - */ - public StringMatcher getStringMatcherForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return getDefaultStringMatcher(); - } - - PropertySpecifier specifier = getPropertySpecifier(path); - return specifier.getStringMatcher() != null ? specifier.getStringMatcher() : getDefaultStringMatcher(); - } - - /** - * Get the ignore case flag for a given path or return the default one if none defined. - * - * @param path - * @return never {@literal null}. - */ - public boolean isIgnoreCaseForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return isIngnoreCaseEnabled(); - } - - PropertySpecifier specifier = getPropertySpecifier(path); - return specifier.getIgnoreCase() != null ? specifier.getIgnoreCase().booleanValue() : isIngnoreCaseEnabled(); - } - - /** - * Get the ignore case flag for a given path or return {@link NoOpPropertyValueTransformer} if none defined. - * - * @param path - * @return - */ - public PropertyValueTransformer getValueTransformerForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return NoOpPropertyValueTransformer.INSTANCE; - } - - return getPropertySpecifier(path).getPropertyValueTransformer(); - } - - /** - * Get the actual type for the example used. - * - * @return - */ - @SuppressWarnings("unchecked") - public Class getProbeType() { - return (Class) ClassUtils.getUserClass(probe.getClass()); - } - - /** - * Create a new {@link Example} including all non-null properties by default. - * - * @param probe must not be {@literal null}. - * @return - */ - public static Example exampleOf(T probe) { - return new Example(probe); - } - - /** - * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. - * - * @param probe must not be {@literal null}. - * @return - */ - public static Example exampleOf(T probe, String... ignoredProperties) { - return new Builder(probe).ignore(ignoredProperties).get(); - } - - /** - * Create new {@link Builder} for specifying {@link Example}. - * - * @param probe must not be {@literal null}. - * @return - * @see Builder - */ - public static Builder newExampleOf(S probe) { - return new Builder(probe); - } - - /** - * Builder for specifying desired behavior of {@link Example}. - * - * @author Christoph Strobl - * @param - */ - public static class Builder { - - private Example example; - - Builder(T probe) { - example = new Example(probe); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling - * @return - * @see Builder#nullHandling(NullHandler) - */ - public Builder with(NullHandler nullHandling) { - return handleNullValues(nullHandling); - } - - /** - * Sets default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher - * @return - * @see Builder#matchStrings(StringMatcher) - */ - public Builder with(StringMatcher stringMatcher) { - return matchStrings(stringMatcher); - } - - /** - * Adds {@link PropertySpecifier} used for {@link Example}. - * - * @param specifier - * @return - * @see Builder#specify(PropertySpecifier...) - */ - public Builder with(PropertySpecifier... specifiers) { - return specify(specifiers); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling Defaulted to {@link NullHandler#INCLUDE} in case of {@literal null}. - * @return - */ - public Builder handleNullValues(NullHandler nullHandling) { - - example.nullHandler = nullHandling == null ? NullHandler.IGNORE : nullHandling; - return this; - } - - public Builder includeNullValues() { - return handleNullValues(NullHandler.INCLUDE); - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @return - */ - public Builder matchStrings(StringMatcher stringMatcher) { - return matchStrings(stringMatcher, example.ignoreCase); - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @param ignoreCase - * @return - */ - public Builder matchStrings(StringMatcher stringMatching, boolean ignoreCase) { - - example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; - example.ignoreCase = ignoreCase; - return this; - } - - /** - * @return - */ - public Builder matchStringsWithIgnoreCase() { - example.ignoreCase = true; - return this; - } - - public Builder matchStringsStartingWith() { - return matchStrings(StringMatcher.STARTING); - } - - public Builder matchStringsEndingWith() { - return matchStrings(StringMatcher.ENDING); - } - - public Builder matchStringsContaining() { - return matchStrings(StringMatcher.CONTAINING); - } - - /** - * Define specific property handling. - * - * @param specifiers - * @return - */ - public Builder specify(PropertySpecifier... specifiers) { - - for (PropertySpecifier specifier : specifiers) { - example.propertySpecifiers.add(specifier); - } - return this; - } - - /** - * Ignore given properties. - * - * @param ignoredProperties - * @return - */ - public Builder ignore(String... ignoredProperties) { - - for (String ignoredProperty : ignoredProperties) { - example.ignoredPaths.add(ignoredProperty); - } - return this; - } - - /** - * @return {@link Example} as defined. - */ - public Example get() { - return this.example; - } - } - - /** - * Null handling for creating criterion out of an {@link Example}. - * - * @author Christoph Strobl - */ - public static enum NullHandler { - - INCLUDE, IGNORE - } - - /** - * Match modes for treatment of {@link String} values. - * - * @author Christoph Strobl - */ - public static enum StringMatcher { - - /** - * Store specific default. - */ - DEFAULT(null), - /** - * Matches the exact string - */ - EXACT(Type.SIMPLE_PROPERTY), - /** - * Matches string starting with pattern - */ - STARTING(Type.STARTING_WITH), - /** - * Matches string ending with pattern - */ - ENDING(Type.ENDING_WITH), - /** - * Matches string containing pattern - */ - CONTAINING(Type.CONTAINING), - /** - * Treats strings as regular expression patterns - */ - REGEX(Type.REGEX); - - private Type type; - - private StringMatcher(Type type) { - this.type = type; - } - - /** - * Get the according {@link Part.Type}. - * - * @return {@literal null} for {@link StringMatcher#DEFAULT}. - */ - public Type getPartType() { - return type; - } - - } - - static class PropertySpecifiers { - - private Map propertySpecifiers = new LinkedHashMap(); - - public void add(PropertySpecifier specifier) { - - Assert.notNull(specifier, "PropertySpecifier must not be null!"); - propertySpecifiers.put(specifier.getPath(), specifier); - } - - public boolean hasSpecifierForPath(String path) { - return propertySpecifiers.containsKey(path); - } - - public PropertySpecifier getForPath(String path) { - return propertySpecifiers.get(path); - } - - public boolean hasValues() { - return !propertySpecifiers.isEmpty(); - } - - public Collection getSpecifiers() { - return propertySpecifiers.values(); - } - } - -} diff --git a/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/src/main/java/org/springframework/data/domain/PropertySpecifier.java deleted file mode 100644 index da5981be09..0000000000 --- a/src/main/java/org/springframework/data/domain/PropertySpecifier.java +++ /dev/null @@ -1,216 +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.domain; - -import org.springframework.data.domain.Example.StringMatcher; -import org.springframework.util.Assert; - -/** - * Define specific property handling for a Dot-Path. - * - * @author Christoph Strobl - */ -public class PropertySpecifier { - - private final String path; - - private StringMatcher stringMatcher; - private Boolean ignoreCase; - - private PropertyValueTransformer valueTransformer; - - /** - * Creates new {@link PropertySpecifier} for given path. - * - * @param path Dot-Path to the property. Must not be {@literal null}. - */ - PropertySpecifier(String path) { - - Assert.hasText(path, "Path must not be null/empty!"); - this.path = path; - } - - /** - * Get the properties Dot-Path. - * - * @return never {@literal null}. - */ - public String getPath() { - return path; - } - - /** - * Get the {@link StringMatcher}. - * - * @return can be {@literal null}. - */ - public StringMatcher getStringMatcher() { - return stringMatcher; - } - - /** - * @return {literal true} in case {@link StringMatcher} defined. - */ - public boolean hasStringMatcher() { - return this.stringMatcher != null; - } - - /** - * @return {@literal null} if not set. - */ - public Boolean getIgnoreCase() { - return ignoreCase; - } - - /** - * Get the property transformer to be applied. - * - * @return never {@literal null}. - */ - public PropertyValueTransformer getPropertyValueTransformer() { - return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; - } - - /** - * Transforms a given source using the {@link PropertyValueTransformer}. - * - * @param source - * @return - */ - public Object transformValue(Object source) { - return getPropertyValueTransformer().convert(source); - } - - /** - * Creates new case ignoring {@link PropertySpecifier} for given path. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static PropertySpecifier ignoreCase(String propertyPath) { - return new Builder(propertyPath).ignoreCase().get(); - } - - /** - * Create new {@link Builder} for specifying {@link PropertySpecifier}. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static Builder newPropertySpecifier(String propertyPath) { - return new Builder(propertyPath); - } - - /** - * Builder for specifying desired behavior of {@link PropertySpecifier}. - * - * @author Christoph Strobl - */ - public static class Builder { - - private PropertySpecifier specifier; - - Builder(String path) { - specifier = new PropertySpecifier(path); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - * @see Builder#stringMatcher(StringMatcher) - */ - public Builder with(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher); - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - * @see Builder#valueTransformer(PropertyValueTransformer) - */ - public Builder with(PropertyValueTransformer valueTransformer) { - return valueTransformer(valueTransformer); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - */ - public Builder stringMatcher(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher, specifier.ignoreCase); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @param ignoreCase - * @return - */ - public Builder stringMatcher(StringMatcher stringMatcher, Boolean ignoreCase) { - - specifier.stringMatcher = stringMatcher; - specifier.ignoreCase = ignoreCase; - return this; - } - - /** - * @return - */ - public Builder ignoreCase() { - specifier.ignoreCase = Boolean.TRUE; - return this; - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - */ - public Builder valueTransformer(PropertyValueTransformer valueTransformer) { - specifier.valueTransformer = valueTransformer; - return this; - } - - /** - * @return {@link PropertySpecifier} as defined. - */ - public PropertySpecifier get() { - return this.specifier; - } - } - - /** - * @author Christoph Strobl - */ - static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { - - INSTANCE; - - @Override - public Object convert(Object source) { - return source; - } - - } -} diff --git a/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java b/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java deleted file mode 100644 index 30d779a1bd..0000000000 --- a/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java +++ /dev/null @@ -1,25 +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.domain; - -import org.springframework.core.convert.converter.Converter; - -/** - * @author Christoph Strobl - */ -public interface PropertyValueTransformer extends Converter { - // TODO: should we use the converter interface directly or not at all? -} 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 506a935dde..2e205e337f 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -75,8 +75,8 @@ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Examp Assert.notNull(cb, "CriteriaBuilder must not be null!"); Assert.notNull(example, "Root must not be null!"); - List predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(), example, - new PathNode("root", null, example.getProbe())); + 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)); @@ -133,7 +133,7 @@ static List getPredicates(String path, CriteriaBuilder cb, Path fr 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.getProbeType()), node)); + ClassUtils.getShortName(example.getSampleType()), node)); } predicates.addAll(getPredicates(currentPath, cb, ((From) from).join(attribute.getName()), 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 cb568caa74..a92559d5cb 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -2014,7 +2014,7 @@ public void findAllByExampleWithStartingStringMatcher() { User prototype = new User(); prototype.setFirstname("Ol"); - Example example = newExampleOf(prototype).with(StringMatcher.STARTING).ignore("age", "createdAt").get(); + Example example = newExampleOf(prototype).matchStringsStartingWith().ignore("age", "createdAt").get(); List users = repository.findAllByExample(example); @@ -2033,7 +2033,7 @@ public void findAllByExampleWithEndingStringMatcher() { User prototype = new User(); prototype.setFirstname("ver"); - Example example = newExampleOf(prototype).with(StringMatcher.ENDING).ignore("age", "createdAt").get(); + Example example = newExampleOf(prototype).matchStringsEndingWith().ignore("age", "createdAt").get(); List users = repository.findAllByExample(example); @@ -2052,7 +2052,8 @@ public void findAllByExampleWithRegexStringMatcher() { User prototype = new User(); prototype.setFirstname("^Oliver$"); - Example example = newExampleOf(prototype).with(StringMatcher.REGEX).ignore("age", "createdAt").get(); + Example example = newExampleOf(prototype).withStringMatcher(StringMatcher.REGEX).ignore("age", "createdAt") + .get(); repository.findAllByExample(example); } @@ -2142,7 +2143,8 @@ public void findAllByExampleWithPropertySpecifier() { prototype.setFirstname("oLi"); Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt") - .with(PropertySpecifier.newPropertySpecifier("firstname").stringMatcher(StringMatcher.STARTING).get()).get(); + .withPropertySpecifier(PropertySpecifier.newPropertySpecifier("firstname").matchStringStartingWith().get()) + .get(); List users = repository.findAllByExample(example); @@ -2166,7 +2168,7 @@ public void findAllByExampleWithSort() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).with(StringMatcher.STARTING).matchStringsWithIgnoreCase() + Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() .ignore("age", "createdAt").get(); List users = repository.findAllByExample(example, new Sort(DESC, "age")); @@ -2194,7 +2196,7 @@ public void findAllByExampleWithPageable() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).with(StringMatcher.STARTING).matchStringsWithIgnoreCase() + Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() .ignore("age", "createdAt").get(); Page users = repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); @@ -2217,7 +2219,7 @@ public void findAllByExampleShouldNotAllowCycles() { user1.setManager(user1); - Example example = newExampleOf(user1).with(StringMatcher.STARTING).matchStringsWithIgnoreCase() + Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() .ignore("age", "createdAt").get(); repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); @@ -2240,7 +2242,7 @@ public void findAllByExampleShouldNotAllowCyclesOverSeveralInstances() { user1.setManager(user2); user2.setManager(user1); - Example example = newExampleOf(user1).with(StringMatcher.STARTING).matchStringsWithIgnoreCase() + Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() .ignore("age", "createdAt").get(); repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); From 4af84b0cd4f5d600afc8d0de5f4a80c3d391f80e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Feb 2016 09:16:51 +0100 Subject: [PATCH 05/11] DATAJPA-218 - Add documentation for Query by Example. --- src/main/asciidoc/index.adoc | 5 +- src/main/asciidoc/query-by-example.adoc | 146 ++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/main/asciidoc/query-by-example.adoc diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index e5ca7b6dfe..4754d444e9 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. @@ -24,6 +24,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] :leveloffset: +1 include::jpa.adoc[] +include::query-by-example.adoc[] :leveloffset: -1 [[appendix]] diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc new file mode 100644 index 0000000000..4dddab02ac --- /dev/null +++ b/src/main/asciidoc/query-by-example.adoc @@ -0,0 +1,146 @@ +[[query.by.example]] += Query by Example + +== Introduction + +This chapter will give you an introduction to Query by Example and explain how to use example specifications. + +Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. + +== Usage + +An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties. You can use Query by Example with Repositories. + +Query by Example is suited for several use-cases but also comes with limitations: + +**When to use** + +* Querying your data store with a set of static or dynamic constraints +* Frequent refactoring of the entities without worrying about breaking existing queries +* Works independently from the data store API + +**Limitations** + +* Query predicates are combined using the `AND` keyword +* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` +* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types + + +Before getting started with Query by Example you need to have your interface to the data store set up. + +.Sample Person object +==== +[source,java] +---- +@Entity +public class Person { + + @Id + private String id; + private String firstname; + private String lastname; + private Address address; + + // … getters and setters omitted +} +---- +==== + +This is a simple entity. You can use it to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. + +.Simple Example specification +==== +[source,xml] +---- +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +Example example = Example.exampleOf(person); <3> +---- +<1> Create a new instance of the entity +<2> Set the properties to query +<3> Create an `Example` +==== + + +NOTE: Property names of the sample object must correlate with the property names of the queried entity. + +.Query by Example using a Repository +==== +[source, java] +---- +public interface JpaRepository { + + List findAllByExample(Example example); + + List findAllByExample(Example example, Sort sort); + + Page findAllByExample(Example example, Pageable pageable); + + // … more functionality omitted. +} +---- +==== + +[[query.by.example.builder]] +== Example builder + +Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the example builder. + +.Query by Example builder +==== +[source, java] +---- +Example.newExampleOf(person) + .withStringMatcher(StringMatcher.ENDING) + .includeNullValues() + .withPropertySpecifier( + newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()) + .withPropertySpecifier( + newPropertySpecifier("lastname").matchStringsWithIgnoreCase().get()) + .withPropertySpecifier( + newPropertySpecifier("address.city").matchStringStartingWith().get()) + .get(); +---- +==== + +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) + '%'` + +|=== From 463e17b117a978f9c52205cee1d443256e2c098b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 25 Feb 2016 09:34:46 +0100 Subject: [PATCH 06/11] DATAJPA-218 - Adopt QBE API refactoring. Adopt changes from query by example API refactoring. Related ticket: DATACMNS-810. --- .gitignore | 2 + .../QueryByExamplePredicateBuilder.java | 51 +++--- .../data/jpa/repository/JpaRepository.java | 52 +++--- .../support/SimpleJpaRepository.java | 111 +++++++++---- ...eryByExamplePredicateBuilderUnitTests.java | 11 +- .../jpa/repository/UserRepositoryTests.java | 157 ++++++++++++------ 6 files changed, 242 insertions(+), 142 deletions(-) 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/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java index 2e205e337f..887c2939e5 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 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.domain.ExampleSpecAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.util.Assert; @@ -48,8 +49,9 @@ * 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,10 +75,11 @@ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Examp 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)); @@ -90,8 +93,8 @@ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Examp } @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 @@ static List getPredicates(String path, CriteriaBuilder cb, Path fr 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 @@ static List getPredicates(String path, CriteriaBuilder cb, Path fr 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 @@ static List getPredicates(String path, CriteriaBuilder cb, Path fr 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 @@ static List getPredicates(String path, CriteriaBuilder cb, Path fr 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 @@ private static boolean isAssociation(Attribute attribute) { /** * {@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/repository/JpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java index 885afd8674..59d6ac2fc2 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -21,43 +21,48 @@ 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) * @see org.springframework.data.repository.CrudRepository#findAll() */ + @Override List findAll(); /* * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ + @Override List findAll(Sort sort); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ + @Override List findAll(Iterable ids); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ + @Override List save(Iterable entities); /** @@ -67,7 +72,7 @@ public interface JpaRepository extends PagingAndSort /** * Saves an entity and flushes changes instantly. - * + * * @param entity * @return the saved entity */ @@ -76,7 +81,7 @@ public interface JpaRepository extends PagingAndSort /** * Deletes the given entities in a batch which means it will create a single {@link Query}. Assume that we will clear * the {@link javax.persistence.EntityManager} after the call. - * + * * @param entities */ void deleteInBatch(Iterable entities); @@ -88,40 +93,23 @@ public interface JpaRepository extends PagingAndSort /** * Returns a reference to the entity with the given identifier. - * + * * @param id must not be {@literal null}. * @return a reference to the entity with the given identifier. * @see EntityManager#getReference(Class, Object) */ 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/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 6d4b2f8e6c..90158482f7 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. @@ -60,17 +60,18 @@ /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer * you a more sophisticated interface than the plain {@link EntityManager} . - * + * * @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!"; @@ -82,7 +83,7 @@ public class SimpleJpaRepository implements JpaRepos /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}. - * + * * @param entityInformation must not be {@literal null}. * @param entityManager must not be {@literal null}. */ @@ -98,7 +99,7 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given domain type. - * + * * @param domainClass must not be {@literal null}. * @param em must not be {@literal null}. */ @@ -109,7 +110,7 @@ public SimpleJpaRepository(Class domainClass, EntityManager em) { /** * Configures a custom {@link CrudMethodMetadata} to be used to detect {@link LockModeType}s and query hints to be * applied to queries. - * + * * @param crudMethodMetadata */ public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { @@ -138,6 +139,7 @@ private String getCountQueryString() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) */ + @Override @Transactional public void delete(ID id) { @@ -146,8 +148,8 @@ public void delete(ID id) { T entity = findOne(id); if (entity == null) { - throw new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", - entityInformation.getJavaType(), id), 1); + throw new EmptyResultDataAccessException( + String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1); } delete(entity); @@ -157,6 +159,7 @@ public void delete(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) */ + @Override @Transactional public void delete(T entity) { @@ -168,6 +171,7 @@ public void delete(T entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) */ + @Override @Transactional public void delete(Iterable entities) { @@ -182,6 +186,7 @@ public void delete(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable) */ + @Override @Transactional public void deleteInBatch(Iterable entities) { @@ -199,6 +204,7 @@ public void deleteInBatch(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.Repository#deleteAll() */ + @Override @Transactional public void deleteAll() { @@ -207,10 +213,11 @@ public void deleteAll() { } } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#deleteAllInBatch() */ + @Override @Transactional public void deleteAllInBatch() { em.createQuery(getDeleteAllQueryString()).executeUpdate(); @@ -220,6 +227,7 @@ public void deleteAllInBatch() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) */ + @Override public T findOne(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); @@ -240,7 +248,7 @@ public T findOne(ID id) { /** * Returns a {@link Map} with the query hints based on the current {@link CrudMethodMetadata} and potential * {@link EntityGraph} information. - * + * * @return */ protected Map getQueryHints() { @@ -263,7 +271,7 @@ private JpaEntityGraph getEntityGraph() { return new JpaEntityGraph(metadata.getEntityGraph(), fallbackName); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#getOne(java.io.Serializable) */ @@ -278,6 +286,7 @@ public T getOne(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) */ + @Override public boolean exists(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); @@ -321,6 +330,7 @@ public boolean exists(ID id) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findAll() */ + @Override public List findAll() { return getQuery(null, (Sort) null).getResultList(); } @@ -329,6 +339,7 @@ public List findAll() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(ID[]) */ + @Override public List findAll(Iterable ids) { if (ids == null || !ids.iterator().hasNext()) { @@ -356,6 +367,7 @@ public List findAll(Iterable ids) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findAll(org.springframework.data.domain.Sort) */ + @Override public List findAll(Sort sort) { return getQuery(null, sort).getResultList(); } @@ -364,19 +376,21 @@ public List findAll(Sort sort) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Pageable) */ + @Override public Page findAll(Pageable pageable) { if (null == pageable) { return new PageImpl(findAll()); } - return findAll(null, pageable); + return findAll((Specification) null, pageable); } /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne(org.springframework.data.jpa.domain.Specification) */ + @Override public T findOne(Specification spec) { try { @@ -390,6 +404,7 @@ public T findOne(Specification spec) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification) */ + @Override public List findAll(Specification spec) { return getQuery(spec, (Sort) null).getResultList(); } @@ -398,6 +413,7 @@ public List findAll(Specification spec) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Pageable) */ + @Override public Page findAll(Specification spec, Pageable pageable) { TypedQuery query = getQuery(spec, pageable); @@ -408,41 +424,68 @@ public Page findAll(Specification spec, Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Sort) */ + @Override public List findAll(Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); } /* (non-Javadoc) - * @see org.springframework.data.jpa.repository.JpaRepository#findWithExample(org.springframework.data.jpa.domain.Example) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example) + */ + @Override + public T findOne(Example example) { + return findOne(new ExampleSpecification((Example) example)); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#count(org.springframework.data.domain.Example) */ @Override - public List findAllByExample(Example example) { - return findAll(new ExampleSpecification(example)); + public long count(Example example) { + return count(new ExampleSpecification((Example) 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) 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 findAll(new ExampleSpecification((Example) example)); } /* * (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 findAll(new ExampleSpecification((Example) example), sort); + } + + /* + * (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) { + return findAll(new ExampleSpecification((Example) example), pageable); } /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#count() */ + @Override public long count() { return em.createQuery(getCountQueryString(), Long.class).getSingleResult(); } @@ -451,6 +494,7 @@ public long count() { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#count(org.springframework.data.jpa.domain.Specification) */ + @Override public long count(Specification spec) { return executeCountQuery(getCountQuery(spec)); @@ -460,6 +504,7 @@ public long count(Specification spec) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) */ + @Override @Transactional public S save(S entity) { @@ -475,6 +520,7 @@ public S save(S entity) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#saveAndFlush(java.lang.Object) */ + @Override @Transactional public S saveAndFlush(S entity) { @@ -488,6 +534,7 @@ public S saveAndFlush(S entity) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#save(java.lang.Iterable) */ + @Override @Transactional public List save(Iterable entities) { @@ -508,6 +555,7 @@ public List save(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#flush() */ + @Override @Transactional public void flush() { @@ -517,7 +565,7 @@ 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}. @@ -536,7 +584,7 @@ protected Page readPage(TypedQuery query, Pageable pageable, Specification /** * Creates a new {@link TypedQuery} from the given {@link Specification}. - * + * * @param spec can be {@literal null}. * @param pageable can be {@literal null}. * @return @@ -549,7 +597,7 @@ protected TypedQuery getQuery(Specification spec, Pageable pageable) { /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. - * + * * @param spec can be {@literal null}. * @param sort can be {@literal null}. * @return @@ -571,7 +619,7 @@ protected TypedQuery getQuery(Specification spec, Sort sort) { /** * Creates a new count query for the given {@link Specification}. - * + * * @param spec can be {@literal null}. * @return */ @@ -593,7 +641,7 @@ protected TypedQuery getCountQuery(Specification spec) { /** * Applies the given {@link Specification} to the given {@link CriteriaQuery}. - * + * * @param spec can be {@literal null}. * @param query must not be {@literal null}. * @return @@ -640,7 +688,7 @@ private void applyQueryHints(Query query) { /** * Executes a count query and transparently sums up all values returned. - * + * * @param query must not be {@literal null}. * @return */ @@ -662,7 +710,7 @@ private static Long executeCountQuery(TypedQuery query) { * Specification that gives access to the {@link Parameter} instance used to bind the ids for * {@link SimpleJpaRepository#findAll(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses * correctly when using by-name binding. - * + * * @see https://issues.apache.org/jira/browse/OPENJPA-2018?focusedCommentId=13924055 * @author Oliver Gierke */ @@ -681,6 +729,7 @@ public ByIdsSpecification(JpaEntityInformation entityInformation) { * (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) { Path path = root.get(entityInformation.getIdAttribute()); @@ -692,7 +741,7 @@ public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuild /** * {@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 +752,7 @@ private static class ExampleSpecification implements Specification { /** * 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 d97eb7a9c7..d75e565386 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 @@ /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class QueryByExamplePredicateBuilderUnitTests { @@ -109,7 +110,7 @@ public void setUp() { */ @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 void getPredicateShouldThrowExceptionOnNullRoot() { */ @Test(expected = IllegalArgumentException.class) public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() { - QueryByExamplePredicateBuilder.getPredicate(root, null, exampleOf(new Person())); + QueryByExamplePredicateBuilder.getPredicate(root, null, of(new Person())); } /** @@ -133,7 +134,7 @@ public void getPredicateShouldThrowExceptionOnNullExample() { */ @Test public void emptyCriteriaListShouldResultFalsePredicate() { - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, exampleOf(new Person())), equalTo(falsePredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(falsePredicate)); } /** @@ -145,7 +146,7 @@ public void singleElementCriteriaShouldJustReturnIt() { 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 void multiPredicateCriteriaShouldReturnCombinedOnes() { 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/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index a92559d5cb..15afe3af7b 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.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; @@ -80,10 +83,11 @@ * well as Hibernate configuration to execute tests. *

* To test further persistence providers subclass this class and provide a custom provider configuration. - * + * * @author Oliver Gierke * @author Kevin Raymond * @author Thomas Darimont + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:application-context.xml") @@ -315,7 +319,7 @@ public void testFindByLastname() throws Exception { /** * Tests, that searching by the email address of the reference user returns exactly that instance. - * + * * @throws Exception */ @Test @@ -343,7 +347,7 @@ public void testReadAll() { /** * Tests that all users get deleted by triggering {@link UserRepository#deleteAll()}. - * + * * @throws Exception */ @Test @@ -526,8 +530,8 @@ public void executesMethodWithAnnotatedNamedParametersCorrectly() throws Excepti firstUser = repository.save(firstUser); secondUser = repository.save(secondUser); - assertTrue(repository.findByLastnameOrFirstname("Oliver", "Arrasz").containsAll( - Arrays.asList(firstUser, secondUser))); + assertTrue( + repository.findByLastnameOrFirstname("Oliver", "Arrasz").containsAll(Arrays.asList(firstUser, secondUser))); } @Test @@ -593,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 @@ -1011,6 +1015,7 @@ public void doesNotDropNullValuesOnPagedSpecificationExecution() { flushTestUsers(); Page page = repository.findAll(new Specification() { + @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { return cb.equal(root.get("lastname"), "Gierke"); } @@ -1769,8 +1774,8 @@ public void shouldFindUsersInNativeQueryWithPagination() { public void shouldfindUsersBySpELExpressionParametersWithSpelTemplateExpression() { flushTestUsers(); - List users = repository.findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntityExpression( - "Joachim", "Arrasz"); + List users = repository + .findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntityExpression("Joachim", "Arrasz"); assertThat(users, hasSize(1)); assertThat(users.get(0), is(secondUser)); @@ -1924,7 +1929,7 @@ public void findAllByExample() { 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)); @@ -1935,7 +1940,7 @@ public void findAllByExample() { */ @Test(expected = InvalidDataAccessApiUsageException.class) public void findAllByNullExample() { - repository.findAllByExample(null); + repository.findAll((Example) null); } /** @@ -1949,7 +1954,8 @@ public void findAllByExampleWithExcludedAttributes() { User prototype = new User(); prototype.setAge(28); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("createdAt").get()); + Example example = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -1976,7 +1982,8 @@ public void findAllByExampleWithAssociation() { prototype.setCreatedAt(null); prototype.setManager(manager); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + Example example = ExampleSpec.of(User.class).withIgnorePaths("age").createExample(prototype); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -1997,7 +2004,8 @@ public void findAllByExampleWithEmbedded() { prototype.setCreatedAt(null); prototype.setAddress(new Address("germany", null, null, null)); - List users = repository.findAllByExample(newExampleOf(prototype).ignore("age").get()); + Example example = ExampleSpec.of(User.class).withIgnorePaths("age").createExample(prototype); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2014,9 +2022,9 @@ public void findAllByExampleWithStartingStringMatcher() { User prototype = new User(); prototype.setFirstname("Ol"); - Example example = newExampleOf(prototype).matchStringsStartingWith().ignore("age", "createdAt").get(); - - List users = repository.findAllByExample(example); + Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.STARTING) + .withIgnorePaths("age", "createdAt").createExample(prototype); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2033,9 +2041,9 @@ public void findAllByExampleWithEndingStringMatcher() { User prototype = new User(); prototype.setFirstname("ver"); - Example example = newExampleOf(prototype).matchStringsEndingWith().ignore("age", "createdAt").get(); - - List users = repository.findAllByExample(example); + Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.ENDING) + .withIgnorePaths("age", "createdAt").createExample(prototype); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2052,10 +2060,8 @@ public void findAllByExampleWithRegexStringMatcher() { User prototype = new User(); prototype.setFirstname("^Oliver$"); - Example example = newExampleOf(prototype).withStringMatcher(StringMatcher.REGEX).ignore("age", "createdAt") - .get(); - - repository.findAllByExample(example); + Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.REGEX).createExample(prototype); + repository.findAll(example); } /** @@ -2069,9 +2075,10 @@ public void findAllByExampleWithIgnoreCase() { User prototype = new User(); prototype.setFirstname("oLiVer"); - Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .createExample(prototype); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2088,10 +2095,10 @@ public void findAllByExampleWithStringMatcherAndIgnoreCase() { User prototype = new User(); prototype.setFirstname("oLiV"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.STARTING).withIgnoreCase() + .withIgnorePaths("age", "createdAt").createExample(prototype); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2122,10 +2129,10 @@ public void findAllByExampleWithIncludeNull() { User prototype = new User(); prototype.setFirstname(firstUser.getFirstname()); - Example example = newExampleOf(prototype).includeNullValues() - .ignore("id", "binaryData", "lastname", "emailAddress", "age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIncludeNullValues() + .withIgnorePaths("id", "binaryData", "lastname", "emailAddress", "age", "createdAt").createExample(prototype); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(fifthUser)); @@ -2142,11 +2149,10 @@ public void findAllByExampleWithPropertySpecifier() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsWithIgnoreCase().ignore("age", "createdAt") - .withPropertySpecifier(PropertySpecifier.newPropertySpecifier("firstname").matchStringStartingWith().get()) - .get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .withMatcher("firstname", new GenericPropertyMatcher().startsWith()).createExample(prototype); - List users = repository.findAllByExample(example); + List users = repository.findAll(example); assertThat(users, hasSize(1)); assertThat(users.get(0), is(firstUser)); @@ -2168,10 +2174,10 @@ public void findAllByExampleWithSort() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(prototype); - 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 +2202,10 @@ public void findAllByExampleWithPageable() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = newExampleOf(prototype).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(prototype); - 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 +2225,10 @@ public void findAllByExampleShouldNotAllowCycles() { user1.setManager(user1); - Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(user1); - repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + repository.findAll(example, new PageRequest(0, 10, new Sort(DESC, "age"))); } /** @@ -2242,10 +2248,61 @@ public void findAllByExampleShouldNotAllowCyclesOverSeveralInstances() { user1.setManager(user2); user2.setManager(user1); - Example example = newExampleOf(user1).matchStringsStartingWith().matchStringsWithIgnoreCase() - .ignore("age", "createdAt").get(); + Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(user1); + + 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 = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + 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 = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + 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 = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + boolean exists = repository.exists(example); - repository.findAllByExample(example, new PageRequest(0, 10, new Sort(DESC, "age"))); + assertThat(exists, is(true)); } private Page executeSpecWithSort(Sort sort) { From 473a0af9d8a391dcccaea5c773b47bee7d1931c1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Feb 2016 09:48:50 +0100 Subject: [PATCH 07/11] DATAJPA-218 - Documentation for Query-by-Example. --- src/main/asciidoc/index.adoc | 1 + src/main/asciidoc/query-by-example.adoc | 102 +++--------------------- 2 files changed, 12 insertions(+), 91 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 4754d444e9..c69102050b 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -24,6 +24,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] :leveloffset: +1 include::jpa.adoc[] +include::{spring-data-commons-docs}/query-by-example.adoc[] include::query-by-example.adoc[] :leveloffset: -1 diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc index 4dddab02ac..72447edc8f 100644 --- a/src/main/asciidoc/query-by-example.adoc +++ b/src/main/asciidoc/query-by-example.adoc @@ -1,109 +1,29 @@ -[[query.by.example]] -= Query by Example +[[query.by.example.execution]] +== Executing Query by Example -== Introduction - -This chapter will give you an introduction to Query by Example and explain how to use example specifications. - -Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. - -== Usage - -An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties. You can use Query by Example with Repositories. - -Query by Example is suited for several use-cases but also comes with limitations: - -**When to use** - -* Querying your data store with a set of static or dynamic constraints -* Frequent refactoring of the entities without worrying about breaking existing queries -* Works independently from the data store API - -**Limitations** - -* Query predicates are combined using the `AND` keyword -* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` -* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types - - -Before getting started with Query by Example you need to have your interface to the data store set up. - -.Sample Person object -==== -[source,java] ----- -@Entity -public class Person { - - @Id - private String id; - private String firstname; - private String lastname; - private Address address; - - // … getters and setters omitted -} ----- -==== - -This is a simple entity. You can use it to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. - -.Simple Example specification -==== -[source,xml] ----- -Person person = new Person(); <1> - -person.setFirstname("Dave"); <2> - -Example example = Example.exampleOf(person); <3> ----- -<1> Create a new instance of the entity -<2> Set the properties to query -<3> Create an `Example` -==== - - -NOTE: Property names of the sample object must correlate with the property names of the queried entity. +In Spring Data JPA you can use Query by Example with Repositories. .Query by Example using a Repository ==== [source, java] ---- -public interface JpaRepository { +public interface PersonRepository extends JpaRepository { - List findAllByExample(Example example); +} - List findAllByExample(Example example, Sort sort); +public class PersonService { - Page findAllByExample(Example example, Pageable pageable); + @Autowired PersonRepository personRepository; - // … more functionality omitted. + public List findPeople(Person probe) { + return personRepository.findAll(Example.of(probe)); + } } ---- ==== -[[query.by.example.builder]] -== Example builder +NOTE: Only SingularAttribute properties can be used for property matching. -Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the example builder. - -.Query by Example builder -==== -[source, java] ----- -Example.newExampleOf(person) - .withStringMatcher(StringMatcher.ENDING) - .includeNullValues() - .withPropertySpecifier( - newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()) - .withPropertySpecifier( - newPropertySpecifier("lastname").matchStringsWithIgnoreCase().get()) - .withPropertySpecifier( - newPropertySpecifier("address.city").matchStringStartingWith().get()) - .get(); ----- -==== 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. From 084abb294a30eb6142002b1ee10c1c07b4594705 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Feb 2016 17:21:11 +0100 Subject: [PATCH 08/11] DATAJPA-218 - Extend Query by Example API to typed and untyped specs. --- .../QueryByExamplePredicateBuilder.java | 2 +- .../data/jpa/repository/JpaRepository.java | 4 +- .../support/SimpleJpaRepository.java | 119 ++++++++++++++---- .../jpa/repository/UserRepositoryTests.java | 116 +++++++++++++---- 4 files changed, 190 insertions(+), 51 deletions(-) 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 887c2939e5..eb47d8ba71 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -35,7 +35,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleSpec; -import org.springframework.data.domain.ExampleSpecAccessor; +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; 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 59d6ac2fc2..7132299cd2 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -104,12 +104,12 @@ public interface JpaRepository * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ @Override - List findAll(Example example); + 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); + 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 90158482f7..efcd42b644 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 @@ -433,17 +433,23 @@ public List findAll(Specification spec, Sort sort) { /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example) */ + @SuppressWarnings("unchecked") @Override - public T findOne(Example example) { - return findOne(new ExampleSpecification((Example) example)); + public S findOne(Example example) { + try { + return getQuery(new ExampleSpecification(example), example.getResultType(), (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 count(new ExampleSpecification((Example) example)); + return executeCountQuery(getCountQuery(new ExampleSpecification(example), example.getResultType())); } /* (non-Javadoc) @@ -451,7 +457,8 @@ public long count(Example example) { */ @Override public boolean exists(Example example) { - return !getQuery(new ExampleSpecification((Example) example), (Sort) null).getResultList().isEmpty(); + return !getQuery(new ExampleSpecification(example), example.getResultType(), (Sort) null).getResultList() + .isEmpty(); } /* @@ -459,8 +466,8 @@ public boolean exists(Example example) { * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ @Override - public List findAll(Example example) { - return findAll(new ExampleSpecification((Example) example)); + public List findAll(Example example) { + return getQuery(new ExampleSpecification(example), example.getResultType(), (Sort) null).getResultList(); } /* @@ -468,8 +475,8 @@ public List findAll(Example example) { * @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 findAll(new ExampleSpecification((Example) example), sort); + public List findAll(Example example, Sort sort) { + return getQuery(new ExampleSpecification(example), example.getResultType(), sort).getResultList(); } /* @@ -477,8 +484,12 @@ public List findAll(Example example, Sort sort) { * @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) { - return findAll(new ExampleSpecification((Example) example), pageable); + public Page findAll(Example example, Pageable pageable) { + + ExampleSpecification spec = new ExampleSpecification(example); + TypedQuery query = getQuery(new ExampleSpecification(example), example.getResultType(), pageable); + return pageable == null ? new PageImpl(query.getResultList()) + : readPage(query, example.getResultType(), pageable, spec); } /* @@ -496,7 +507,6 @@ public long count() { */ @Override public long count(Specification spec) { - return executeCountQuery(getCountQuery(spec)); } @@ -572,14 +582,29 @@ public void flush() { * @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); } /** @@ -592,7 +617,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); } /** @@ -603,11 +642,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) { @@ -624,11 +675,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)); @@ -647,9 +709,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; @@ -665,14 +742,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); 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 15afe3af7b..f39c869e61 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -1283,6 +1283,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 */ @@ -1954,7 +2016,7 @@ public void findAllByExampleWithExcludedAttributes() { User prototype = new User(); prototype.setAge(28); - Example example = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt")); List users = repository.findAll(example); assertThat(users, hasSize(1)); @@ -1982,7 +2044,7 @@ public void findAllByExampleWithAssociation() { prototype.setCreatedAt(null); prototype.setManager(manager); - Example example = ExampleSpec.of(User.class).withIgnorePaths("age").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("age")); List users = repository.findAll(example); assertThat(users, hasSize(1)); @@ -2004,7 +2066,7 @@ public void findAllByExampleWithEmbedded() { prototype.setCreatedAt(null); prototype.setAddress(new Address("germany", null, null, null)); - Example example = ExampleSpec.of(User.class).withIgnorePaths("age").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("age")); List users = repository.findAll(example); assertThat(users, hasSize(1)); @@ -2022,8 +2084,8 @@ public void findAllByExampleWithStartingStringMatcher() { User prototype = new User(); prototype.setFirstname("Ol"); - Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.STARTING) - .withIgnorePaths("age", "createdAt").createExample(prototype); + Example example = Example.of(prototype, + ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.STARTING).withIgnorePaths("age", "createdAt")); List users = repository.findAll(example); assertThat(users, hasSize(1)); @@ -2041,8 +2103,8 @@ public void findAllByExampleWithEndingStringMatcher() { User prototype = new User(); prototype.setFirstname("ver"); - Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.ENDING) - .withIgnorePaths("age", "createdAt").createExample(prototype); + Example example = Example.of(prototype, + ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.ENDING).withIgnorePaths("age", "createdAt")); List users = repository.findAll(example); assertThat(users, hasSize(1)); @@ -2060,7 +2122,7 @@ public void findAllByExampleWithRegexStringMatcher() { User prototype = new User(); prototype.setFirstname("^Oliver$"); - Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.REGEX).createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withStringMatcher(StringMatcher.REGEX)); repository.findAll(example); } @@ -2075,8 +2137,8 @@ public void findAllByExampleWithIgnoreCase() { User prototype = new User(); prototype.setFirstname("oLiVer"); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .createExample(prototype); + Example example = Example.of(prototype, + ExampleSpec.typed(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt")); List users = repository.findAll(example); @@ -2095,8 +2157,8 @@ public void findAllByExampleWithStringMatcherAndIgnoreCase() { User prototype = new User(); prototype.setFirstname("oLiV"); - Example example = ExampleSpec.of(User.class).withStringMatcher(StringMatcher.STARTING).withIgnoreCase() - .withIgnorePaths("age", "createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class) + .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().withIgnorePaths("age", "createdAt")); List users = repository.findAll(example); @@ -2129,8 +2191,8 @@ public void findAllByExampleWithIncludeNull() { User prototype = new User(); prototype.setFirstname(firstUser.getFirstname()); - Example example = ExampleSpec.of(User.class).withIncludeNullValues() - .withIgnorePaths("id", "binaryData", "lastname", "emailAddress", "age", "createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIncludeNullValues() + .withIgnorePaths("id", "binaryData", "lastname", "emailAddress", "age", "createdAt")); List users = repository.findAll(example); @@ -2149,8 +2211,8 @@ public void findAllByExampleWithPropertySpecifier() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .withMatcher("firstname", new GenericPropertyMatcher().startsWith()).createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnoreCase() + .withIgnorePaths("age", "createdAt").withMatcher("firstname", new GenericPropertyMatcher().startsWith())); List users = repository.findAll(example); @@ -2174,8 +2236,8 @@ public void findAllByExampleWithSort() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(prototype); + 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")); @@ -2202,8 +2264,8 @@ public void findAllByExampleWithPageable() { User prototype = new User(); prototype.setFirstname("oLi"); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(prototype); + 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"))); @@ -2225,8 +2287,8 @@ public void findAllByExampleShouldNotAllowCycles() { user1.setManager(user1); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(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"))); } @@ -2248,8 +2310,8 @@ public void findAllByExampleShouldNotAllowCyclesOverSeveralInstances() { user1.setManager(user2); user2.setManager(user1); - Example example = ExampleSpec.of(User.class).withIgnoreCase().withIgnorePaths("age", "createdAt") - .withStringMatcher(StringMatcher.STARTING).withIgnoreCase().createExample(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"))); } @@ -2265,7 +2327,7 @@ public void findOneByExampleWithExcludedAttributes() { User prototype = new User(); prototype.setAge(28); - Example example = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt")); User users = repository.findOne(example); assertThat(users, is(firstUser)); @@ -2282,7 +2344,7 @@ public void countByExampleWithExcludedAttributes() { User prototype = new User(); prototype.setAge(28); - Example example = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt")); long count = repository.count(example); assertThat(count, is(1L)); @@ -2299,7 +2361,7 @@ public void existsByExampleWithExcludedAttributes() { User prototype = new User(); prototype.setAge(28); - Example example = ExampleSpec.of(User.class).withIgnorePaths("createdAt").createExample(prototype); + Example example = Example.of(prototype, ExampleSpec.typed(User.class).withIgnorePaths("createdAt")); boolean exists = repository.exists(example); assertThat(exists, is(true)); From bf4837e0cbdb7ada657786066e471962da6d3a36 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 11:36:37 +0100 Subject: [PATCH 09/11] DATAJPA-218 - Update documentation. --- src/main/asciidoc/index.adoc | 2 -- src/main/asciidoc/jpa.adoc | 3 +++ src/main/asciidoc/query-by-example.adoc | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index c69102050b..6e02d38ff1 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -24,8 +24,6 @@ include::{spring-data-commons-docs}/repositories.adoc[] :leveloffset: +1 include::jpa.adoc[] -include::{spring-data-commons-docs}/query-by-example.adoc[] -include::query-by-example.adoc[] :leveloffset: -1 [[appendix]] 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 index 72447edc8f..92654f16e3 100644 --- a/src/main/asciidoc/query-by-example.adoc +++ b/src/main/asciidoc/query-by-example.adoc @@ -1,5 +1,5 @@ [[query.by.example.execution]] -== Executing Query by Example +== Executing Example In Spring Data JPA you can use Query by Example with Repositories. @@ -22,6 +22,8 @@ public class PersonService { ---- ==== +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. From 7b24496ddd4f56eff4a26d980b2ee0cf34ce2570 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 12:24:17 +0100 Subject: [PATCH 10/11] DATAJPA-218 - Use domain type for untyped Examples. --- .../support/SimpleJpaRepository.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) 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 efcd42b644..38f43718cd 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 @@ -44,6 +44,7 @@ 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; @@ -437,7 +438,7 @@ public List findAll(Specification spec, Sort sort) { @Override public S findOne(Example example) { try { - return getQuery(new ExampleSpecification(example), example.getResultType(), (Sort) null).getSingleResult(); + return getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getSingleResult(); } catch (NoResultException e) { return null; } @@ -449,7 +450,7 @@ public S findOne(Example example) { @SuppressWarnings("unchecked") @Override public long count(Example example) { - return executeCountQuery(getCountQuery(new ExampleSpecification(example), example.getResultType())); + return executeCountQuery(getCountQuery(new ExampleSpecification(example), getResultType(example))); } /* (non-Javadoc) @@ -457,7 +458,7 @@ public long count(Example example) { */ @Override public boolean exists(Example example) { - return !getQuery(new ExampleSpecification(example), example.getResultType(), (Sort) null).getResultList() + return !getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList() .isEmpty(); } @@ -467,7 +468,7 @@ public boolean exists(Example example) { */ @Override public List findAll(Example example) { - return getQuery(new ExampleSpecification(example), example.getResultType(), (Sort) null).getResultList(); + return getQuery(new ExampleSpecification(example), getResultType(example), (Sort) null).getResultList(); } /* @@ -476,7 +477,7 @@ public List findAll(Example example) { */ @Override public List findAll(Example example, Sort sort) { - return getQuery(new ExampleSpecification(example), example.getResultType(), sort).getResultList(); + return getQuery(new ExampleSpecification(example), getResultType(example), sort).getResultList(); } /* @@ -487,9 +488,9 @@ public List findAll(Example example, Sort sort) { public Page findAll(Example example, Pageable pageable) { ExampleSpecification spec = new ExampleSpecification(example); - TypedQuery query = getQuery(new ExampleSpecification(example), example.getResultType(), pageable); + TypedQuery query = getQuery(new ExampleSpecification(example), getResultType(example), pageable); return pageable == null ? new PageImpl(query.getResultList()) - : readPage(query, example.getResultType(), pageable, spec); + : readPage(query, getResultType(example), pageable, spec); } /* @@ -763,6 +764,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. * From 9828c193db6be5c7b46b49b7eaf91001bc4fe22d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 15:14:14 +0100 Subject: [PATCH 11/11] DATAJPA-218 - Use true is true predicate if probe yields to empty criteria. --- .../QueryByExamplePredicateBuilder.java | 2 +- .../jpa/provider/PersistenceProvider.java | 14 ++--- .../data/jpa/repository/JpaRepository.java | 10 ++-- .../repository/query/AbstractJpaQuery.java | 4 +- .../support/SimpleJpaRepository.java | 52 ++++++------------- ...eryByExamplePredicateBuilderUnitTests.java | 8 +-- .../PersistenceProviderIntegrationTests.java | 2 + .../jpa/repository/UserRepositoryTests.java | 33 ++++++++---- 8 files changed, 56 insertions(+), 69 deletions(-) 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 eb47d8ba71..72cee28f22 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -82,7 +82,7 @@ public static Predicate getPredicate(Root root, CriteriaBuilder cb, Examp 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) { 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 901dd92d6f..35c28e8ec6 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 @@ * @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 Collection potentiallyConvertEmptyCollection(Collection collect 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 Collection potentiallyConvertEmptyCollection(Collection collect public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new EclipseLinkScrollableResultsIterator(jpaQuery); } - }, /** @@ -203,7 +201,6 @@ public Object getIdentifierFrom(Object entity) { public CloseableIterator executeQueryWithResultStream(Query jpaQuery) { return new OpenJpaResultStreamingIterator(jpaQuery); } - }, /** @@ -246,7 +243,6 @@ public boolean shouldUseAccessorFor(Object entity) { public Object getIdentifierFrom(Object entity) { return null; } - }; /** @@ -388,8 +384,8 @@ public Collection potentiallyConvertEmptyCollection(Collection collect } 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 7132299cd2..299616d00a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -41,28 +41,24 @@ public interface JpaRepository * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ - @Override List findAll(); /* * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ - @Override List findAll(Sort sort); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ - @Override List findAll(Iterable ids); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ - @Override List save(Iterable entities); /** @@ -72,7 +68,7 @@ public interface JpaRepository /** * Saves an entity and flushes changes instantly. - * + * * @param entity * @return the saved entity */ @@ -81,7 +77,7 @@ public interface JpaRepository /** * Deletes the given entities in a batch which means it will create a single {@link Query}. Assume that we will clear * the {@link javax.persistence.EntityManager} after the call. - * + * * @param entities */ void deleteInBatch(Iterable entities); @@ -93,7 +89,7 @@ public interface JpaRepository /** * Returns a reference to the entity with the given identifier. - * + * * @param id must not be {@literal null}. * @return a reference to the entity with the given identifier. * @see EntityManager#getReference(Class, Object) 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 d81cf30e2c..b8ec62d3f3 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 @@ private Query applyEntityGraphConfiguration(Query query, JpaQueryMethod method) 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 38f43718cd..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 @@ -61,7 +61,7 @@ /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer * you a more sophisticated interface than the plain {@link EntityManager} . - * + * * @author Oliver Gierke * @author Eberhard Wolff * @author Thomas Darimont @@ -84,7 +84,7 @@ public class SimpleJpaRepository /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}. - * + * * @param entityInformation must not be {@literal null}. * @param entityManager must not be {@literal null}. */ @@ -100,7 +100,7 @@ public SimpleJpaRepository(JpaEntityInformation entityInformation, EntityM /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given domain type. - * + * * @param domainClass must not be {@literal null}. * @param em must not be {@literal null}. */ @@ -111,7 +111,7 @@ public SimpleJpaRepository(Class domainClass, EntityManager em) { /** * Configures a custom {@link CrudMethodMetadata} to be used to detect {@link LockModeType}s and query hints to be * applied to queries. - * + * * @param crudMethodMetadata */ public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { @@ -140,7 +140,6 @@ private String getCountQueryString() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) */ - @Override @Transactional public void delete(ID id) { @@ -149,8 +148,8 @@ public void delete(ID id) { T entity = findOne(id); if (entity == null) { - throw new EmptyResultDataAccessException( - String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1); + throw new EmptyResultDataAccessException(String.format("No %s entity with id %s exists!", + entityInformation.getJavaType(), id), 1); } delete(entity); @@ -160,7 +159,6 @@ public void delete(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) */ - @Override @Transactional public void delete(T entity) { @@ -172,7 +170,6 @@ public void delete(T entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) */ - @Override @Transactional public void delete(Iterable entities) { @@ -187,7 +184,6 @@ public void delete(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable) */ - @Override @Transactional public void deleteInBatch(Iterable entities) { @@ -205,7 +201,6 @@ public void deleteInBatch(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.Repository#deleteAll() */ - @Override @Transactional public void deleteAll() { @@ -214,11 +209,10 @@ public void deleteAll() { } } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#deleteAllInBatch() */ - @Override @Transactional public void deleteAllInBatch() { em.createQuery(getDeleteAllQueryString()).executeUpdate(); @@ -228,7 +222,6 @@ public void deleteAllInBatch() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) */ - @Override public T findOne(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); @@ -249,7 +242,7 @@ public T findOne(ID id) { /** * Returns a {@link Map} with the query hints based on the current {@link CrudMethodMetadata} and potential * {@link EntityGraph} information. - * + * * @return */ protected Map getQueryHints() { @@ -272,7 +265,7 @@ private JpaEntityGraph getEntityGraph() { return new JpaEntityGraph(metadata.getEntityGraph(), fallbackName); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#getOne(java.io.Serializable) */ @@ -287,7 +280,6 @@ public T getOne(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) */ - @Override public boolean exists(ID id) { Assert.notNull(id, ID_MUST_NOT_BE_NULL); @@ -331,7 +323,6 @@ public boolean exists(ID id) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findAll() */ - @Override public List findAll() { return getQuery(null, (Sort) null).getResultList(); } @@ -340,7 +331,6 @@ public List findAll() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(ID[]) */ - @Override public List findAll(Iterable ids) { if (ids == null || !ids.iterator().hasNext()) { @@ -368,7 +358,6 @@ public List findAll(Iterable ids) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#findAll(org.springframework.data.domain.Sort) */ - @Override public List findAll(Sort sort) { return getQuery(null, sort).getResultList(); } @@ -377,7 +366,6 @@ public List findAll(Sort sort) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Pageable) */ - @Override public Page findAll(Pageable pageable) { if (null == pageable) { @@ -391,7 +379,6 @@ public Page findAll(Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne(org.springframework.data.jpa.domain.Specification) */ - @Override public T findOne(Specification spec) { try { @@ -405,7 +392,6 @@ public T findOne(Specification spec) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification) */ - @Override public List findAll(Specification spec) { return getQuery(spec, (Sort) null).getResultList(); } @@ -414,7 +400,6 @@ public List findAll(Specification spec) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Pageable) */ - @Override public Page findAll(Specification spec, Pageable pageable) { TypedQuery query = getQuery(spec, pageable); @@ -425,7 +410,6 @@ public Page findAll(Specification spec, Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Sort) */ - @Override public List findAll(Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); @@ -497,7 +481,6 @@ public Page findAll(Example example, Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#count() */ - @Override public long count() { return em.createQuery(getCountQueryString(), Long.class).getSingleResult(); } @@ -506,8 +489,8 @@ public long count() { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#count(org.springframework.data.jpa.domain.Specification) */ - @Override public long count(Specification spec) { + return executeCountQuery(getCountQuery(spec)); } @@ -515,7 +498,6 @@ public long count(Specification spec) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) */ - @Override @Transactional public S save(S entity) { @@ -531,7 +513,6 @@ public S save(S entity) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#saveAndFlush(java.lang.Object) */ - @Override @Transactional public S saveAndFlush(S entity) { @@ -545,7 +526,6 @@ public S saveAndFlush(S entity) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#save(java.lang.Iterable) */ - @Override @Transactional public List save(Iterable entities) { @@ -566,7 +546,6 @@ public List save(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#flush() */ - @Override @Transactional public void flush() { @@ -637,7 +616,7 @@ protected TypedQuery getQuery(Specification spec, Class d /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. - * + * * @param spec can be {@literal null}. * @param sort can be {@literal null}. * @return @@ -671,7 +650,7 @@ protected TypedQuery getQuery(Specification spec, Class d /** * Creates a new count query for the given {@link Specification}. - * + * * @param spec can be {@literal null}. * @return */ @@ -704,7 +683,7 @@ protected TypedQuery getCountQuery(Specification spec, Cl /** * Applies the given {@link Specification} to the given {@link CriteriaQuery}. - * + * * @param spec can be {@literal null}. * @param query must not be {@literal null}. * @return @@ -775,7 +754,7 @@ private Class getResultType(Example example) { /** * Executes a count query and transparently sums up all values returned. - * + * * @param query must not be {@literal null}. * @return */ @@ -797,7 +776,7 @@ private static Long executeCountQuery(TypedQuery query) { * Specification that gives access to the {@link Parameter} instance used to bind the ids for * {@link SimpleJpaRepository#findAll(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses * correctly when using by-name binding. - * + * * @see https://issues.apache.org/jira/browse/OPENJPA-2018?focusedCommentId=13924055 * @author Oliver Gierke */ @@ -816,7 +795,6 @@ public ByIdsSpecification(JpaEntityInformation entityInformation) { * (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) { Path path = root.get(entityInformation.getIdAttribute()); 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 d75e565386..3ddf152da5 100644 --- a/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java @@ -56,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; @@ -101,7 +101,7 @@ public void setUp() { 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); } @@ -133,8 +133,8 @@ public void getPredicateShouldThrowExceptionOnNullExample() { * @see DATAJPA-218 */ @Test - public void emptyCriteriaListShouldResultFalsePredicate() { - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(falsePredicate)); + public void emptyCriteriaListShouldResultTruePredicate() { + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(truePredicate)); } /** 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 543ebbc200..c72792c091 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.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 f39c869e61..691ab45e13 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -83,7 +83,7 @@ * well as Hibernate configuration to execute tests. *

* To test further persistence providers subclass this class and provide a custom provider configuration. - * + * * @author Oliver Gierke * @author Kevin Raymond * @author Thomas Darimont @@ -319,7 +319,7 @@ public void testFindByLastname() throws Exception { /** * Tests, that searching by the email address of the reference user returns exactly that instance. - * + * * @throws Exception */ @Test @@ -347,7 +347,7 @@ public void testReadAll() { /** * Tests that all users get deleted by triggering {@link UserRepository#deleteAll()}. - * + * * @throws Exception */ @Test @@ -530,8 +530,8 @@ public void executesMethodWithAnnotatedNamedParametersCorrectly() throws Excepti firstUser = repository.save(firstUser); secondUser = repository.save(secondUser); - assertTrue( - repository.findByLastnameOrFirstname("Oliver", "Arrasz").containsAll(Arrays.asList(firstUser, secondUser))); + assertTrue(repository.findByLastnameOrFirstname("Oliver", "Arrasz").containsAll( + Arrays.asList(firstUser, secondUser))); } @Test @@ -1015,7 +1015,6 @@ public void doesNotDropNullValuesOnPagedSpecificationExecution() { flushTestUsers(); Page page = repository.findAll(new Specification() { - @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) { return cb.equal(root.get("lastname"), "Gierke"); } @@ -1836,8 +1835,8 @@ public void shouldFindUsersInNativeQueryWithPagination() { public void shouldfindUsersBySpELExpressionParametersWithSpelTemplateExpression() { flushTestUsers(); - List users = repository - .findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntityExpression("Joachim", "Arrasz"); + List users = repository.findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntityExpression( + "Joachim", "Arrasz"); assertThat(users, hasSize(1)); assertThat(users.get(0), is(secondUser)); @@ -1997,6 +1996,23 @@ public void findAllByExample() { 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 */ @@ -2377,5 +2393,4 @@ private Page executeSpecWithSort(Sort sort) { assertThat(result.getTotalElements(), is(2L)); return result; } - }