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"