Skip to content

Commit 66badf3

Browse files
authored
Merge pull request #156 from apache/OPENJPA-2985
[OPENJPA-2985] Implement the mandatory JPA 3.2 methods that threw UnsupportedOperationException
2 parents 9b0a5fe + c83f543 commit 66badf3

9 files changed

Lines changed: 1027 additions & 4 deletions

File tree

openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/EGDepartment.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@
1818
*/
1919
package org.apache.openjpa.persistence.entitygraph;
2020

21+
import java.util.ArrayList;
22+
import java.util.List;
23+
2124
import jakarta.persistence.Entity;
25+
import jakarta.persistence.FetchType;
2226
import jakarta.persistence.Id;
27+
import jakarta.persistence.OneToMany;
2328
import jakarta.persistence.Table;
2429

2530
@Entity
@@ -31,6 +36,9 @@ public class EGDepartment {
3136

3237
private String name;
3338

39+
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
40+
private List<EGEmployee> employees = new ArrayList<>();
41+
3442
public EGDepartment() {}
3543

3644
public EGDepartment(int id, String name) {
@@ -42,4 +50,6 @@ public EGDepartment(int id, String name) {
4250
public void setId(int id) { this.id = id; }
4351
public String getName() { return name; }
4452
public void setName(String name) { this.name = name; }
53+
public List<EGEmployee> getEmployees() { return employees; }
54+
public void setEmployees(List<EGEmployee> employees) { this.employees = employees; }
4555
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.query;
20+
21+
import jakarta.persistence.Entity;
22+
import jakarta.persistence.Id;
23+
import jakarta.persistence.NamedNativeQuery;
24+
import jakarta.persistence.NamedQueries;
25+
import jakarta.persistence.NamedQuery;
26+
import jakarta.persistence.QueryHint;
27+
import jakarta.persistence.Table;
28+
29+
/**
30+
* Entity declaring named queries with and without a JPA 3.2 {@code resultClass}.
31+
*/
32+
@Entity
33+
@Table(name = "NQ_REF_ENTITY")
34+
@NamedQueries({
35+
@NamedQuery(name = "NQRef.all",
36+
query = "select o from NamedQueryRefEntity o",
37+
resultClass = NamedQueryRefEntity.class,
38+
hints = @QueryHint(name = "openjpa.FetchPlan.MaxFetchDepth", value = "2")),
39+
@NamedQuery(name = "NQRef.names",
40+
query = "select o.name from NamedQueryRefEntity o",
41+
resultClass = String.class),
42+
@NamedQuery(name = "NQRef.untyped",
43+
query = "select o from NamedQueryRefEntity o where o.name = 'x'")
44+
})
45+
@NamedNativeQuery(name = "NQRef.native",
46+
query = "select id, name from NQ_REF_ENTITY",
47+
resultClass = NamedQueryRefEntity.class)
48+
public class NamedQueryRefEntity {
49+
50+
@Id
51+
private int id;
52+
53+
private String name;
54+
55+
public int getId() {
56+
return id;
57+
}
58+
59+
public void setId(int id) {
60+
this.id = id;
61+
}
62+
63+
public String getName() {
64+
return name;
65+
}
66+
67+
public void setName(String name) {
68+
this.name = name;
69+
}
70+
}

0 commit comments

Comments
 (0)