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