From 5fd8578d30b62f8785248c16d4e3c9d090a7bdfd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 13 Mar 2017 09:55:31 +0100 Subject: [PATCH] DATAJPA-1075 - Append ad hoc EntityGraph subgraphs properly. We now make sure to append instead of recreate subgraphs correctly when AttributeNodes already exist for a given property or dot path. This change fixes errors when creating ad hoc fetch graphs like below where multiple properties are loaded via a subgraph on the same type. @EntityGraph(attributePaths = { "colleagues.roles", "colleagues.colleagues" }) Previously the first path was accidentally dropped. Additionally we fixed issues with the creation of "deep" ad hoc fetch graphs via @EntityGraph so that @EntityGraph(attributePaths = { "roles", "colleagues.roles", "colleagues.colleagues.roles" }) can be used as a short form of @NamedEntityGraph(name = "User.deepGraph", attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode(value="colleagues", subgraph = "User.colleagues") }, subgraphs = { @NamedSubgraph(name = "User.colleagues", attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode(value = "colleagues", subgraph = "User.colleaguesOfColleagues") }), @NamedSubgraph(name="User.colleaguesOfColleagues", attributeNodes = { @NamedAttributeNode("roles"), }) }) }) We had to disable EclipseLink tests for this one since there seems to be a glitch when calling EntityManager.clear() prior to applying the fetch-/loadgraph causing the properties not to be in the correct load state. Omitting EntityManager.clear() is not an option since the load state is always LOADED even when leaving out query hints. Related issue: DATAJPA-1041. Original pull request: #192. Related pull request: #188. --- .../data/jpa/repository/query/Jpa21Utils.java | 118 +++++---- .../data/jpa/domain/sample/User.java | 12 +- ...raphRepositoryMethodsIntegrationTests.java | 15 +- ...raphRepositoryMethodsIntegrationTests.java | 35 +++ .../query/EclipseLinkJpa21UtilsTests.java | 26 ++ .../jpa/repository/query/Jpa21UtilsTests.java | 146 +++++++++++ .../query/OpenJpaJpa21UtilsTests.java | 26 ++ ...ethodsWithEntityGraphConfigRepository.java | 4 + .../data/jpa/util/IsAttributeNode.java | 228 ++++++++++++++++++ 9 files changed, 560 insertions(+), 50 deletions(-) create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java create mode 100644 src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java create mode 100644 src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java 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 d3e3380fd..c1c67df02 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 @@ -149,81 +149,103 @@ public class Jpa21Utils { // Sort to ensure that the intermediate entity subgraphs are created accordingly. Collections.sort(attributePaths); - Collections.reverse(attributePaths); - // We build the entity graph based on the paths with highest depth first for (String path : attributePaths) { - // Fast path - just single attribute - if (!path.contains(".")) { - - if (findAttributeNode(path, entityGraph) == null) { - entityGraph.addAttributeNodes(path); - } - - continue; - } - - // We need to build nested sub fetch graphs String[] pathComponents = StringUtils.delimitedListToStringArray(path, "."); - Subgraph parent = null; + createGraph(pathComponents, 0, entityGraph, null); + } + } - for (int c = 0; c < pathComponents.length - 1; c++) { - parent = c == 0 ? findOrCreateSubgraph(pathComponents[c], entityGraph) : parent.addSubgraph(pathComponents[c]); + private static void createGraph(String[] pathComponents, int offset, EntityGraph root, Subgraph parent) { + + String attributeName = pathComponents[offset]; + + // we found our leaf property, now let's see if it already exists and add it if not + if (pathComponents.length - 1 == offset) { + + if (parent == null && !exists(attributeName, root.getAttributeNodes())) { + root.addAttributeNodes(attributeName); + } else if (parent != null && !exists(attributeName, parent.getAttributeNodes())) { + parent.addAttributeNodes(attributeName); } + return; + } - parent.addAttributeNodes(pathComponents[pathComponents.length - 1]); + AttributeNode node = findAttributeNode(attributeName, root, parent); + if (node != null) { + + Subgraph subgraph = getSubgraph(node); + if (subgraph == null) { + subgraph = parent != null ? parent.addSubgraph(attributeName) : root.addSubgraph(attributeName); + } + createGraph(pathComponents, offset + 1, root, subgraph); + return; + } + + if (parent == null) { + createGraph(pathComponents, offset + 1, root, root.addSubgraph(attributeName)); + } else { + createGraph(pathComponents, offset + 1, root, parent.addSubgraph(attributeName)); } } /** - * Returns the {@link Subgraph} with the given name fro the given {@link EntityGraph} or creates a new one if none - * already available. - * - * @param name - * @param entityGraph + * Checks the given {@link List} of {@link AttributeNode}s for the existence of an {@link AttributeNode} matching the + * given {@literal attributeNodeName}. + * + * @param attributeNodeName + * @param nodes * @return */ - private static Subgraph findOrCreateSubgraph(String name, EntityGraph entityGraph) { - - Subgraph subgraph = findSubgraph(name, entityGraph); - - return subgraph != null ? subgraph : entityGraph.addSubgraph(name); + private static boolean exists(String attributeNodeName, List> nodes) { + return findAttributeNode(attributeNodeName, nodes) != null; } /** - * Returns the {@link Subgraph} with the given name from the given {@link EntityGraph}. - * - * @param name + * Find the {@link AttributeNode} matching the given {@literal attributeNodeName} in given {@link Subgraph} or + * {@link EntityGraph} favoring matches {@link Subgraph} over {@link EntityGraph}. + * + * @param attributeNodeName * @param entityGraph - * @return + * @param parent + * @return {@literal null} if not found. */ - private static Subgraph findSubgraph(String name, EntityGraph entityGraph) { - - AttributeNode node = findAttributeNode(name, entityGraph); - - if (node != null && !ObjectUtils.isEmpty(node.getSubgraphs())) { - return node.getSubgraphs().values().iterator().next(); - } - - return null; + private static AttributeNode findAttributeNode(String attributeNodeName, EntityGraph entityGraph, + Subgraph parent) { + return findAttributeNode(attributeNodeName, + parent != null ? parent.getAttributeNodes() : entityGraph.getAttributeNodes()); } /** - * Returns the {@link AttributeNode} with the given name if present in the given {@link EntityGraph}. - * - * @param name - * @param entityGraph must not be {@literal null}. - * @return + * Find the {@link AttributeNode} matching the given {@literal attributeNodeName} in given {@link List} of + * {@link AttributeNode}s. + * + * @param attributeNodeName + * @param nodes + * @return {@literal null} if not found. */ - private static AttributeNode findAttributeNode(String name, EntityGraph entityGraph) { + private static AttributeNode findAttributeNode(String attributeNodeName, List> nodes) { - for (AttributeNode node : entityGraph.getAttributeNodes()) { - if (ObjectUtils.nullSafeEquals(node.getAttributeName(), name)) { + for (AttributeNode node : nodes) { + if (ObjectUtils.nullSafeEquals(node.getAttributeName(), attributeNodeName)) { return node; } } return null; } + + /** + * Extracts the first {@link Subgraph} from the given {@link AttributeNode}. Ignores any potential different + * {@link Subgraph}s registered for more concrete {@link Class}es as the dynamically created graph does not + * distinguish between those. + * + * @param node + * @return + */ + private static Subgraph getSubgraph(AttributeNode node) { + return node.getSubgraphs().isEmpty() ? null : node.getSubgraphs().values().iterator().next(); + } + } 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 40d15b2fd..1fa4065fb 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 @@ -64,7 +64,17 @@ import javax.persistence.TemporalType; attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode(value = "colleagues", subgraph = "User.colleagues") }, subgraphs = { @NamedSubgraph(name = "User.colleagues", - attributeNodes = { @NamedAttributeNode("colleagues"), @NamedAttributeNode("roles") }) }) }) + attributeNodes = { @NamedAttributeNode("colleagues"), + @NamedAttributeNode("roles") }) }), + @NamedEntityGraph(name = "User.deepGraph", + attributeNodes = { @NamedAttributeNode("roles"), + @NamedAttributeNode(value = "colleagues", subgraph = "User.colleagues") }, + subgraphs = { + @NamedSubgraph(name = "User.colleagues", + attributeNodes = { @NamedAttributeNode("roles"), + @NamedAttributeNode(value = "colleagues", subgraph = "User.colleaguesOfColleagues") }), + @NamedSubgraph(name = "User.colleaguesOfColleagues", + attributeNodes = { @NamedAttributeNode("roles"), }) }) }) @NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1") @NamedStoredProcedureQueries({ // @NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", diff --git a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java index 6c2bd0da6..9211130f0 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2017 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,6 +21,7 @@ import org.springframework.test.context.ContextConfiguration; /** * @author Oliver Gierke + * @author Christoph Strobl */ @ContextConfiguration("classpath:eclipselink.xml") public class EclipseLinkEntityGraphRepositoryMethodsIntegrationTests @@ -37,4 +38,16 @@ public class EclipseLinkEntityGraphRepositoryMethodsIntegrationTests @Ignore @Test public void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() {} + + @Ignore + @Test + public void shouldCreateDynamicGraphWithMultipleLevelsOfSubgraphs() {} + + @Ignore + @Test + public void shouldRespectConfiguredJpaEntityGraphInFindOne() {} + + @Ignore + @Test + public void shouldRespectInferFetchGraphFromMethodName() {} } 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 72727429f..b0f9ccda2 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java @@ -78,6 +78,7 @@ public class EntityGraphRepositoryMethodsIntegrationTests { ollie = repository.save(ollie); tom.getColleagues().add(ollie); + christoph.addRole(role); christoph = repository.save(christoph); ollie.getColleagues().add(christoph); @@ -88,6 +89,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraph() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); List result = repository.findAll(); @@ -100,6 +103,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraphInFindOne() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); User user = repository.findOne(tom.getId()).get(); @@ -112,6 +117,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectInferFetchGraphFromMethodName() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); User user = repository.getOneWithDefinedEntityGraphById(tom.getId()); @@ -124,6 +131,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); em.flush(); em.clear(); @@ -145,6 +154,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraphWithPaginationAndQueryDslPredicates() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); Page page = repository.findAll(QUser.user.firstname.isNotNull(), PageRequest.of(0, 100)); List result = page.getContent(); @@ -195,4 +206,28 @@ public class EntityGraphRepositoryMethodsIntegrationTests { assertThat(util.isLoaded(colleague, "roles"), is(true)); } } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateDynamicGraphWithMultipleLevelsOfSubgraphs() { + + Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); + + User user = repository.findOneWithDeepGraphById(tom.getId()); + + assertThat(user, is(notNullValue())); + + assertThat("colleagues on root should have been fetched by dynamic subgraph declaration", + Persistence.getPersistenceUtil().isLoaded(user, "colleagues"), is(true)); + + for (User colleague : user.getColleagues()) { + assertThat(Persistence.getPersistenceUtil().isLoaded(colleague, "colleagues"), is(true)); + assertThat(Persistence.getPersistenceUtil().isLoaded(colleague, "roles"), is(true)); + for (User colleagueOfColleague : colleague.getColleagues()) { + assertThat(Persistence.getPersistenceUtil().isLoaded(colleagueOfColleague, "roles"), is(true)); + assertThat(Persistence.getPersistenceUtil().isLoaded(colleagueOfColleague, "colleagues"), is(false)); + } + } + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java new file mode 100644 index 000000000..51e66c89a --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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.query; + +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Christoph Strobl + */ +@ContextConfiguration("classpath:eclipselink.xml") +public class EclipseLinkJpa21UtilsTests extends Jpa21UtilsTests { + +} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java new file mode 100644 index 000000000..6a2e43066 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017 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.query; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.springframework.data.jpa.support.EntityManagerTestUtils.*; +import static org.springframework.data.jpa.util.IsAttributeNode.*; + +import javax.persistence.AttributeNode; +import javax.persistence.EntityGraph; +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +public class Jpa21UtilsTests { + + @Autowired EntityManager em; + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithoutSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom( + new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "roles", "colleagues" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraph()); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithMultipleSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, + new String[] { "roles", "colleagues.roles", "colleagues.colleagues" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles", "colleagues")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithDeepSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, + new String[] { "roles", "colleagues.roles", "colleagues.colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldIgnoreIntermedeateSubGraphNodesThatAreNotNeeded() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "roles", + "colleagues", "colleagues.roles", "colleagues.colleagues", "colleagues.colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void orderOfSubGraphsShouldNotMatter() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { + "colleagues.colleagues.roles", "roles", "colleagues.colleagues", "colleagues", "colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test(expected = Exception.class) // DATAJPA-1041, DATAJPA-1075 + public void errorsOnUnknownProperties() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "¯\\_(ツ)_/¯" }), + em.createEntityGraph(User.class)); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java new file mode 100644 index 000000000..03c8468c5 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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.query; + +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Christoph Strobl + */ +@ContextConfiguration("classpath:openjpa.xml") +public class OpenJpaJpa21UtilsTests extends Jpa21UtilsTests { + +} diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java index 928ef24be..3b896f9d1 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java @@ -70,4 +70,8 @@ public interface RepositoryMethodsWithEntityGraphConfigRepository // DATAJPA-1041 @EntityGraph(attributePaths = { "colleagues", "colleagues.roles", "colleagues.colleagues" }) User findOneWithMultipleSubGraphsById(Integer id); + + // DATAJPA-1041, DATAJPA-1075 + @EntityGraph(attributePaths = { "colleagues", "colleagues.roles", "colleagues.colleagues.roles" }) + User findOneWithDeepGraphById(Integer id); } diff --git a/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java b/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java new file mode 100644 index 000000000..63e76fa62 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java @@ -0,0 +1,228 @@ +/* + * Copyright 2017 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.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.persistence.AttributeNode; +import javax.persistence.EntityGraph; +import javax.persistence.Subgraph; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class IsAttributeNode extends TypeSafeMatcher> { + + private boolean terminatingNodeCheck = false; + private List nodes; + private List subgraphs; + private List errors = new ArrayList(); + + @Override + protected boolean matchesSafely(AttributeNode item) { + + if (item == null) { + + errors.add("AttributeNode was null!"); + return false; + } + + if (terminatingNodeCheck) { + + if (!CollectionUtils.isEmpty(item.getSubgraphs())) { + + errors.add(String.format("'%s' was expected to be a terminating node but has subgraphs %s.", + item.getAttributeName(), extractExistingAttributeNames(item.getSubgraphs().values().iterator().next()))); + return false; + } + return true; + } + + if (CollectionUtils.isEmpty(item.getSubgraphs())) { + + if (!CollectionUtils.isEmpty(nodes)) { + + errors + .add(String.format("Leaf properties %s could not be found. The node does not have any subgraphs.", nodes)); + } + if (!CollectionUtils.isEmpty(subgraphs)) { + errors.add(String.format("Subgraphs %s could not be found. The node does not have any subgraphs.", subgraphs)); + } + return false; + } + + Subgraph graph = item.getSubgraphs().values().iterator().next(); + + if (!CollectionUtils.isEmpty(nodes)) { + for (String nodeName : nodes) { + + AttributeNode node = findNode(nodeName, graph.getAttributeNodes()); + if (node == null) { + + errors.add(String.format("AttributeNode '%s' could not be found in subgraph for '%s'. Know nodes are: %s.", + nodeName, item.getAttributeName(), extractExistingAttributeNames(graph))); + return false; + } + + if (!CollectionUtils.isEmpty(node.getSubgraphs())) { + + errors.add(String.format("AttributeNode %s of subgraph %s is not a leaf property but has % SubGraph(s).", + nodeName, item.getAttributeName(), node.getSubgraphs().size())); + return false; + } + } + } + + if (!CollectionUtils.isEmpty(subgraphs)) { + for (String subgraphName : subgraphs) { + + AttributeNode node = findNode(subgraphName, graph.getAttributeNodes()); + if (node == null) { + + errors.add(String.format("Subgraph '%s' could not be found in SubGraph for '%s'. Know nodes are: %s.", + subgraphName, item.getAttributeName(), extractExistingAttributeNames(graph))); + return false; + } + + if (CollectionUtils.isEmpty(node.getSubgraphs())) { + + errors.add(String.format("'%s' of SubGraph '%s' is not a SubGraph.", subgraphName, item.getAttributeName())); + return false; + } + } + } + return true; + } + + @Override + public void describeTo(Description description) { + + for (String error : errors) { + description.appendText(error); + } + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the root of the given {@literal graph}. + * + * @param nodeName + * @param graph + * @return + */ + public static AttributeNode findNode(String nodeName, EntityGraph graph) { + + if (graph == null) { + return null; + } + + return findNode(nodeName, graph.getAttributeNodes()); + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the {@link List} of given {@literal nodes}. + * + * @param nodeName + * @param nodes + * @return + */ + public static AttributeNode findNode(String nodeName, List> nodes) { + + if (CollectionUtils.isEmpty(nodes)) { + return null; + } + + for (AttributeNode node : nodes) { + if (ObjectUtils.nullSafeEquals(node.getAttributeName(), nodeName)) { + return node; + } + } + + return null; + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the first {@link Subgraph} of the given + * {@literal node}. + * + * @param attributeName + * @param node + * @return + */ + public static AttributeNode findNode(String attributeName, AttributeNode node) { + + if (CollectionUtils.isEmpty(node.getSubgraphs())) { + return null; + } + + Subgraph subgraph = node.getSubgraphs().values().iterator().next(); + return findNode(attributeName, subgraph.getAttributeNodes()); + } + + private List extractExistingAttributeNames(Subgraph graph) { + + List result = new ArrayList(graph.getAttributeNodes().size()); + for (AttributeNode node : graph.getAttributeNodes()) { + result.add(node.getAttributeName()); + } + return result; + } + + /** + * Asserts that the fetch graph terminates with {@link AttributeNode}s having the given {@literal nodeNames}. + * + * @param nodeNames + * @return + */ + public static IsAttributeNode terminatesGraphWith(String... nodeNames) { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.nodes = Arrays.asList(nodeNames); + return matcher; + } + + /** + * Asserts that the fetch graph continues with {@link AttributeNode}s having {@link AttributeNode#getSubgraphs()} with + * given {@literal subgraphNames}. + * + * @return + */ + public static IsAttributeNode hasSubgraphs(String... subgraphNames) { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.subgraphs = Arrays.asList(subgraphNames); + return matcher; + } + + /** + * Asserts that the fetch graph terminates with the given {@link AttributeNode} by checking + * {@link AttributeNode#getSubgraphs()} is empty. + * + * @return + */ + public static IsAttributeNode terminatesGraph() { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.terminatingNodeCheck = true; + return matcher; + } +}