From 1443b155db009a86ff91c217c0aeb6c55c6559df Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 1 Apr 2015 14:19:08 +0200 Subject: [PATCH] DATAJPA-696 - Support ad-hoc entity graph definitions for repository finder methods. We now support the specification of ad-hoc entity graphs on repository finder methods by allowing to specify the fetch graph paths via the "attributePaths" attribute on the EntityGraph annotation. Configured EclipseLink tests to use dynamic weaving, required for dynamic entity fetch graphs. Fixed typo in CrudMethodMetadataPopulatingMethodInterceptor. Original pull request: #140. --- pom.xml | 2 +- .../jpa/provider/PersistenceProvider.java | 80 ++++++++++++++++++- .../data/jpa/repository/EntityGraph.java | 25 +++++- .../repository/query/AbstractJpaQuery.java | 2 +- .../data/jpa/repository/query/Jpa21Utils.java | 19 ++++- .../jpa/repository/query/JpaEntityGraph.java | 50 ++++++++++-- .../jpa/repository/query/JpaQueryMethod.java | 5 +- .../support/CrudMethodMetadata.java | 16 +++- .../CrudMethodMetadataPostProcessor.java | 35 ++++---- .../support/SimpleJpaRepository.java | 16 +++- .../data/jpa/domain/sample/User.java | 2 + .../PersistenceProviderUnitTests.java | 45 +++++++++-- ...raphRepositoryMethodsIntegrationTests.java | 46 ++++++++--- .../query/JpaQueryMethodUnitTests.java | 35 +++++++- ...odsWithEntityGraphConfigJpaRepository.java | 12 +++ ...aPopulatingMethodInterceptorUnitTests.java | 6 +- .../support/SimpleJpaRepositoryUnitTests.java | 22 +++-- 17 files changed, 355 insertions(+), 63 deletions(-) diff --git a/pom.xml b/pom.xml index 3e76b546e..d674e070a 100644 --- a/pom.xml +++ b/pom.xml @@ -326,7 +326,7 @@ **/EclipseLink*Tests.java - -javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar + -javaagent:${settings.localRepository}/org/eclipse/persistence/org.eclipse.persistence.jpa/${eclipselink}/org.eclipse.persistence.jpa-${eclipselink}.jar -javaagent:${settings.localRepository}/org/springframework/spring-instrument/${spring}/spring-instrument-${spring}.jar diff --git a/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java b/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java index eb5020ce2..a2278e11d 100644 --- a/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java +++ b/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java @@ -15,8 +15,17 @@ */ package org.springframework.data.jpa.provider; -import static org.springframework.data.jpa.provider.JpaClassUtils.*; -import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; +import static org.springframework.data.jpa.provider.JpaClassUtils.isEntityManagerOfType; +import static org.springframework.data.jpa.provider.JpaClassUtils.isMetamodelOfType; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.ECLIPSELINK_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.ECLIPSELINK_JPA_METAMODEL_TYPE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.GENERIC_JPA_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE43_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE43_JPA_METAMODEL_TYPE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE_JPA_METAMODEL_TYPE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.OPENJPA_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.OPENJPA_JPA_METAMODEL_TYPE; import java.util.Arrays; import java.util.Collection; @@ -24,8 +33,10 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import javax.persistence.EntityGraph; import javax.persistence.EntityManager; import javax.persistence.Query; +import javax.persistence.Subgraph; import javax.persistence.metamodel.Metamodel; import org.apache.openjpa.enhance.PersistenceCapable; @@ -42,10 +53,12 @@ import org.hibernate.ScrollableResults; import org.hibernate.ejb.HibernateQuery; import org.hibernate.proxy.HibernateProxy; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.data.util.CloseableIterator; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Enumeration representing persistence providers to be used. @@ -354,6 +367,69 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor { + name()); } + /** + * Creates a dynamic {@link EntityGraph} from the given {@link JpaEntityGraph} information. + * + * @param em + * @param jpaEntityGraph + * @param entityType + * @return + * + * @since 1.9 + */ + public EntityGraph createDynamicEntityGraph(EntityManager em, JpaEntityGraph jpaEntityGraph, Class entityType) { + + Assert.isTrue(jpaEntityGraph.isDynamicEntityGraph(), "The given " + jpaEntityGraph + " is not dynamic!"); + + EntityGraph entityGraph = em.createEntityGraph(entityType); + + configureFetchGraphFrom(jpaEntityGraph, entityGraph); + + return entityGraph; + } + + + /** + * Configures the given {@link EntityGraph} with the fetch graph information stored in {@link JpaEntityGraph}. + * + * @param jpaEntityGraph + * @param entityGraph + */ + /* visible for testing */ + void configureFetchGraphFrom(JpaEntityGraph jpaEntityGraph, EntityGraph entityGraph) { + + String[] attributePaths = jpaEntityGraph.getAttributePaths().clone(); + + // sort to ensure that the intermediate entity subgraphs are created accordingly. + Arrays.sort(attributePaths); + + // we build the entity graph based on the paths with highest depth first + for (int i = attributePaths.length - 1; i >= 0; i--) { + + String path = attributePaths[i]; + + //fast path just single attribute + if (!path.contains(".")) { + entityGraph.addAttributeNodes(path); + continue; + } + + //we need to build nested sub fetch graphs + String[] pathComponents = StringUtils.delimitedListToStringArray(path, "."); + + Subgraph parent = null; + for (int c = 0; c < pathComponents.length - 1; c++) { + + if (c == 0) { + parent = entityGraph.addSubgraph(pathComponents[c]); + } else { + parent = parent.addSubgraph(pathComponents[c]); + } + } + parent.addAttributeNodes(pathComponents[pathComponents.length - 1]); + } + } + /** * {@link CloseableIterator} for Hibernate. * diff --git a/src/main/java/org/springframework/data/jpa/repository/EntityGraph.java b/src/main/java/org/springframework/data/jpa/repository/EntityGraph.java index d8dddd254..7a2bc5452 100644 --- a/src/main/java/org/springframework/data/jpa/repository/EntityGraph.java +++ b/src/main/java/org/springframework/data/jpa/repository/EntityGraph.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -21,9 +21,19 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.persistence.NamedAttributeNode; + +import org.springframework.data.jpa.repository.query.JpaQueryMethod; + /** * Annotation to configure the JPA 2.1 {@link javax.persistence.EntityGraph}s that should be used on repository methods. * + * Since 1.9 we support the definition of dynamic {@link EntityGraph}s by allowing to customize the fetch-graph via + * via {@link #attributePaths()} ad-hoc fetch-graph configuration. + * + * If {@link #attributePaths()} are specified then we ignore the entity-graph name {@link #value()} + * and treat this {@link EntityGraph} as dynamic. + * * @author Thomas Darimont * @since 1.6 */ @@ -34,10 +44,11 @@ public @interface EntityGraph { /** * The name of the EntityGraph to use. + * If empty we fall-back to {@link JpaQueryMethod#getNamedQueryName()} as the value. * * @return */ - String value(); + String value() default ""; /** * The {@link Type} of the EntityGraph to use, defaults to {@link Type#FETCH}. @@ -45,6 +56,16 @@ public @interface EntityGraph { * @return */ EntityGraphType type() default EntityGraphType.FETCH; + + /** + * The paths of attributes of this {@link EntityGraph} to use, empty by default. + * + * You can refer to direct properties of the entity or nested properties via a {@code property.nestedProperty}. + * + * @return + * @since 1.9 + */ + String[] attributePaths() default {}; /** * Enum for JPA 2.1 {@link javax.persistence.EntityGraph} types. diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index 41269876e..d87ee193b 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -182,7 +182,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { Assert.notNull(query, "Query must not be null!"); Assert.notNull(method, "JpaQueryMethod must not be null!"); - Map hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph()); + Map hints = Jpa21Utils.tryGetFetchGraphHints(em, method.getEntityGraph(), getQueryMethod().getEntityInformation().getJavaType()); for (Map.Entry hint : hints.entrySet()) { query.setHint(hint.getKey(), hint.getValue()); diff --git a/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java b/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java index 63793443a..87aa84fbf 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java @@ -23,6 +23,7 @@ import javax.persistence.EntityGraph; import javax.persistence.EntityManager; import javax.persistence.Query; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -59,16 +60,18 @@ public class Jpa21Utils { * @param em must not be {@literal null} * @param query must not be {@literal null} * @param entityGraph can be {@literal null} + * @param entityType must not be {@literal null} * @return a {@code Map} with the hints or an empty {@code Map} if no hints were found * @since 1.8 */ - public static Map tryGetFetchGraphHints(EntityManager em, JpaEntityGraph entityGraph) { + public static Map tryGetFetchGraphHints(EntityManager em, JpaEntityGraph entityGraph, + Class entityType) { if (entityGraph == null) { return Collections.emptyMap(); } - EntityGraph graph = tryGetFetchGraph(em, entityGraph); + EntityGraph graph = tryGetFetchGraph(em, entityGraph, entityType); if (graph == null) { return Collections.emptyMap(); @@ -83,17 +86,25 @@ public class Jpa21Utils { * @see JPA 2.1 Specfication 3.7.4 - Use of Entity Graphs in find and query operations P.117 * @param em must not be {@literal null}. * @param jpaEntityGraph must not be {@literal null}. + * @param entityType must not be {@literal null}. * @return the {@link EntityGraph} described by the given {@code entityGraph}. */ - public static EntityGraph tryGetFetchGraph(EntityManager em, JpaEntityGraph jpaEntityGraph) { + private static EntityGraph tryGetFetchGraph(EntityManager em, JpaEntityGraph jpaEntityGraph, Class entityType) { Assert.notNull(em, "EntityManager must not be null!"); Assert.notNull(jpaEntityGraph, "EntityGraph must not be null!"); + Assert.notNull(entityType, "EntityType must not be null!"); Assert.isTrue(JPA21_AVAILABLE, "The EntityGraph-Feature requires at least a JPA 2.1 persistence provider!"); Assert.isTrue(GET_ENTITY_GRAPH_METHOD != null, "It seems that you have the JPA 2.1 API but a JPA 2.0 implementation on the classpath!"); - return em.getEntityGraph(jpaEntityGraph.getName()); + try { + // first check whether an entityGraph with that name is already registered. + return em.getEntityGraph(jpaEntityGraph.getName()); + } catch (Exception ex) { + // try to create and dynamically register the entityGraph + return PersistenceProvider.fromEntityManager(em).createDynamicEntityGraph(em, jpaEntityGraph, entityType); + } } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java index 47c56f6ac..877ceb42a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaEntityGraph.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -15,10 +15,12 @@ */ package org.springframework.data.jpa.repository.query; -import javax.persistence.EntityGraph; +import java.util.Arrays; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * EntityGraph configuration for JPA 2.1 {@link EntityGraph}s. @@ -28,22 +30,38 @@ import org.springframework.util.Assert; */ public class JpaEntityGraph { + private static String[] EMPTY_ATTRIBUTE_PATHS = {}; + private final String name; private final EntityGraphType type; + private final String[] attributePaths; /** * Creates an {@link JpaEntityGraph}. * - * @param name must not be {@null} or empty. - * @param type must not be {@null}. + * @param entityGraph must not be {@literal null}. + * @param nameFallback must not be {@literal null} or empty. */ - public JpaEntityGraph(String name, EntityGraphType type) { + public JpaEntityGraph(EntityGraph entityGraph, String nameFallback) { + this(StringUtils.hasText(entityGraph.value()) ? entityGraph.value() : nameFallback, entityGraph.type(), entityGraph + .attributePaths()); + } + + /** + * Creates an {@link JpaEntityGraph}. + * + * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. + * @param attributePaths may be {@literal null}. + */ + public JpaEntityGraph(String name, EntityGraphType type, String[] attributePaths) { Assert.hasText(name, "The name of an EntityGraph must not be null or empty!"); Assert.notNull(type, "FetchGraphType must not be null!"); this.name = name; this.type = type; + this.attributePaths = attributePaths == null ? EMPTY_ATTRIBUTE_PATHS : attributePaths; } /** @@ -64,12 +82,32 @@ public class JpaEntityGraph { return type; } + /** + * Returns the attribute node names to be used for this {@link JpaEntityGraph}. + * + * @return + * @since 1.9 + */ + public String[] getAttributePaths() { + return attributePaths; + } + + /** + * Return {@literal true} if this {@link JpaEntityGraph} needs to be generated on-the-fly. + * + * @return + */ + public boolean isDynamicEntityGraph() { + return this.attributePaths.length > 0; + } + /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { - return "JpaEntityGraph [name=" + name + ", type=" + type + "]"; + return "JpaEntityGraph [name=" + name + ", type=" + type + ", attributePaths=" + Arrays.toString(attributePaths) + + "]"; } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index 084c5e30b..5416a5850 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -15,7 +15,8 @@ */ package org.springframework.data.jpa.repository.query; -import static org.springframework.core.annotation.AnnotationUtils.*; +import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; +import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; import java.lang.reflect.Method; import java.util.ArrayList; @@ -173,7 +174,7 @@ public class JpaQueryMethod extends QueryMethod { JpaEntityGraph getEntityGraph() { EntityGraph annotation = findAnnotation(method, EntityGraph.class); - return annotation == null ? null : new JpaEntityGraph(annotation.value(), annotation.type()); + return annotation == null ? null : new JpaEntityGraph(annotation, getNamedQueryName()); } /** diff --git a/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java b/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java index af4679414..93b915f9f 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadata.java @@ -15,10 +15,12 @@ */ package org.springframework.data.jpa.repository.support; +import java.lang.reflect.Method; import java.util.Map; import javax.persistence.LockModeType; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.JpaEntityGraph; /** @@ -45,10 +47,18 @@ public interface CrudMethodMetadata { Map getQueryHints(); /** - * Returns the {@link JpaEntityGraph} to be used. + * Returns the {@link EntityGraph} to be used. * * @return - * @since 1.8 + * @since 1.9 */ - JpaEntityGraph getEntityGraph(); + EntityGraph getEntityGraph(); + + /** + * Returns the {@link Method} to be used. + * + * @return + * @since 1.9 + */ + Method getMethod(); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java b/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java index 5c524c48d..bf4394a5c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPostProcessor.java @@ -37,6 +37,7 @@ import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; +import org.springframework.data.util.ReflectionUtils; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -60,7 +61,7 @@ enum CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { factory.addAdvice(ExposeInvocationInterceptor.INSTANCE); - factory.addAdvice(CrudMethodMetadataPopulatingMethodIntercceptor.INSTANCE); + factory.addAdvice(CrudMethodMetadataPopulatingMethodInterceptor.INSTANCE); } /** @@ -85,7 +86,7 @@ enum CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { * @author Oliver Gierke * @author Thomas Darimont */ - static enum CrudMethodMetadataPopulatingMethodIntercceptor implements MethodInterceptor { + static enum CrudMethodMetadataPopulatingMethodInterceptor implements MethodInterceptor { INSTANCE; @@ -136,7 +137,8 @@ enum CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { private final LockModeType lockModeType; private final Map queryHints; - private final JpaEntityGraph entityGraph; + private final EntityGraph entityGraph; + private final Method method; /** * Creates a new {@link DefaultCrudMethodMetadata} for the given {@link Method}. @@ -150,13 +152,11 @@ enum CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { this.lockModeType = findLockModeType(method); this.queryHints = findQueryHints(method); this.entityGraph = findEntityGraph(method); + this.method = method; } - private static JpaEntityGraph findEntityGraph(Method method) { - - EntityGraph entityGraphAnnotation = AnnotationUtils.findAnnotation(method, EntityGraph.class); - return entityGraphAnnotation == null ? null : new JpaEntityGraph(entityGraphAnnotation.value(), - entityGraphAnnotation.type()); + private static EntityGraph findEntityGraph(Method method) { + return AnnotationUtils.findAnnotation(method, EntityGraph.class); } private static LockModeType findLockModeType(Method method) { @@ -203,14 +203,21 @@ enum CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor { public Map getQueryHints() { return queryHints; } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getEntityGraphHint() + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getEntityGraph() */ @Override - public JpaEntityGraph getEntityGraph() { - return this.entityGraph; + public EntityGraph getEntityGraph() { + return entityGraph; + } + + /* (non-Javadoc) + * @see org.springframework.data.jpa.repository.support.CrudMethodMetadata#getMethod() + */ + @Override + public Method getMethod() { + return method; } } 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 8f6797395..6111a0157 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 @@ -15,7 +15,11 @@ */ package org.springframework.data.jpa.repository.support; -import static org.springframework.data.jpa.repository.query.QueryUtils.*; +import static org.springframework.data.jpa.repository.query.QueryUtils.COUNT_QUERY_STRING; +import static org.springframework.data.jpa.repository.query.QueryUtils.DELETE_ALL_QUERY_STRING; +import static org.springframework.data.jpa.repository.query.QueryUtils.applyAndBind; +import static org.springframework.data.jpa.repository.query.QueryUtils.getQueryString; +import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders; import java.io.Serializable; import java.util.ArrayList; @@ -49,6 +53,7 @@ import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.query.Jpa21Utils; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @@ -248,11 +253,18 @@ public class SimpleJpaRepository implements JpaRepos Map hints = new HashMap(); hints.putAll(metadata.getQueryHints()); - hints.putAll(Jpa21Utils.tryGetFetchGraphHints(em, metadata.getEntityGraph())); + + hints.putAll(Jpa21Utils.tryGetFetchGraphHints(em, getEntityGraph(), getDomainClass())); return hints; } + private JpaEntityGraph getEntityGraph() { + + String fallbackName = this.entityInformation.getEntityName() + "." + metadata.getMethod().getName(); + return new JpaEntityGraph(metadata.getEntityGraph(), fallbackName); + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#getOne(java.io.Serializable) 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 3ca89ed80..962972c46 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 @@ -52,6 +52,8 @@ import javax.persistence.TemporalType; @NamedEntityGraphs({ @NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }), @NamedEntityGraph(name = "User.detail", attributeNodes = { @NamedAttributeNode("roles"), + @NamedAttributeNode("manager"), @NamedAttributeNode("colleagues") }), + @NamedEntityGraph(name = "User.getOneWithDefinedEntityGraphById", attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode("manager"), @NamedAttributeNode("colleagues") }) }) @NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1") @NamedStoredProcedureQueries({ // diff --git a/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java index 41f809bd6..24039fca1 100644 --- a/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java @@ -15,22 +15,37 @@ */ package org.springframework.data.jpa.provider; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.springframework.data.jpa.provider.PersistenceProvider.*; -import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.data.jpa.provider.PersistenceProvider.ECLIPSELINK; +import static org.springframework.data.jpa.provider.PersistenceProvider.GENERIC_JPA; +import static org.springframework.data.jpa.provider.PersistenceProvider.HIBERNATE; +import static org.springframework.data.jpa.provider.PersistenceProvider.OPEN_JPA; +import static org.springframework.data.jpa.provider.PersistenceProvider.fromEntityManager; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.ECLIPSELINK_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE43_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.HIBERNATE_ENTITY_MANAGER_INTERFACE; +import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.OPENJPA_ENTITY_MANAGER_INTERFACE; import java.util.ArrayList; import java.util.List; +import javax.persistence.EntityGraph; import javax.persistence.EntityManager; +import javax.persistence.Subgraph; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.springframework.asm.ClassWriter; import org.springframework.asm.Opcodes; -import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.instrument.classloading.ShadowingClassLoader; import org.springframework.util.ClassUtils; @@ -103,6 +118,26 @@ public class PersistenceProviderUnitTests { assertThat(fromEntityManager(em), is(GENERIC_JPA)); } + /** + * @see DATAJPA-696 + */ + @Test + public void shouldBuildCorrectSubgraphForJpaEntityGraph() throws Exception { + + EntityGraph entityGraph = mock(EntityGraph.class); + Subgraph subgraph = mock(Subgraph.class); + doReturn(subgraph).when(entityGraph).addSubgraph(anyString()); + + JpaEntityGraph jpaEntityGraph = new JpaEntityGraph("foo", EntityGraphType.FETCH, + new String[] { "foo", "gugu.gaga" }); + + PersistenceProvider.GENERIC_JPA.configureFetchGraphFrom(jpaEntityGraph, entityGraph); + + verify(entityGraph, times(1)).addAttributeNodes("foo"); + verify(entityGraph, times(1)).addSubgraph("gugu"); + verify(subgraph, times(1)).addAttributeNodes("gaga"); + } + private EntityManager mockProviderSpecificEntityManagerInterface(String interfaceName) throws ClassNotFoundException { Class providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader, diff --git a/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java index 9dc459f30..bc2ba63d3 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-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. @@ -60,6 +60,10 @@ public class EntityGraphRepositoryMethodsIntegrationTests { role = new Role("Developer"); em.persist(role); tom.getRoles().add(role); + tom = repository.save(tom); + + olli = repository.save(olli); + tom.getColleagues().add(olli); } /** @@ -70,11 +74,9 @@ public class EntityGraphRepositoryMethodsIntegrationTests { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); - tom = repository.save(tom); - List result = repository.findAll(); - assertThat(result.size(), is(1)); + assertThat(result.size(), is(2)); assertThat(Persistence.getPersistenceUtil().isLoaded(result.get(0).getRoles()), is(true)); assertThat(result.get(0), is(tom)); } @@ -87,16 +89,40 @@ public class EntityGraphRepositoryMethodsIntegrationTests { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); - olli = repository.save(olli); - tom.getColleagues().add(olli); - tom = repository.save(tom); - - em.flush(); - User user = repository.findOne(tom.getId()); assertThat(user, is(notNullValue())); assertThat("colleages should be fetched with 'user.detail' fetchgraph", Persistence.getPersistenceUtil().isLoaded(user.getColleagues()), is(true)); } + + /** + * @see DATAJPA-696 + */ + @Test + public void shouldRespectInferFetchGraphFromMethodName() { + + Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + User user = repository.getOneWithDefinedEntityGraphById(tom.getId()); + + assertThat(user, is(notNullValue())); + assertThat("colleages should be fetched with 'user.detail' fetchgraph", + Persistence.getPersistenceUtil().isLoaded(user.getColleagues()), is(true)); + } + + /** + * @see DATAJPA-696 + */ + @Test + public void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() { + + Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + User user = repository.getOneWithAttributeNamesById(tom.getId()); + + assertThat(user, is(notNullValue())); + assertThat("colleages should be fetched with 'user.detail' fetchgraph", + Persistence.getPersistenceUtil().isLoaded(user.getColleagues()), is(true)); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java index 7bd7ebfe3..29672d482 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryMethodUnitTests.java @@ -324,6 +324,9 @@ public class JpaQueryMethodUnitTests { */ @Test public void shouldStoreJpa21FetchGraphInformationAsHint() { + + doReturn(User.class).when(metadata).getDomainType(); + doReturn(User.class).when(metadata).getReturnedDomainClass(queryMethodWithCustomEntityFetchGraph); JpaQueryMethod method = new JpaQueryMethod(queryMethodWithCustomEntityFetchGraph, metadata, extractor); @@ -338,6 +341,9 @@ public class JpaQueryMethodUnitTests { @Test public void shouldFindEntityGraphAnnotationOnOverriddenSimpleJpaRepositoryMethod() throws Exception { + doReturn(User.class).when(metadata).getDomainType(); + doReturn(User.class).when(metadata).getReturnedDomainClass((Method)any()); + JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("findAll"), metadata, extractor); assertThat(method.getEntityGraph(), is(notNullValue())); @@ -351,12 +357,31 @@ public class JpaQueryMethodUnitTests { @Test public void shouldFindEntityGraphAnnotationOnOverriddenSimpleJpaRepositoryMethodFindOne() throws Exception { - JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("findOne"), metadata, extractor); + doReturn(User.class).when(metadata).getDomainType(); + doReturn(User.class).when(metadata).getReturnedDomainClass((Method)any()); + + JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("findOne", Long.class), metadata, extractor); assertThat(method.getEntityGraph(), is(notNullValue())); assertThat(method.getEntityGraph().getName(), is("User.detail")); assertThat(method.getEntityGraph().getType(), is(EntityGraphType.FETCH)); } + + /** + * DATAJPA-696 + */ + @Test + public void shouldFindEntityGraphAnnotationOnQueryMethodGetOneByWithDerivedName() throws Exception { + + doReturn(User.class).when(metadata).getDomainType(); + doReturn(User.class).when(metadata).getReturnedDomainClass((Method)any()); + + JpaQueryMethod method = new JpaQueryMethod(JpaRepositoryOverride.class.getMethod("getOneById", Long.class), metadata, extractor); + + assertThat(method.getEntityGraph(), is(notNullValue())); + assertThat(method.getEntityGraph().getName(), is("User.getOneById")); + assertThat(method.getEntityGraph().getType(), is(EntityGraphType.FETCH)); + } /** * Interface to define invalid repository methods for testing. @@ -433,7 +458,13 @@ public class JpaQueryMethodUnitTests { * DATAJPA-689 */ @EntityGraph("User.detail") - User findOne(); + User findOne(Long id); + + /** + * DATAJPA-696 + */ + @EntityGraph + User getOneById(Long id); } @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigJpaRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigJpaRepository.java index a10c65a8e..6b24249e9 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigJpaRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigJpaRepository.java @@ -41,4 +41,16 @@ public interface RepositoryMethodsWithEntityGraphConfigJpaRepository extends Jpa */ @EntityGraph(type = EntityGraphType.FETCH, value = "User.detail") User findOne(Integer id); + + /** + * @see DATAJPA-696 + */ + @EntityGraph + User getOneWithDefinedEntityGraphById(Integer id); + + /** + * @see DATAJPA-696 + */ + @EntityGraph(attributePaths = { "roles", "colleagues.roles" }) + User getOneWithAttributeNamesById(Integer id); } diff --git a/src/test/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPopulatingMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPopulatingMethodInterceptorUnitTests.java index 452b6a2c3..fbcc8d605 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPopulatingMethodInterceptorUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/CrudMethodMetadataPopulatingMethodInterceptorUnitTests.java @@ -29,11 +29,11 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor.CrudMethodMetadataPopulatingMethodIntercceptor; +import org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor.CrudMethodMetadataPopulatingMethodInterceptor; import org.springframework.transaction.support.TransactionSynchronizationManager; /** - * Unit tests for {@link CrudMethodMetadataPopulatingMethodIntercceptor}. + * Unit tests for {@link CrudMethodMetadataPopulatingMethodInterceptor}. * * @author Oliver Gierke */ @@ -51,7 +51,7 @@ public class CrudMethodMetadataPopulatingMethodInterceptorUnitTests { Method method = Sample.class.getMethod("someMethod"); when(invocation.getMethod()).thenReturn(method); - CrudMethodMetadataPopulatingMethodIntercceptor interceptor = CrudMethodMetadataPopulatingMethodIntercceptor.INSTANCE; + CrudMethodMetadataPopulatingMethodInterceptor interceptor = CrudMethodMetadataPopulatingMethodInterceptor.INSTANCE; interceptor.invoke(invocation); assertThat(TransactionSynchronizationManager.getResource(method), is(nullValue())); diff --git a/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java index 070f5a1c1..2536fb363 100644 --- a/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java @@ -15,8 +15,12 @@ */ package org.springframework.data.jpa.repository.support; -import static java.util.Collections.*; -import static org.mockito.Mockito.*; +import static java.util.Collections.singletonMap; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.Serializable; import javax.persistence.EntityGraph; import javax.persistence.EntityManager; @@ -33,7 +37,7 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; -import org.springframework.data.jpa.repository.query.JpaEntityGraph; +import org.springframework.data.repository.CrudRepository; /** * Unit tests for {@link SimpleJpaRepository}. @@ -55,6 +59,7 @@ public class SimpleJpaRepositoryUnitTests { @Mock JpaEntityInformation information; @Mock CrudMethodMetadata metadata; @Mock EntityGraph entityGraph; + @Mock org.springframework.data.jpa.repository.EntityGraph entityGraphAnnotation; @Before public void setUp() { @@ -97,15 +102,20 @@ public class SimpleJpaRepositoryUnitTests { /** * @see DATAJPA-689 + * @see DATAJPA-696 */ @Test @SuppressWarnings({ "rawtypes", "unchecked" }) - public void shouldPropagateConfiguredEntityGraphToFindOne() { + public void shouldPropagateConfiguredEntityGraphToFindOne() throws Exception{ String entityGraphName = "User.detail"; - when(metadata.getEntityGraph()).thenReturn(new JpaEntityGraph(entityGraphName, EntityGraphType.LOAD)); + when(entityGraphAnnotation.value()).thenReturn(entityGraphName); + when(entityGraphAnnotation.type()).thenReturn(EntityGraphType.LOAD); + when(metadata.getEntityGraph()).thenReturn(entityGraphAnnotation); when(em.getEntityGraph(entityGraphName)).thenReturn((EntityGraph) entityGraph); - + when(information.getEntityName()).thenReturn("User"); + when(metadata.getMethod()).thenReturn(CrudRepository.class.getMethod("findOne", Serializable.class)); + Integer id = 0; repo.findOne(id);