|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | +package org.apache.openjpa.persistence.entitygraph; |
| 20 | + |
| 21 | +import java.lang.reflect.Proxy; |
| 22 | + |
| 23 | +import jakarta.persistence.CacheRetrieveMode; |
| 24 | +import jakarta.persistence.CacheStoreMode; |
| 25 | +import jakarta.persistence.EntityGraph; |
| 26 | +import jakarta.persistence.EntityManager; |
| 27 | + |
| 28 | +import org.apache.openjpa.enhance.PersistenceCapable; |
| 29 | +import org.apache.openjpa.kernel.OpenJPAStateManager; |
| 30 | +import org.apache.openjpa.meta.FieldMetaData; |
| 31 | +import org.apache.openjpa.persistence.test.SingleEMFTestCase; |
| 32 | +import org.apache.openjpa.util.ImplHelper; |
| 33 | + |
| 34 | +/** |
| 35 | + * Tests {@code EntityManager.find(EntityGraph, Object, FindOption...)} (JPA 3.2). |
| 36 | + * The graph is interpreted as a <em>load graph</em>: attributes named by the graph are fetched eagerly, |
| 37 | + * attributes outside the graph keep their declared fetch behaviour. |
| 38 | + */ |
| 39 | +public class TestEntityGraphFind extends SingleEMFTestCase { |
| 40 | + |
| 41 | + private static final int DEPT_ID = 1; |
| 42 | + private static final int EMP_ID = 10; |
| 43 | + |
| 44 | + @Override |
| 45 | + public void setUp() { |
| 46 | + setUp(EGEmployee.class, EGEmployee2.class, EGEmployee3.class, |
| 47 | + EGDepartment.class, CLEAR_TABLES); |
| 48 | + createData(); |
| 49 | + } |
| 50 | + |
| 51 | + private void createData() { |
| 52 | + EntityManager em = emf.createEntityManager(); |
| 53 | + try { |
| 54 | + em.getTransaction().begin(); |
| 55 | + EGDepartment dept = new EGDepartment(DEPT_ID, "Engineering"); |
| 56 | + em.persist(dept); |
| 57 | + for (int i = 0; i < 2; i++) { |
| 58 | + EGEmployee emp = new EGEmployee(); |
| 59 | + emp.setId(EMP_ID + i); |
| 60 | + emp.setFirstName("First" + i); |
| 61 | + emp.setLastName("Last" + i); |
| 62 | + emp.setSalary(1000 + i); |
| 63 | + emp.setDepartment(dept); |
| 64 | + dept.getEmployees().add(emp); |
| 65 | + em.persist(emp); |
| 66 | + } |
| 67 | + em.getTransaction().commit(); |
| 68 | + } finally { |
| 69 | + if (em.getTransaction().isActive()) { |
| 70 | + em.getTransaction().rollback(); |
| 71 | + } |
| 72 | + em.close(); |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * Direct, non-brittle load state check: looks at the state manager's loaded bit for the given attribute |
| 78 | + * rather than counting SQL statements. |
| 79 | + */ |
| 80 | + private boolean isLoaded(Object entity, String attribute) { |
| 81 | + PersistenceCapable pc = ImplHelper.toPersistenceCapable(entity, emf.getConfiguration()); |
| 82 | + assertNotNull("not a persistence capable instance", pc); |
| 83 | + OpenJPAStateManager sm = (OpenJPAStateManager) pc.pcGetStateManager(); |
| 84 | + assertNotNull("entity is not managed", sm); |
| 85 | + FieldMetaData fmd = sm.getMetaData().getField(attribute); |
| 86 | + assertNotNull("no such persistent attribute: " + attribute, fmd); |
| 87 | + return sm.getLoaded().get(fmd.getIndex()); |
| 88 | + } |
| 89 | + |
| 90 | + /** |
| 91 | + * Baseline: without a graph the lazy collection is not loaded. Without this control the positive |
| 92 | + * assertion below would be vacuous. |
| 93 | + */ |
| 94 | + public void testFindWithoutGraphLeavesLazyAttributeUnloaded() { |
| 95 | + EntityManager em = emf.createEntityManager(); |
| 96 | + try { |
| 97 | + EGDepartment dept = em.find(EGDepartment.class, DEPT_ID); |
| 98 | + assertNotNull(dept); |
| 99 | + assertFalse("employees must be lazy without a graph", isLoaded(dept, "employees")); |
| 100 | + } finally { |
| 101 | + em.close(); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * The load bearing test: the very same lookup with a graph naming the lazy collection loads it eagerly. |
| 107 | + */ |
| 108 | + public void testFindWithEntityGraphLoadsGraphAttribute() { |
| 109 | + EntityManager em = emf.createEntityManager(); |
| 110 | + try { |
| 111 | + EntityGraph<EGDepartment> graph = em.createEntityGraph(EGDepartment.class); |
| 112 | + graph.addAttributeNodes("employees"); |
| 113 | + EGDepartment dept = em.find(graph, DEPT_ID); |
| 114 | + assertNotNull(dept); |
| 115 | + assertEquals(DEPT_ID, dept.getId()); |
| 116 | + assertTrue("employees must be loaded by the load graph", isLoaded(dept, "employees")); |
| 117 | + assertEquals(2, dept.getEmployees().size()); |
| 118 | + } finally { |
| 119 | + em.close(); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Load graph, not fetch graph: attributes outside the graph keep their declared (eager) behaviour. |
| 125 | + */ |
| 126 | + public void testFindWithEntityGraphKeepsNonGraphAttributesDefault() { |
| 127 | + EntityManager em = emf.createEntityManager(); |
| 128 | + try { |
| 129 | + EntityGraph<EGEmployee> graph = em.createEntityGraph(EGEmployee.class); |
| 130 | + graph.addAttributeNodes("firstName"); |
| 131 | + EGEmployee emp = em.find(graph, EMP_ID); |
| 132 | + assertNotNull(emp); |
| 133 | + assertEquals("First0", emp.getFirstName()); |
| 134 | + assertTrue("lastName is outside the graph but eager by default", isLoaded(emp, "lastName")); |
| 135 | + assertNotNull(emp.getDepartment()); |
| 136 | + } finally { |
| 137 | + em.close(); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + /** |
| 142 | + * Subgraphs are applied recursively: the department reached from the employee has its lazy collection |
| 143 | + * loaded. The control below proves the plain find() does not. |
| 144 | + */ |
| 145 | + public void testFindWithSubgraph() { |
| 146 | + EntityManager control = emf.createEntityManager(); |
| 147 | + try { |
| 148 | + EGEmployee emp = control.find(EGEmployee.class, EMP_ID); |
| 149 | + assertNotNull(emp); |
| 150 | + assertFalse(isLoaded(emp.getDepartment(), "employees")); |
| 151 | + } finally { |
| 152 | + control.close(); |
| 153 | + } |
| 154 | + |
| 155 | + EntityManager em = emf.createEntityManager(); |
| 156 | + try { |
| 157 | + EntityGraph<EGEmployee> graph = em.createEntityGraph(EGEmployee.class); |
| 158 | + graph.addSubgraph("department").addAttributeNodes("employees"); |
| 159 | + EGEmployee emp = em.find(graph, EMP_ID); |
| 160 | + assertNotNull(emp); |
| 161 | + assertNotNull(emp.getDepartment()); |
| 162 | + assertTrue("the subgraph must load department.employees", isLoaded(emp.getDepartment(), "employees")); |
| 163 | + assertEquals(2, emp.getDepartment().getEmployees().size()); |
| 164 | + } finally { |
| 165 | + em.close(); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * A graph that references itself must not send the graph traversal into an endless recursion. |
| 171 | + */ |
| 172 | + public void testFindWithCyclicGraph() { |
| 173 | + EntityManager em = emf.createEntityManager(); |
| 174 | + try { |
| 175 | + EntityGraph<EGDepartment> graph = em.createEntityGraph(EGDepartment.class); |
| 176 | + jakarta.persistence.Subgraph<EGEmployee> emps = graph.addSubgraph("employees", EGEmployee.class); |
| 177 | + emps.addSubgraph("department", EGDepartment.class).addAttributeNodes("employees"); |
| 178 | + EGDepartment dept = em.find(graph, DEPT_ID); |
| 179 | + assertNotNull(dept); |
| 180 | + assertTrue(isLoaded(dept, "employees")); |
| 181 | + } finally { |
| 182 | + em.close(); |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + * FindOptions are honoured on the graph based overload as well, sharing the option parsing with |
| 188 | + * {@code find(Class, Object, FindOption...)}. |
| 189 | + */ |
| 190 | + public void testFindWithGraphAndFindOptions() { |
| 191 | + EntityManager em = emf.createEntityManager(); |
| 192 | + try { |
| 193 | + EntityGraph<EGDepartment> graph = em.createEntityGraph(EGDepartment.class); |
| 194 | + graph.addAttributeNodes("employees"); |
| 195 | + EGDepartment dept = em.find(graph, DEPT_ID, CacheStoreMode.BYPASS, CacheRetrieveMode.BYPASS); |
| 196 | + assertNotNull(dept); |
| 197 | + assertTrue(isLoaded(dept, "employees")); |
| 198 | + } finally { |
| 199 | + em.close(); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + public void testFindWithGraphReturnsNullWhenMissing() { |
| 204 | + EntityManager em = emf.createEntityManager(); |
| 205 | + try { |
| 206 | + EntityGraph<EGDepartment> graph = em.createEntityGraph(EGDepartment.class); |
| 207 | + graph.addAttributeNodes("employees"); |
| 208 | + assertNull(em.find(graph, 9999)); |
| 209 | + } finally { |
| 210 | + em.close(); |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + public void testFindWithNullGraphThrowsIAE() { |
| 215 | + EntityManager em = emf.createEntityManager(); |
| 216 | + try { |
| 217 | + em.find((EntityGraph<EGDepartment>) null, DEPT_ID); |
| 218 | + fail("expected IllegalArgumentException"); |
| 219 | + } catch (IllegalArgumentException expected) { |
| 220 | + // expected |
| 221 | + } finally { |
| 222 | + em.close(); |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + public void testFindWithNullPrimaryKeyThrowsIAE() { |
| 227 | + EntityManager em = emf.createEntityManager(); |
| 228 | + try { |
| 229 | + EntityGraph<EGDepartment> graph = em.createEntityGraph(EGDepartment.class); |
| 230 | + em.find(graph, null); |
| 231 | + fail("expected IllegalArgumentException"); |
| 232 | + } catch (IllegalArgumentException expected) { |
| 233 | + // expected |
| 234 | + } finally { |
| 235 | + em.close(); |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + @SuppressWarnings("unchecked") |
| 240 | + public void testFindWithForeignEntityGraphThrowsIAE() { |
| 241 | + EntityManager em = emf.createEntityManager(); |
| 242 | + try { |
| 243 | + EntityGraph<EGDepartment> foreign = (EntityGraph<EGDepartment>) Proxy.newProxyInstance( |
| 244 | + getClass().getClassLoader(), new Class<?>[] { EntityGraph.class }, (p, m, a) -> null); |
| 245 | + em.find(foreign, DEPT_ID); |
| 246 | + fail("expected IllegalArgumentException"); |
| 247 | + } catch (IllegalArgumentException expected) { |
| 248 | + assertTrue(String.valueOf(expected.getMessage()).contains("Unknown EntityGraph implementation")); |
| 249 | + } finally { |
| 250 | + em.close(); |
| 251 | + } |
| 252 | + } |
| 253 | +} |
0 commit comments