From a63157237b23e8b1f7908c4886fd8330c4a8d800 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 29 Dec 2010 13:42:10 +0100 Subject: [PATCH] DATAJPA-8 - Introduced support for QueryDsl specifications Extracted methods taking a Specification from JpaRepository into JpaSpecificationExecutor. Introduced QueryDslPredicateExecutor that takes QueryDsl Predicate objects to execute them. Introduced QueryDsl specific subclass to execute Predicate instances. Extended JpaRepositoryFactory to rather use the QueryDsl specific implementation if the actual user repository implements QueryDslPredicateExecutor. Added test case to ensure detection of query class. Added QueryDslRepositorySupport to ease implementation of repositories. Added constructor to QueryDslJpaRepository to take an EntityPathResolver to allow plugging in a different implementation. Extracted actual EntityPath resolution tests into unit test for SimpleEntityPathResolver. Check for QueryDsl classpath presence to prevent ClassNotFoundExceptions. --- pom.xml | 44 ++- .../data/jpa/repository/JpaRepository.java | 29 -- .../repository/JpaSpecificationExecutor.java | 70 ++++ .../repository/QueryDslPredicateExecutor.java | 81 ++++ .../support/JpaRepositoryFactory.java | 39 +- .../support/QueryDslJpaRepository.java | 348 ++++++++++++++++++ .../support/QueryDslRepositorySupport.java | 104 ++++++ .../support/SimpleJpaRepository.java | 16 +- src/main/resources/changelog.txt | 1 + .../jpa/repository/sample/UserRepository.java | 3 +- .../JpaRepositoryFactoryUnitTests.java | 37 +- ...pleEntityPathResolverUnitTests_Sample.java | 26 ++ .../support/QueryDslJpaRepositoryTests.java | 103 ++++++ .../QueryDslRepositorySupportTests.java | 142 +++++++ .../SimpleEntityPathResolverUnitTests.java | 74 ++++ .../util/JpaClassUtilsUnitTests.java | 2 +- template.mf | 10 +- 17 files changed, 1077 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/QueryDslPredicateExecutor.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepository.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupport.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/support/QSimpleEntityPathResolverUnitTests_Sample.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepositoryTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupportTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/support/SimpleEntityPathResolverUnitTests.java diff --git a/pom.xml b/pom.xml index cc5126fd7..c704025b7 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,6 @@ 4.0.0 - org.springframework.data spring-data-jpa 1.0.0.BUILD-SNAPSHOT @@ -58,6 +57,7 @@ 2.0.0 2.2.0 1.6.8 + 2.1.1 4.8.1 2.0.0 1.6.1 @@ -385,6 +385,20 @@ + + + + com.mysema.querydsl + querydsl-apt + ${querydsl.version} + provided + + + + com.mysema.querydsl + querydsl-jpa + ${querydsl.version} + @@ -502,6 +516,24 @@ + + + com.mysema.maven + maven-apt-plugin + 1.0 + + + generate-test-sources + + test-process + + + target/generated-sources/test-annotations + com.mysema.query.apt.jpa.JPAAnnotationProcessor + + + + @@ -566,6 +598,11 @@ EclipseLink Repo http://www.eclipse.org/downloads/download.php?r=1&nf=1&file=/rt/eclipselink/maven.repo + + querydsl + QueryDsl + http://source.mysema.com/maven2/releases + @@ -575,5 +612,10 @@ Spring Framework Maven Release Repository http://maven.springframework.org/release + + querydsl + QueryDsl + http://source.mysema.com/maven2/releases + \ No newline at end of file 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 fdb5fa702..fda8f6231 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaRepository.java @@ -148,35 +148,6 @@ public interface JpaRepository extends Page findAll(Pageable pageable); - /** - * Returns a single entity matching the given {@link Specification}. - * - * @param spec - * @return - */ - T findOne(Specification spec); - - - /** - * Returns all entities matching the given {@link Specification}. - * - * @param spec - * @return - */ - List findAll(Specification spec); - - - /** - * Returns a {@link Page} of entities matching the given - * {@link Specification}. - * - * @param spec - * @param pageable - * @return - */ - Page findAll(Specification spec, Pageable pageable); - - /** * Flushes all pending changes to the database. */ diff --git a/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java new file mode 100644 index 000000000..c62bbecfe --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2008-2011 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.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; + + +/** + * Interface to allow execution of {@link Specification}s based on the JPA + * criteria API. + * + * @author Oliver Gierke + */ +public interface JpaSpecificationExecutor { + + /** + * Returns a single entity matching the given {@link Specification}. + * + * @param spec + * @return + */ + T findOne(Specification spec); + + + /** + * Returns all entities matching the given {@link Specification}. + * + * @param spec + * @return + */ + List findAll(Specification spec); + + + /** + * Returns a {@link Page} of entities matching the given + * {@link Specification}. + * + * @param spec + * @param pageable + * @return + */ + Page findAll(Specification spec, Pageable pageable); + + + /** + * Returns the number of instances that the given {@link Specification} will + * return. + * + * @param spec the {@link Specification} to count instances for + * @return the number of instances + */ + Long count(Specification spec); +} diff --git a/src/main/java/org/springframework/data/jpa/repository/QueryDslPredicateExecutor.java b/src/main/java/org/springframework/data/jpa/repository/QueryDslPredicateExecutor.java new file mode 100644 index 000000000..3c2b9394b --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/QueryDslPredicateExecutor.java @@ -0,0 +1,81 @@ +/* + * Copyright 2008-2011 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.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.mysema.query.types.OrderSpecifier; +import com.mysema.query.types.Predicate; + + +/** + * Interface to allow execution of QueryDsl {@link Predicate} instances. + * + * @author Oliver Gierke + */ +public interface QueryDslPredicateExecutor { + + /** + * Returns a single entity matching the given {@link Predicate}. + * + * @param spec + * @return + */ + T findOne(Predicate predicate); + + + /** + * Returns all entities matching the given {@link Predicate}. + * + * @param spec + * @return + */ + List findAll(Predicate predicate); + + + /** + * Returns all entities matching the given {@link Predicate} applying the + * given {@link OrderSpecifier}s. + * + * @param predicate + * @param orders + * @return + */ + List findAll(Predicate predicate, OrderSpecifier... orders); + + + /** + * Returns a {@link Page} of entities matching the given {@link Predicate}. + * + * @param predicate + * @param pageable + * @return + */ + Page findAll(Predicate predicate, Pageable pageable); + + + /** + * Returns the number of instances that the given {@link Predicate} will + * return. + * + * @param predicate the {@link Predicate} to count instances for + * @return the number of instances + */ + Long count(Predicate predicate); +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 6543dcb73..7b1ad8de4 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -19,6 +19,7 @@ import java.io.Serializable; import javax.persistence.EntityManager; +import org.springframework.data.jpa.repository.QueryDslPredicateExecutor; import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; import org.springframework.data.jpa.repository.query.QueryExtractor; import org.springframework.data.jpa.repository.utils.JpaClassUtils; @@ -27,6 +28,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.support.RepositoryFactorySupport; import org.springframework.data.repository.support.RepositoryMetadata; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** @@ -36,6 +38,10 @@ import org.springframework.util.Assert; */ public class JpaRepositoryFactory extends RepositoryFactorySupport { + private static final boolean QUERY_DSL_PRESENT = ClassUtils.isPresent( + "com.mysema.query.types.Predicate", + JpaRepositoryFactory.class.getClassLoader()); + private final EntityManager entityManager; private final QueryExtractor extractor; @@ -81,9 +87,15 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { protected Object getTargetRepository( RepositoryMetadata metadata, EntityManager entityManager) { - JpaEntityInformation entityMetadata = - getEntityInformation((Class) metadata.getDomainClass()); - return new SimpleJpaRepository(entityMetadata, entityManager); + Class repositoryInterface = metadata.getRepositoryInterface(); + JpaEntityInformation entityInformation = + getEntityInformation(metadata.getDomainClass()); + + if (isQueryDslExecutor(repositoryInterface)) { + return new QueryDslJpaRepository(entityInformation, entityManager); + } else { + return new SimpleJpaRepository(entityInformation, entityManager); + } } @@ -97,7 +109,26 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { @Override protected Class getRepositoryBaseClass(Class repositoryInterface) { - return SimpleJpaRepository.class; + if (isQueryDslExecutor(repositoryInterface)) { + return QueryDslJpaRepository.class; + } else { + return SimpleJpaRepository.class; + } + } + + + /** + * Returns whether the given repository interface requires a QueryDsl + * specific implementation to be chosen. + * + * @param repositoryInterface + * @return + */ + private boolean isQueryDslExecutor(Class repositoryInterface) { + + return QUERY_DSL_PRESENT + && QueryDslPredicateExecutor.class + .isAssignableFrom(repositoryInterface); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepository.java new file mode 100644 index 000000000..a282049eb --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepository.java @@ -0,0 +1,348 @@ +/* + * Copyright 2008-2011 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.repository.support; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; + +import javax.persistence.EntityManager; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.jpa.repository.QueryDslPredicateExecutor; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import com.mysema.query.jpa.JPQLQuery; +import com.mysema.query.jpa.impl.JPAQuery; +import com.mysema.query.types.EntityPath; +import com.mysema.query.types.Expression; +import com.mysema.query.types.OrderSpecifier; +import com.mysema.query.types.Predicate; +import com.mysema.query.types.path.PathBuilder; + + +/** + * QueryDsl specific extension of {@link SimpleJpaRepository} which adds + * implementation for {@link QueryDslPredicateExecutor}. + * + * @author Oliver Gierke + */ +public class QueryDslJpaRepository extends + SimpleJpaRepository implements QueryDslPredicateExecutor { + + private final EntityManager em; + private final EntityPath path; + private final PathBuilder builder; + + + /** + * Creates a new {@link QueryDslJpaRepository} from the given domain class + * and {@link EntityManager}. This will use the + * {@link SimpleEntityPathResolver} to translate the given domain class into + * an {@link EntityPath}. + * + * @param domainClass + * @param entityManager + */ + public QueryDslJpaRepository(JpaEntityInformation entityMetadata, + EntityManager entityManager) { + + this(entityMetadata, entityManager, SimpleEntityPathResolver.INSTANCE); + } + + + /** + * Creates a new {@link QueryDslJpaRepository} from the given domain class + * and {@link EntityManager} and uses the given {@link EntityPathResolver} + * to translate the domain class into an {@link EntityPath}. + * + * @param domainClass + * @param entityManager + * @param resolver + */ + public QueryDslJpaRepository(JpaEntityInformation entityMetadata, + EntityManager entityManager, EntityPathResolver resolver) { + + super(entityMetadata, entityManager); + this.em = entityManager; + this.path = resolver.createPath(entityMetadata.getJavaType()); + this.builder = new PathBuilder(path.getType(), path.getMetadata()); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.jpa.repository.querydsl. + * QueryDslSpecificationExecutor#findOne(com.mysema.query.types.Predicate) + */ + public T findOne(Predicate predicate) { + + return createQuery(predicate).uniqueResult(path); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.jpa.repository.querydsl. + * QueryDslSpecificationExecutor#findAll(com.mysema.query.types.Predicate) + */ + public List findAll(Predicate predicate) { + + return createQuery(predicate).list(path); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.jpa.repository.querydsl. + * QueryDslSpecificationExecutor#findAll(com.mysema.query.types.Predicate, + * com.mysema.query.types.OrderSpecifier[]) + */ + public List findAll(Predicate predicate, OrderSpecifier... orders) { + + return createQuery(predicate).orderBy(orders).list(path); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.jpa.repository.querydsl. + * QueryDslSpecificationExecutor#findAll(com.mysema.query.types.Predicate, + * org.springframework.data.domain.Pageable) + */ + public Page findAll(Predicate predicate, Pageable pageable) { + + JPQLQuery countQuery = createQuery(predicate); + JPQLQuery query = applyPagination(createQuery(predicate), pageable); + + return new PageImpl(query.list(path), pageable, countQuery.count()); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.jpa.repository.querydsl. + * QueryDslSpecificationExecutor#count(com.mysema.query.types.Predicate) + */ + public Long count(Predicate predicate) { + + return createQuery(predicate).count(); + } + + + /** + * Creates a new {@link JPQLQuery} for the given {@link Predicate}. + * + * @param predicate + * @return + */ + private JPQLQuery createQuery(Predicate... predicate) { + + return new JPAQuery(em).from(path).where(predicate); + } + + + /** + * Applies the given {@link Pageable} to the given {@link JPQLQuery}. + * + * @param query + * @param pageable + * @return + */ + private JPQLQuery applyPagination(JPQLQuery query, Pageable pageable) { + + if (pageable == null) { + return query; + } + + query.offset(pageable.getOffset()); + query.limit(pageable.getPageSize()); + + return applySorting(query, pageable.getSort()); + } + + + /** + * Applies sorting to the given {@link JPQLQuery}. + * + * @param query + * @param sort + * @return + */ + private JPQLQuery applySorting(JPQLQuery query, Sort sort) { + + if (sort == null) { + return query; + } + + for (Order order : sort) { + query.orderBy(toOrder(order)); + } + + return query; + } + + + /** + * Transforms a plain {@link Order} into a QueryDsl specific + * {@link OrderSpecifier}. + * + * @param order + * @return + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private OrderSpecifier toOrder(Order order) { + + Expression property = builder.get(order.getProperty()); + + return new OrderSpecifier( + order.isAscending() ? com.mysema.query.types.Order.ASC + : com.mysema.query.types.Order.DESC, property); + } + + /** + * Strategy interface to abstract the ways to translate an plain domain + * class into a {@link EntityPath}. + * + * @author Oliver Gierke + */ + public static interface EntityPathResolver { + + EntityPath createPath(Class domainClass); + } + + /** + * Simple implementation of {@link EntityPathResolver} to lookup a query + * class by reflection and using the static field of the same type. + * + * @author Oliver Gierke + */ + static enum SimpleEntityPathResolver implements EntityPathResolver { + + INSTANCE; + + private static final String NO_CLASS_FOUND_TEMPLATE = + "Did not find a query class %s for domain class %s!"; + private static final String NO_FIELD_FOUND_TEMPLATE = + "Did not find a static field of the same type in %s!"; + + + /** + * Creates an {@link EntityPath} instance for the given domain class. + * Tries to lookup a class matching the naming convention (prepend Q to + * the simple name of the class, same package) and find a static field + * of the same type in it. + * + * @param domainClass + * @return + */ + @SuppressWarnings("unchecked") + public EntityPath createPath(Class domainClass) { + + String pathClassName = getQueryClassName(domainClass); + + try { + Class pathClass = + ClassUtils.forName(pathClassName, + QueryDslJpaRepository.class.getClassLoader()); + Field field = getStaticFieldOfType(pathClass); + + if (field == null) { + throw new IllegalStateException(String.format( + NO_FIELD_FOUND_TEMPLATE, pathClass)); + } else { + return (EntityPath) ReflectionUtils + .getField(field, null); + } + + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(String.format( + NO_CLASS_FOUND_TEMPLATE, pathClassName, + domainClass.getName()), e); + } + } + + + /** + * Returns the first static field of the given type inside the given + * type. + * + * @param type + * @return + */ + private Field getStaticFieldOfType(Class type) { + + for (Field field : type.getDeclaredFields()) { + + boolean isStatic = Modifier.isStatic(field.getModifiers()); + boolean hasSameType = type.equals(field.getType()); + + if (isStatic && hasSameType) { + return field; + } + } + + Class superclass = type.getSuperclass(); + return Object.class.equals(superclass) ? null + : getStaticFieldOfType(superclass); + } + + + /** + * Returns the name of the query class for the given domain class. + * + * @param domainClass + * @return + */ + private String getQueryClassName(Class domainClass) { + + String simpleClassName = ClassUtils.getShortName(domainClass); + return String.format("%s.Q%s%s", + domainClass.getPackage().getName(), + getClassBase(simpleClassName), domainClass.getSimpleName()); + } + + + /** + * Analyzes the short class name and potentially returns the outer + * class. + * + * @param shortName + * @return + */ + private String getClassBase(String shortName) { + + String[] parts = shortName.split("\\."); + + if (parts.length < 2) { + return ""; + } + + return parts[0] + "_"; + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupport.java b/src/main/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupport.java new file mode 100644 index 000000000..f667db5d0 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupport.java @@ -0,0 +1,104 @@ +package org.springframework.data.jpa.repository.support; + +import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.beans.factory.annotation.Required; +import org.springframework.stereotype.Repository; +import org.springframework.util.Assert; + +import com.mysema.query.dml.DeleteClause; +import com.mysema.query.dml.UpdateClause; +import com.mysema.query.jpa.JPQLQuery; +import com.mysema.query.jpa.impl.JPADeleteClause; +import com.mysema.query.jpa.impl.JPAQuery; +import com.mysema.query.jpa.impl.JPAUpdateClause; +import com.mysema.query.types.EntityPath; +import com.mysema.query.types.path.PathBuilder; +import com.mysema.query.types.path.PathBuilderFactory; + + +/** + * Base class for implementing repositories using QueryDsl library. + * + * @author Oliver Gierke + */ +@Repository +public abstract class QueryDslRepositorySupport { + + @PersistenceContext + private EntityManager entityManager; + private PathBuilderFactory builderFactory = new PathBuilderFactory(); + + + /** + * Setter to inject {@link EntityManager}. + * + * @param entityManager must not be {@literal null} + */ + @Required + public void setEntityManager(EntityManager entityManager) { + + Assert.notNull(entityManager); + this.entityManager = entityManager; + } + + + /** + * Callback to verify configuration. Used by containers. + */ + @PostConstruct + public void validate() { + + Assert.notNull(entityManager, "EntityManager must not be null!"); + } + + + /** + * Returns a fresh {@link JPQLQuery}. + * + * @return + */ + protected JPQLQuery from(EntityPath... paths) { + + return new JPAQuery(entityManager).from(paths); + } + + + /** + * Returns a fresh {@link DeleteClause}. + * + * @param path + * @return + */ + protected DeleteClause delete(EntityPath path) { + + return new JPADeleteClause(entityManager, path); + } + + + /** + * Returns a fresh {@link UpdateClause}. + * + * @param path + * @return + */ + protected UpdateClause update(EntityPath path) { + + return new JPAUpdateClause(entityManager, path); + } + + + /** + * Returns a {@link PathBuilder} for the given type. + * + * @param + * @param type + * @return + */ + protected PathBuilder getBuilder(Class type) { + + return builderFactory.create(type); + } +} 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 4622f10a9..5e529b94b 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 @@ -35,6 +35,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.Repository; import org.springframework.util.Assert; @@ -50,7 +51,7 @@ import org.springframework.util.Assert; */ @org.springframework.stereotype.Repository public class SimpleJpaRepository implements - JpaRepository { + JpaRepository, JpaSpecificationExecutor { private final JpaEntityInformation entityInformation; private final EntityManager em; @@ -282,6 +283,19 @@ public class SimpleJpaRepository implements } + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.JpaSpecificationExecutor#count + * (org.springframework.data.jpa.domain.Specification) + */ + public Long count(Specification spec) { + + return getCountQuery(spec).getSingleResult(); + } + + /* * (non-Javadoc) * diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 1b5bea952..e1a441626 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -11,6 +11,7 @@ Changes in version 1.0.0.M2 * @Query annotated queries get validated on Query meta-model creation (DATAJPA-14) * Fixed dependency scopes and missing repository declarations (DATAJPA-33, DATAJPA-26) * Adapted meta-model API from Commons module (DATAJPA-32) +* Added support for QueryDsl (DATAJPA-8) Changes in version 1.0.0.M1 (2011-02-10) - https://jira.springsource.org/browse/DATAJPA/fixforversion/11786 ---------------------------------------- diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 6fc0e7861..94ddb7f77 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -25,6 +25,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; @@ -39,7 +40,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Oliver Gierke */ public interface UserRepository extends JpaRepository, - UserRepositoryCustom { + JpaSpecificationExecutor, UserRepositoryCustom { /** * Retrieve users by their lastname. The finder diff --git a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java index 9bd54ef76..ebd956815 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jpa.repository.support; import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.io.Serializable; @@ -27,10 +28,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.data.domain.Persistable; +import org.springframework.aop.framework.Advised; +import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.QueryDslPredicateExecutor; import org.springframework.data.jpa.repository.custom.CustomGenericJpaRepositoryFactory; import org.springframework.data.jpa.repository.custom.UserCustomExtendedRepository; +import org.springframework.transaction.annotation.Transactional; /** @@ -46,7 +50,7 @@ public class JpaRepositoryFactoryUnitTests { @Mock EntityManager entityManager; @Mock - JpaEntityInformation metadata; + JpaEntityInformation metadata; @Before @@ -60,7 +64,7 @@ public class JpaRepositoryFactoryUnitTests { public JpaEntityInformation getEntityInformation( Class domainClass) { - return (JpaEntityInformation) metadata; + return metadata; }; }; } @@ -134,7 +138,7 @@ public class JpaRepositoryFactoryUnitTests { @Test(expected = UnsupportedOperationException.class) - public void createsProxyWithCustomBaseClass() throws Exception { + public void createsProxyWithCustomBaseClass() { JpaRepositoryFactory factory = new CustomGenericJpaRepositoryFactory(entityManager); @@ -144,9 +148,25 @@ public class JpaRepositoryFactoryUnitTests { repository.customMethod(1); } + + @Test + public void usesQueryDslRepositoryIfInterfaceImplementsExecutor() { + + when(metadata.getJavaType()).thenReturn(User.class); + assertEquals(QueryDslJpaRepository.class, + factory.getRepositoryBaseClass(QueryDslSampleRepository.class)); + + QueryDslSampleRepository repository = + factory.getRepository(QueryDslSampleRepository.class); + assertEquals(QueryDslJpaRepository.class, + ((Advised) repository).getTargetClass()); + } + private interface SimpleSampleRepository extends JpaRepository { + @Transactional + User findOne(Integer id); } /** @@ -186,13 +206,8 @@ public class JpaRepositoryFactoryUnitTests { } - /** - * Helper class to make the factory use {@link PersistableMetadata} . - * - * @author Oliver Gierke - */ - @SuppressWarnings("serial") - private static abstract class User implements Persistable { + private interface QueryDslSampleRepository extends SimpleSampleRepository, + QueryDslPredicateExecutor { } } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QSimpleEntityPathResolverUnitTests_Sample.java b/src/test/java/org/springframework/data/jpa/repository/support/QSimpleEntityPathResolverUnitTests_Sample.java new file mode 100644 index 000000000..f5a4458f7 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/QSimpleEntityPathResolverUnitTests_Sample.java @@ -0,0 +1,26 @@ +/* + * Copyright 2011 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.repository.support; + +/** + * Stub for a generated QueryDsl query class. + * + * @author Oliver Gierke + */ +class QSimpleEntityPathResolverUnitTests_Sample { + + public QSimpleEntityPathResolverUnitTests_Sample field; +} diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepositoryTests.java new file mode 100644 index 000000000..188981fe4 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/QueryDslJpaRepositoryTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2008-2011 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.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.jpa.domain.sample.QUser; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import com.mysema.query.types.expr.BooleanExpression; +import com.mysema.query.types.path.PathBuilder; +import com.mysema.query.types.path.PathBuilderFactory; + + +/** + * Integration test for {@link QueryDslJpaRepository}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration({ "classpath:infrastructure.xml" }) +@Transactional +public class QueryDslJpaRepositoryTests { + + @PersistenceContext + EntityManager em; + + QueryDslJpaRepository repository; + QUser user = new QUser("user"); + User dave, carter; + + + @Before + public void setUp() { + + JpaEntityInformation information = + new JpaMetamodelEntityInformation(User.class, + em.getMetamodel()); + + repository = new QueryDslJpaRepository(information, em); + dave = + repository.save(new User("Dave", "Matthews", + "dave@matthews.com")); + carter = + repository.save(new User("Carter", "Beauford", + "carter@beauford.com")); + } + + + @Test + public void executesPredicatesCorrectly() throws Exception { + + BooleanExpression isCalledDave = user.firstname.eq("Dave"); + BooleanExpression isBeauford = user.lastname.eq("Beauford"); + + List result = repository.findAll(isCalledDave.or(isBeauford)); + + assertThat(result.size(), is(2)); + assertThat(result, hasItems(carter, dave)); + } + + + @Test + public void executesStringBasedPredicatesCorrectly() throws Exception { + + PathBuilder builder = new PathBuilderFactory().create(User.class); + + BooleanExpression isCalledDave = + builder.getString("firstname").eq("Dave"); + BooleanExpression isBeauford = + builder.getString("lastname").eq("Beauford"); + + List result = repository.findAll(isCalledDave.or(isBeauford)); + + assertThat(result.size(), is(2)); + assertThat(result, hasItems(carter, dave)); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupportTests.java b/src/test/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupportTests.java new file mode 100644 index 000000000..402232994 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/QueryDslRepositorySupportTests.java @@ -0,0 +1,142 @@ +package org.springframework.data.jpa.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.jpa.domain.sample.QUser; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + + +/** + * Integration test for {@link QueryDslRepositorySupport}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration({ "classpath:infrastructure.xml" }) +@Transactional +public class QueryDslRepositorySupportTests { + + @PersistenceContext + EntityManager em; + + UserRepository repository; + User dave, carter; + + + @Before + public void setup() { + + dave = new User("Dave", "Matthews", "dave@matthews.com"); + em.persist(dave); + + carter = new User("Carter", "Beauford", "carter@beauford.com"); + em.persist(carter); + + UserRepositoryImpl repository = new UserRepositoryImpl(); + repository.setEntityManager(em); + repository.validate(); + + this.repository = repository; + } + + + @Test + public void readsUsersCorrectly() throws Exception { + + List result = repository.findUsersByLastname("Matthews"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(dave)); + + result = repository.findUsersByLastname("Beauford"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(carter)); + } + + + @Test + public void updatesUsersCorrectly() throws Exception { + + long updates = repository.updateLastnamesTo("Foo"); + assertThat(updates, is(2L)); + + List result = repository.findUsersByLastname("Matthews"); + assertThat(result.size(), is(0)); + + result = repository.findUsersByLastname("Beauford"); + assertThat(result.size(), is(0)); + + result = repository.findUsersByLastname("Foo"); + assertThat(result.size(), is(2)); + assertThat(result, hasItems(dave, carter)); + } + + + @Test + public void deletesAllWithLastnameCorrectly() throws Exception { + + long updates = repository.deleteAllWithLastname("Matthews"); + assertThat(updates, is(1L)); + + List result = repository.findUsersByLastname("Matthews"); + assertThat(result.size(), is(0)); + + result = repository.findUsersByLastname("Beauford"); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is(carter)); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsUnsetEntityManager() throws Exception { + + UserRepositoryImpl repositoryImpl = new UserRepositoryImpl(); + repositoryImpl.validate(); + } + + private static interface UserRepository { + + List findUsersByLastname(String firstname); + + + long updateLastnamesTo(String lastname); + + + long deleteAllWithLastname(String lastname); + } + + private static class UserRepositoryImpl extends QueryDslRepositorySupport + implements UserRepository { + + private static final QUser user = QUser.user; + + + public List findUsersByLastname(String lastname) { + + return from(user).where(user.lastname.eq(lastname)).list(user); + } + + + public long updateLastnamesTo(String lastname) { + + return update(user).set(user.lastname, lastname).execute(); + } + + + public long deleteAllWithLastname(String lastname) { + + return delete(user).where(user.lastname.eq(lastname)).execute(); + } + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/support/SimpleEntityPathResolverUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/SimpleEntityPathResolverUnitTests.java new file mode 100644 index 000000000..1745c2e53 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/support/SimpleEntityPathResolverUnitTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2008-2011 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.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.jpa.domain.sample.QUser; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.support.QueryDslJpaRepository.EntityPathResolver; +import org.springframework.data.jpa.repository.support.QueryDslJpaRepository.SimpleEntityPathResolver; +import org.springframework.data.jpa.repository.util.JpaClassUtilsUnitTests.NamedUser; +import org.springframework.data.jpa.repository.util.QJpaClassUtilsUnitTests_NamedUser; + + +/** + * Unit test for {@link SimpleEntityPathResolver}. + * + * @author Oliver Gierke + */ +public class SimpleEntityPathResolverUnitTests { + + EntityPathResolver resolver = + QueryDslJpaRepository.SimpleEntityPathResolver.INSTANCE; + + + @Test + public void createsRepositoryFromDomainClassCorrectly() throws Exception { + + assertThat(resolver.createPath(User.class), is(QUser.class)); + } + + + @Test + public void resolvesEntityPathForInnerClassCorrectly() throws Exception { + + assertThat(resolver.createPath(NamedUser.class), + is(QJpaClassUtilsUnitTests_NamedUser.class)); + } + + + @Test(expected = IllegalStateException.class) + public void rejectsFoundClassWithoutStaticFieldOfSameType() + throws Exception { + + resolver.createPath(Sample.class); + } + + + @Test(expected = IllegalArgumentException.class) + public void rejectsClassWithoutQueryClassConfrmingToTheNamingScheme() + throws Exception { + + resolver.createPath(QSimpleEntityPathResolverUnitTests_Sample.class); + } + + static class Sample { + + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/util/JpaClassUtilsUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/util/JpaClassUtilsUnitTests.java index 4b7f99cc4..206cc5f1b 100644 --- a/src/test/java/org/springframework/data/jpa/repository/util/JpaClassUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/util/JpaClassUtilsUnitTests.java @@ -40,7 +40,7 @@ public class JpaClassUtilsUnitTests { } @Entity(name = "AnotherNamedUser") - static class NamedUser { + public static class NamedUser { } } diff --git a/template.mf b/template.mf index 9306dc34f..00f806a4d 100644 --- a/template.mf +++ b/template.mf @@ -6,15 +6,17 @@ Bundle-Version: ${project.version} Bundle-RequiredExecutionEnvironment: J2SE-1.5 Export-Template: org.springframework.data.jpa.*;version="${project.version}" -Import-Template: - javax.persistence.*;version="${jpa.version:[=.=.=,+1.0.0)}", +Import-Template: + com.mysema.query.*;version="${querydsl.version:[=.=.=,+1.0.0)}", + javax.persistence.*;version="${jpa.version:[=.=.=,+1.0.0)}", + javax.annotation.*;version="[1.3.0,2.0.0)", org.aopalliance.*;version="[1.0.0,2.0.0)", - org.slf4j.*;version="${slf4j.version:[=.=.=,+1.0.0)}", + org.apache.openjpa.persistence.*;version="${openjpa.version:[=.=.=,+1.0.0)}";resolution:=optional, org.aspectj.*;version="${aspectj.version:[=.=.=,+1.0.0)}";resolution:=optional, org.eclipse.persistence.*;version="${eclipselink.version:[=.=.=,+1.0.0)}";resolution:=optional, org.hibernate.*;version="[3.5.3,4.0.0)";resolution:=optional, - org.apache.openjpa.persistence.*;version="${openjpa.version:[=.=.=,+1.0.0)}";resolution:=optional, org.joda.time.*;version="[1.5.0,2.0.0)";resolution:=optional, + org.slf4j.*;version="${slf4j.version:[=.=.=,+1.0.0)}", org.springframework.*;version="${spring.version:[=.=.=.=,+1.0.0)}", org.springframework.beans.factory.aspectj;version="${spring.version:[=.=.=.=,+1.0.0)}";resolution:=optional, org.w3c.*;version="0.0.0"