diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/EGDepartment.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/EGDepartment.java index 83b4b12b61..054b21682f 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/EGDepartment.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/EGDepartment.java @@ -18,8 +18,13 @@ */ package org.apache.openjpa.persistence.entitygraph; +import java.util.ArrayList; +import java.util.List; + import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @@ -31,6 +36,9 @@ public class EGDepartment { private String name; + @OneToMany(mappedBy = "department", fetch = FetchType.LAZY) + private List employees = new ArrayList<>(); + public EGDepartment() {} public EGDepartment(int id, String name) { @@ -42,4 +50,6 @@ public EGDepartment(int id, String name) { public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } + public List getEmployees() { return employees; } + public void setEmployees(List employees) { this.employees = employees; } } diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/TestEntityGraphFind.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/TestEntityGraphFind.java new file mode 100644 index 0000000000..7b3393c0a0 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/entitygraph/TestEntityGraphFind.java @@ -0,0 +1,253 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.entitygraph; + +import java.lang.reflect.Proxy; + +import jakarta.persistence.CacheRetrieveMode; +import jakarta.persistence.CacheStoreMode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; + +import org.apache.openjpa.enhance.PersistenceCapable; +import org.apache.openjpa.kernel.OpenJPAStateManager; +import org.apache.openjpa.meta.FieldMetaData; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; +import org.apache.openjpa.util.ImplHelper; + +/** + * Tests {@code EntityManager.find(EntityGraph, Object, FindOption...)} (JPA 3.2). + * The graph is interpreted as a load graph: attributes named by the graph are fetched eagerly, + * attributes outside the graph keep their declared fetch behaviour. + */ +public class TestEntityGraphFind extends SingleEMFTestCase { + + private static final int DEPT_ID = 1; + private static final int EMP_ID = 10; + + @Override + public void setUp() { + setUp(EGEmployee.class, EGEmployee2.class, EGEmployee3.class, + EGDepartment.class, CLEAR_TABLES); + createData(); + } + + private void createData() { + EntityManager em = emf.createEntityManager(); + try { + em.getTransaction().begin(); + EGDepartment dept = new EGDepartment(DEPT_ID, "Engineering"); + em.persist(dept); + for (int i = 0; i < 2; i++) { + EGEmployee emp = new EGEmployee(); + emp.setId(EMP_ID + i); + emp.setFirstName("First" + i); + emp.setLastName("Last" + i); + emp.setSalary(1000 + i); + emp.setDepartment(dept); + dept.getEmployees().add(emp); + em.persist(emp); + } + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + + /** + * Direct, non-brittle load state check: looks at the state manager's loaded bit for the given attribute + * rather than counting SQL statements. + */ + private boolean isLoaded(Object entity, String attribute) { + PersistenceCapable pc = ImplHelper.toPersistenceCapable(entity, emf.getConfiguration()); + assertNotNull("not a persistence capable instance", pc); + OpenJPAStateManager sm = (OpenJPAStateManager) pc.pcGetStateManager(); + assertNotNull("entity is not managed", sm); + FieldMetaData fmd = sm.getMetaData().getField(attribute); + assertNotNull("no such persistent attribute: " + attribute, fmd); + return sm.getLoaded().get(fmd.getIndex()); + } + + /** + * Baseline: without a graph the lazy collection is not loaded. Without this control the positive + * assertion below would be vacuous. + */ + public void testFindWithoutGraphLeavesLazyAttributeUnloaded() { + EntityManager em = emf.createEntityManager(); + try { + EGDepartment dept = em.find(EGDepartment.class, DEPT_ID); + assertNotNull(dept); + assertFalse("employees must be lazy without a graph", isLoaded(dept, "employees")); + } finally { + em.close(); + } + } + + /** + * The load bearing test: the very same lookup with a graph naming the lazy collection loads it eagerly. + */ + public void testFindWithEntityGraphLoadsGraphAttribute() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGDepartment.class); + graph.addAttributeNodes("employees"); + EGDepartment dept = em.find(graph, DEPT_ID); + assertNotNull(dept); + assertEquals(DEPT_ID, dept.getId()); + assertTrue("employees must be loaded by the load graph", isLoaded(dept, "employees")); + assertEquals(2, dept.getEmployees().size()); + } finally { + em.close(); + } + } + + /** + * Load graph, not fetch graph: attributes outside the graph keep their declared (eager) behaviour. + */ + public void testFindWithEntityGraphKeepsNonGraphAttributesDefault() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGEmployee.class); + graph.addAttributeNodes("firstName"); + EGEmployee emp = em.find(graph, EMP_ID); + assertNotNull(emp); + assertEquals("First0", emp.getFirstName()); + assertTrue("lastName is outside the graph but eager by default", isLoaded(emp, "lastName")); + assertNotNull(emp.getDepartment()); + } finally { + em.close(); + } + } + + /** + * Subgraphs are applied recursively: the department reached from the employee has its lazy collection + * loaded. The control below proves the plain find() does not. + */ + public void testFindWithSubgraph() { + EntityManager control = emf.createEntityManager(); + try { + EGEmployee emp = control.find(EGEmployee.class, EMP_ID); + assertNotNull(emp); + assertFalse(isLoaded(emp.getDepartment(), "employees")); + } finally { + control.close(); + } + + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGEmployee.class); + graph.addSubgraph("department").addAttributeNodes("employees"); + EGEmployee emp = em.find(graph, EMP_ID); + assertNotNull(emp); + assertNotNull(emp.getDepartment()); + assertTrue("the subgraph must load department.employees", isLoaded(emp.getDepartment(), "employees")); + assertEquals(2, emp.getDepartment().getEmployees().size()); + } finally { + em.close(); + } + } + + /** + * A graph that references itself must not send the graph traversal into an endless recursion. + */ + public void testFindWithCyclicGraph() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGDepartment.class); + jakarta.persistence.Subgraph emps = graph.addSubgraph("employees", EGEmployee.class); + emps.addSubgraph("department", EGDepartment.class).addAttributeNodes("employees"); + EGDepartment dept = em.find(graph, DEPT_ID); + assertNotNull(dept); + assertTrue(isLoaded(dept, "employees")); + } finally { + em.close(); + } + } + + /** + * FindOptions are honoured on the graph based overload as well, sharing the option parsing with + * {@code find(Class, Object, FindOption...)}. + */ + public void testFindWithGraphAndFindOptions() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGDepartment.class); + graph.addAttributeNodes("employees"); + EGDepartment dept = em.find(graph, DEPT_ID, CacheStoreMode.BYPASS, CacheRetrieveMode.BYPASS); + assertNotNull(dept); + assertTrue(isLoaded(dept, "employees")); + } finally { + em.close(); + } + } + + public void testFindWithGraphReturnsNullWhenMissing() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGDepartment.class); + graph.addAttributeNodes("employees"); + assertNull(em.find(graph, 9999)); + } finally { + em.close(); + } + } + + public void testFindWithNullGraphThrowsIAE() { + EntityManager em = emf.createEntityManager(); + try { + em.find((EntityGraph) null, DEPT_ID); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } finally { + em.close(); + } + } + + public void testFindWithNullPrimaryKeyThrowsIAE() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph graph = em.createEntityGraph(EGDepartment.class); + em.find(graph, null); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } finally { + em.close(); + } + } + + @SuppressWarnings("unchecked") + public void testFindWithForeignEntityGraphThrowsIAE() { + EntityManager em = emf.createEntityManager(); + try { + EntityGraph foreign = (EntityGraph) Proxy.newProxyInstance( + getClass().getClassLoader(), new Class[] { EntityGraph.class }, (p, m, a) -> null); + em.find(foreign, DEPT_ID); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(String.valueOf(expected.getMessage()).contains("Unknown EntityGraph implementation")); + } finally { + em.close(); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/NamedQueryRefEntity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/NamedQueryRefEntity.java new file mode 100644 index 0000000000..6ec45c7bdf --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/NamedQueryRefEntity.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedNativeQuery; +import jakarta.persistence.NamedQueries; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.QueryHint; +import jakarta.persistence.Table; + +/** + * Entity declaring named queries with and without a JPA 3.2 {@code resultClass}. + */ +@Entity +@Table(name = "NQ_REF_ENTITY") +@NamedQueries({ + @NamedQuery(name = "NQRef.all", + query = "select o from NamedQueryRefEntity o", + resultClass = NamedQueryRefEntity.class, + hints = @QueryHint(name = "openjpa.FetchPlan.MaxFetchDepth", value = "2")), + @NamedQuery(name = "NQRef.names", + query = "select o.name from NamedQueryRefEntity o", + resultClass = String.class), + @NamedQuery(name = "NQRef.untyped", + query = "select o from NamedQueryRefEntity o where o.name = 'x'") +}) +@NamedNativeQuery(name = "NQRef.native", + query = "select id, name from NQ_REF_ENTITY", + resultClass = NamedQueryRefEntity.class) +public class NamedQueryRefEntity { + + @Id + private int id; + + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestGetNamedQueries.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestGetNamedQueries.java new file mode 100644 index 0000000000..0658a48593 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestGetNamedQueries.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.query; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQueryReference; + +import org.apache.openjpa.meta.MetaDataRepository; +import org.apache.openjpa.meta.QueryMetaData; +import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; +import org.apache.openjpa.persistence.TypedQueryReferenceImpl; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * Tests the JPA 3.2 {@code EntityManagerFactory.getNamedQueries(Class)} and the + * {@code @NamedQuery.resultClass()} support it relies on. + */ +public class TestGetNamedQueries extends SingleEMFTestCase { + + @Override + public void setUp() { + setUp(NamedQueryRefEntity.class, CLEAR_TABLES); + } + + /** + * Regression test for @NamedQuery.resultClass() being parsed at all. + */ + public void testAnnotationResultClassParsed() { + MetaDataRepository mdr = emf.getConfiguration().getMetaDataRepositoryInstance(); + + QueryMetaData all = mdr.getQueryMetaData(null, "NQRef.all", null, true); + assertEquals(NamedQueryRefEntity.class, all.getCandidateType()); + assertNull(all.getResultType()); + + QueryMetaData names = mdr.getQueryMetaData(null, "NQRef.names", null, true); + assertEquals(String.class, names.getResultType()); + assertNull(names.getCandidateType()); + + // resultClass() defaults to void.class - it must not leak into the metadata + QueryMetaData untyped = mdr.getQueryMetaData(null, "NQRef.untyped", null, true); + assertNull(untyped.getCandidateType()); + assertNull(untyped.getResultType()); + } + + public void testEntityResultType() { + Map> refs = + emf.getNamedQueries(NamedQueryRefEntity.class); + assertTrue(refs.containsKey("NQRef.all")); + assertTrue(refs.containsKey("NQRef.native")); + assertFalse(refs.containsKey("NQRef.names")); + assertFalse(refs.containsKey("NQRef.untyped")); + + TypedQueryReference ref = refs.get("NQRef.all"); + assertEquals("NQRef.all", ref.getName()); + assertEquals(NamedQueryRefEntity.class, ref.getResultType()); + } + + public void testScalarResultType() { + Map> refs = emf.getNamedQueries(String.class); + assertTrue(refs.containsKey("NQRef.names")); + assertEquals(String.class, refs.get("NQRef.names").getResultType()); + assertFalse(refs.containsKey("NQRef.all")); + assertFalse(refs.containsKey("NQRef.untyped")); + } + + /** + * Queries without a declared result type are never returned - not even for Object.class. + */ + public void testSupertypeIsAssignable() { + Map> refs = emf.getNamedQueries(Object.class); + assertTrue(refs.containsKey("NQRef.all")); + assertTrue(refs.containsKey("NQRef.names")); + assertTrue(refs.containsKey("NQRef.native")); + assertFalse(refs.containsKey("NQRef.untyped")); + } + + public void testHints() { + Map hints = emf.getNamedQueries(NamedQueryRefEntity.class) + .get("NQRef.all").getHints(); + assertEquals("2", hints.get("openjpa.FetchPlan.MaxFetchDepth")); + try { + hints.put("x", "y"); + fail("hints must be unmodifiable"); + } catch (UnsupportedOperationException expected) { + // expected + } + } + + /** + * The hints of a {@link TypedQueryReference} must keep the declaration order they have in the + * metadata, because {@link jakarta.persistence.EntityManager#createQuery(TypedQueryReference)} + * replays them in iteration order and OpenJPA has aliased hint keys writing the same setting. + */ + public void testHintsKeepDeclarationOrder() { + Map declared = new LinkedHashMap<>(); + declared.put("openjpa.FetchPlan.LockTimeout", "1000"); + declared.put("jakarta.persistence.lock.timeout", "2000"); + declared.put("openjpa.FetchPlan.MaxFetchDepth", "2"); + Map hints = new TypedQueryReferenceImpl<>( + "NQRef.all", NamedQueryRefEntity.class, declared).getHints(); + assertEquals(new ArrayList<>(declared.keySet()), new ArrayList<>(hints.keySet())); + } + + public void testReturnedMapIsACopy() { + Map> refs = + emf.getNamedQueries(NamedQueryRefEntity.class); + refs.clear(); + assertTrue(emf.getNamedQueries(NamedQueryRefEntity.class).containsKey("NQRef.all")); + } + + public void testNullResultTypeRejected() { + try { + emf.getNamedQueries(null); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + /** + * getNamedQueries() must work as the very first operation on a factory, i.e. before any + * entity manager forced the metadata repository to load the persistent types. + */ + public void testColdRepository() { + OpenJPAEntityManagerFactorySPI emf2 = createEMF(NamedQueryRefEntity.class); + try { + assertTrue(emf2.getNamedQueries(NamedQueryRefEntity.class).containsKey("NQRef.all")); + } finally { + closeEMF(emf2); + } + } + + public void testDynamicallyAddedNamedQuery() { + EntityManager em = emf.createEntityManager(); + try { + Query q = em.createQuery("select o from NamedQueryRefEntity o"); + emf.addNamedQuery("NQRef.dyn", q); + assertTrue(emf.getNamedQueries(NamedQueryRefEntity.class).containsKey("NQRef.dyn")); + } finally { + em.close(); + } + } + + /** + * The declared result class must not break execution of the named queries. + */ + public void testCreateNamedQueryWithDeclaredResultClassStillWorks() { + EntityManager em = emf.createEntityManager(); + try { + assertNotNull(em.createNamedQuery("NQRef.all").getResultList()); + assertNotNull(em.createNamedQuery("NQRef.names", String.class).getResultList()); + assertNotNull(em.createQuery(emf.getNamedQueries(NamedQueryRefEntity.class) + .get("NQRef.all")).getResultList()); + } finally { + em.close(); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestTypedQueryReference.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestTypedQueryReference.java new file mode 100644 index 0000000000..bcd60495ff --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/query/TestTypedQueryReference.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence.query; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.TypedQueryReference; + +import org.apache.openjpa.persistence.OpenJPAQuery; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * Tests {@code EntityManager.createQuery(TypedQueryReference)} added in JPA 3.2. + */ +public class TestTypedQueryReference extends SingleEMFTestCase { + + private static final String TIMEOUT_HINT = "jakarta.persistence.query.timeout"; + + @Override + public void setUp() { + setUp(SimpleEntity.class, CLEAR_TABLES); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + em.persist(new SimpleEntity("Name One", "Value One")); + em.persist(new SimpleEntity("Name Two", "Value Two")); + em.getTransaction().commit(); + em.close(); + } + + private static TypedQueryReference ref(final String name, final Class type, + final Map hints) { + return new TypedQueryReference() { + + @Override + public String getName() { + return name; + } + + @Override + public Class getResultType() { + return type; + } + + @Override + public Map getHints() { + return hints; + } + }; + } + + public void testCreateQueryFromReference() { + EntityManager em = emf.createEntityManager(); + try { + TypedQuery q = + em.createQuery(ref("FindAll", SimpleEntity.class, Collections.emptyMap())); + assertNotNull(q); + List list = q.getResultList(); + assertEquals(2, list.size()); + for (SimpleEntity s : list) { + assertNotNull(s.getName()); + } + } finally { + em.close(); + } + } + + public void testResultTypeIsApplied() { + EntityManager em = emf.createEntityManager(); + try { + TypedQuery q = + em.createQuery(ref("FindAll", SimpleEntity.class, Collections.emptyMap())); + assertEquals(SimpleEntity.class, ((OpenJPAQuery) q).getResultClass()); + } finally { + em.close(); + } + } + + public void testReferenceHintsAreApplied() { + EntityManager em = emf.createEntityManager(); + try { + Map hints = + Collections.singletonMap(TIMEOUT_HINT, Integer.valueOf(12345)); + TypedQuery q = em.createQuery(ref("FindAll", SimpleEntity.class, hints)); + assertEquals(Integer.valueOf(12345), q.getHints().get(TIMEOUT_HINT)); + } finally { + em.close(); + } + } + + public void testParametersStillWork() { + EntityManager em = emf.createEntityManager(); + try { + TypedQuery q = + em.createQuery(ref("FindOne", SimpleEntity.class, Collections.emptyMap())); + q.setParameter(1, "Name One"); + assertEquals("Value One", q.getSingleResult().getValue()); + } finally { + em.close(); + } + } + + public void testNullReference() { + EntityManager em = emf.createEntityManager(); + try { + em.createQuery((TypedQueryReference) null); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } finally { + em.close(); + } + } + + public void testUnknownQueryName() { + EntityManager em = emf.createEntityManager(); + try { + em.createQuery(ref("NoSuchNamedQuery", SimpleEntity.class, Collections.emptyMap())); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } finally { + em.close(); + } + } + + public void testClosedEntityManager() { + EntityManager em = emf.createEntityManager(); + em.close(); + try { + em.createQuery(ref("FindAll", SimpleEntity.class, Collections.emptyMap())); + fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + // expected + } + } +} diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java index 72b1a5a143..7ab5207b05 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/AnnotationPersistenceMetaDataParser.java @@ -2029,6 +2029,14 @@ private void parseNamedQueries(AnnotatedElement el, NamedQuery... queries) { meta = getRepository().addQueryMetaData(_cls, query.name()); meta.setLanguage(JPQLParser.LANG_JPQL); meta.setQueryString(query.query()); + // JPA 3.2 added @NamedQuery.resultClass(); handle it exactly like + // @NamedNativeQuery.resultClass() so that a declared result type is + // available from the metadata (and to EntityManagerFactory.getNamedQueries). + Class res = query.resultClass(); + if (ImplHelper.isManagedType(getConfiguration(), res)) + meta.setCandidateType(res); + else if (!void.class.equals(res)) + meta.setResultType(res); for (QueryHint hint : query.hints()) meta.addHint(hint.name(), hint.value()); LockModeType lmt = processNamedQueryLockModeType(query); diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerFactoryImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerFactoryImpl.java index 61df456888..51c2709d3e 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerFactoryImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerFactoryImpl.java @@ -24,9 +24,11 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.function.Function; @@ -92,6 +94,9 @@ public class EntityManagerFactoryImpl private final java.util.concurrent.ConcurrentHashMap> _entityGraphs = new java.util.concurrent.ConcurrentHashMap<>(); private volatile boolean _entityGraphsInitialized; + // internal lock: must stay serializable, this factory is serialized as-is + private final ReentrantLock _namedQueriesLock = new ReentrantLock(); + private volatile boolean _namedQueriesInitialized; private transient Map properties; private transient Map emEmptyPropsProperties; @@ -926,9 +931,78 @@ public Class getClass(T entity) { return OpenJPAPersistenceUtil.getClass(this, entity); } + /** + * Return the named queries whose declared result type is assignable to the given result type. + *

+ * The declared result type of a query is {@link QueryMetaData#getResultType()} if set, otherwise + * {@link QueryMetaData#getCandidateType()} - managed result classes are recorded as the candidate + * type by the metadata parsers, exactly as {@code createNamedQuery(name, resultClass)} would do. + * Named queries without any declared result type (i.e. a JPQL {@code @NamedQuery} without + * {@code resultClass}, or a query declared in XML - the bundled orm_3_2 schema does not allow a + * {@code result-class} attribute on {@code named-query}) are never returned, for any result type + * including {@code Object.class}, because their result type cannot be proven. + *

+ * The hints carried by the returned references are the hints of the named query, including the + * OpenJPA specific hints synthesized by the parsers (e.g. {@code openjpa.FetchPlan.ReadLockMode} + * derived from {@code @NamedQuery.lockMode()}); these are exactly the hints replayed by + * {@code createNamedQuery}. + *

+ * A freshly computed, modifiable map is returned on every call; it is not a live view of the + * metadata repository. + */ @Override + @SuppressWarnings("unchecked") public Map> getNamedQueries(Class resultType) { - throw new UnsupportedOperationException("Not yet implemented (JPA 3.2)"); + if (resultType == null) { + throw new IllegalArgumentException("resultType is required"); + } + _factory.assertOpen(); + loadQueryMetaData(); + Map> result = new HashMap<>(); + for (QueryMetaData qmd : getConfiguration().getMetaDataRepositoryInstance().getQueryMetaDatas()) { + Class declared = qmd.getResultType(); + if (declared == null) { + declared = qmd.getCandidateType(); + } + if (declared == null || !resultType.isAssignableFrom(declared)) { + continue; + } + String[] keys = qmd.getHintKeys(); + Object[] values = qmd.getHintValues(); + Map hints = new LinkedHashMap<>(); + for (int i = 0; i < keys.length && i < values.length; i++) { + hints.put(keys[i], values[i]); + } + result.put(qmd.getName(), + new TypedQueryReferenceImpl<>(qmd.getName(), (Class) declared, hints)); + } + return result; + } + + /** + * Force loading of all persistent types so that their named queries are registered in the + * metadata repository. Mirrors the private {@code MetaDataRepository.resolveAll()} that + * {@code getQueryMetaData(...)} uses for the same reason: on a cold repository + * {@code getQueryMetaDatas()} would otherwise return an empty array. + */ + private void loadQueryMetaData() { + if (_namedQueriesInitialized) { + return; + } + _namedQueriesLock.lock(); + try { + if (_namedQueriesInitialized) { + return; + } + MetaDataRepository mdr = getConfiguration().getMetaDataRepositoryInstance(); + ClassLoader loader = getConfiguration().getClassResolverInstance().getClassLoader(null, null); + for (Class cls : mdr.loadPersistentTypes(false, loader)) { + mdr.getMetaData(cls, loader, false); + } + _namedQueriesInitialized = true; + } finally { + _namedQueriesLock.unlock(); + } } @Override diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java index bd693bb52c..6ce4b39210 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java @@ -33,6 +33,7 @@ import java.sql.Connection; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.IdentityHashMap; @@ -41,6 +42,7 @@ import java.util.Map; import java.util.Set; +import jakarta.persistence.AttributeNode; import jakarta.persistence.CacheRetrieveMode; import jakarta.persistence.CacheStoreMode; import jakarta.persistence.ConnectionConsumer; @@ -55,6 +57,7 @@ import jakarta.persistence.Query; import jakarta.persistence.RefreshOption; import jakarta.persistence.StoredProcedureQuery; +import jakarta.persistence.Subgraph; import jakarta.persistence.Timeout; import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; @@ -98,6 +101,7 @@ import org.apache.openjpa.meta.MultiQueryMetaData; import org.apache.openjpa.meta.QueryMetaData; import org.apache.openjpa.meta.SequenceMetaData; +import org.apache.openjpa.meta.ValueMetaData; import org.apache.openjpa.persistence.meta.MetamodelImpl; import org.apache.openjpa.persistence.criteria.CriteriaBuilderImpl; import org.apache.openjpa.persistence.criteria.OpenJPACriteriaBuilder; @@ -721,6 +725,23 @@ public T find(Class cls, Object oid, @Override public T find(Class cls, Object oid, FindOption... options) { Map props = new HashMap<>(); + LockModeType mode = parseFindOptions(props, options); + return find(cls, oid, mode, props); + } + + /** + * Translates the given JPA 3.2 {@link FindOption}s into kernel level properties, which are put into the + * given (modifiable) property map, and returns the {@link LockModeType} found among the options, if any. + * Unknown (custom) options are ignored. + * + * @param props the map to receive the translated properties, never null + * @param options the options to translate, may be null or empty + * @return the lock mode given among the options or null if none was given + */ + private LockModeType parseFindOptions(Map props, FindOption... options) { + if (options == null) { + return null; + } LockModeType mode = null; for (FindOption opt: options) { if (opt instanceof LockModeType lmt) { @@ -736,7 +757,7 @@ public T find(Class cls, Object oid, FindOption... options) { } // open to custom options } - return find(cls, oid, mode, props); + return mode; } @Override @@ -2640,9 +2661,138 @@ private QueryMetaData getQueryMetadata(String name) { return meta; } + /** + * Finds an instance of the root entity type of the given entity graph by primary key, using the graph + * as a load graph as mandated by JPA 3.2: every attribute named by the graph (and, recursively, + * by its subgraphs) is fetched eagerly, while attributes not mentioned by the graph keep their declared + * fetch behaviour. This is in contrast to a fetch graph, which would additionally force every attribute + * outside the graph to be lazy; therefore the default fetch group is deliberately left untouched here. + *

+ * The graph is applied to a fetch plan that is pushed for the duration of this call only, so neither the + * entity manager's fetch plan nor its maximum fetch depth is affected once the call returns. + * + * @param entityGraph the load graph, its root type determines the type to look up + * @param primaryKey the primary key of the instance to find + * @param options optional find options, interpreted exactly as by {@link #find(Class, Object, FindOption...)} + * @return the found instance or null if no instance with the given primary key exists + * @throws IllegalArgumentException if the graph is null, is not an OpenJPA entity graph, its root type is + * not an entity type, or the primary key is null or of an invalid type + */ @Override + @SuppressWarnings("unchecked") public T find(EntityGraph entityGraph, Object primaryKey, FindOption... options) { - throw new UnsupportedOperationException("Not yet implemented (JPA 3.2)"); + assertNotCloseInvoked(); + if (entityGraph == null) { + throw new IllegalArgumentException("entityGraph is null"); + } + if (!(entityGraph instanceof EntityGraphImpl)) { + throw new IllegalArgumentException("Unknown EntityGraph implementation: " + entityGraph.getClass()); + } + EntityGraphImpl graph = (EntityGraphImpl) entityGraph; + Class cls = graph.getEntityType(); + Map props = new HashMap<>(); + LockModeType mode = parseFindOptions(props, options); + try { + validateFindArguments(cls, primaryKey); + configureCurrentCacheModes(pushFetchPlan(), props); + try { + FetchPlan fetch = getFetchPlan(); + configureCurrentFetchPlan(fetch, props, mode, true); + applyEntityGraph(fetch, graph); + Object oid = _broker.newObjectId(cls, primaryKey); + return (T) _broker.find(oid, true, this); + } finally { + popFetchPlan(); + } + } catch (RuntimeException re) { + throw translateException(re); + } + } + + /** + * Applies the given entity graph to the given (already pushed) fetch plan as a load graph, and raises the + * plan's maximum fetch depth if - and only if - the graph is nested deeper than the currently configured + * finite depth. A max fetch depth of {@link FetchPlan#DEPTH_INFINITE} already covers any graph and is never + * lowered. + */ + private void applyEntityGraph(FetchPlan fetch, EntityGraphImpl graph) { + MetaDataRepository repos = _broker.getConfiguration().getMetaDataRepositoryInstance(); + ClassMetaData meta = toMetaData(repos, graph.getEntityType()); + if (meta == null) { + return; + } + int depth = applyGraphNodes(fetch, meta, graph.getAttributeNodes(), repos, + Collections.newSetFromMap(new IdentityHashMap())); + int max = fetch.getMaxFetchDepth(); + if (max != FetchPlan.DEPTH_INFINITE && max < depth) { + fetch.setMaxFetchDepth(depth); + } + } + + /** + * Adds every attribute named by the given graph nodes to the given fetch plan and recurses into their + * subgraphs. Nodes that do not name a persistent attribute of the given type are silently skipped, so a + * loosely declared named entity graph cannot turn a find() into a hard failure. + * + * @return the number of relation levels spanned by the given nodes, 0 if there are none + */ + private int applyGraphNodes(FetchPlan fetch, ClassMetaData meta, List> nodes, + MetaDataRepository repos, Set path) { + if (meta == null || nodes == null || nodes.isEmpty()) { + return 0; + } + int depth = 1; + for (AttributeNode node : nodes) { + FieldMetaData fmd = meta.getField(node.getAttributeName()); + if (fmd == null) { + continue; + } + // the full name is declaringType.fieldName, which is what FetchConfiguration matches against; + // building the name from the root class instead would silently miss inherited attributes + fetch.addField(fmd.getFullName(false)); + ValueMetaData element = fmd.getElement(); + ValueMetaData key = fmd.getKey(); + depth = Math.max(depth, 1 + applySubgraphs(fetch, node.getSubgraphs(), + (element == null) ? null : element.getDeclaredType(), repos, path)); + depth = Math.max(depth, 1 + applySubgraphs(fetch, node.getKeySubgraphs(), + (key == null) ? null : key.getDeclaredType(), repos, path)); + } + return depth; + } + + /** + * Recurses into the given subgraphs. The type declared by a subgraph is preferred, but for a plural + * attribute it may be the collection type rather than the element type, in which case the declared type of + * the owning value is used instead. Recursion is bounded by a path scoped identity guard, so a graph that + * references itself terminates while a subgraph shared by sibling branches is still expanded. + * + * @return the number of relation levels spanned below the given subgraphs + */ + private int applySubgraphs(FetchPlan fetch, Map subgraphs, Class fallbackType, + MetaDataRepository repos, Set path) { + int depth = 0; + if (subgraphs == null || subgraphs.isEmpty()) { + return depth; + } + for (Subgraph sub : subgraphs.values()) { + if (sub == null || !path.add(sub)) { + continue; + } + try { + ClassMetaData subMeta = toMetaData(repos, sub.getClassType()); + if (subMeta == null) { + subMeta = toMetaData(repos, fallbackType); + } + depth = Math.max(depth, applyGraphNodes(fetch, subMeta, sub.getAttributeNodes(), repos, path)); + } finally { + path.remove(sub); + } + } + return depth; + } + + private ClassMetaData toMetaData(MetaDataRepository repos, Class cls) { + return (cls == null) ? null : repos.getMetaData(cls, _broker.getClassLoader(), false); } @Override @@ -2775,8 +2925,29 @@ public TypedQuery createQuery(CriteriaSelect selectQuery) { } @Override + @SuppressWarnings("unchecked") public TypedQuery createQuery(TypedQueryReference reference) { - throw new UnsupportedOperationException("Not yet implemented (JPA 3.2)"); + assertNotCloseInvoked(); + if (reference == null) { + throw new IllegalArgumentException("TypedQueryReference must not be null"); + } + String name = reference.getName(); + if (name == null) { + throw new IllegalArgumentException("TypedQueryReference name must not be null"); + } + Class resultType = reference.getResultType(); + Query query = (resultType == null) ? createNamedQuery(name) : createNamedQuery(name, resultType); + Map hints = reference.getHints(); + if (hints != null) { + try { + for (Map.Entry hint : hints.entrySet()) { + query.setHint(hint.getKey(), hint.getValue()); + } + } catch (RuntimeException re) { + throw translateException(re); + } + } + return (TypedQuery) query; } @Override diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/TypedQueryReferenceImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/TypedQueryReferenceImpl.java new file mode 100644 index 0000000000..cbdd14dff5 --- /dev/null +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/TypedQueryReferenceImpl.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.openjpa.persistence; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import jakarta.persistence.TypedQueryReference; + +/** + * Immutable implementation of the JPA 3.2 {@link TypedQueryReference}. Instances are handed out by + * {@link EntityManagerFactoryImpl#getNamedQueries(Class)} and can be passed back to + * {@link jakarta.persistence.EntityManager#createQuery(TypedQueryReference)}. + * + * @param the result type of the referenced named query + * + * @since 4.2.0 + */ +public class TypedQueryReferenceImpl implements TypedQueryReference { + + private final String _name; + private final Class _resultType; + private final Map _hints; + + /** + * Constructor. + * + * @param name the name of the named query, must not be null + * @param resultType the declared result type of the named query, must not be null + * @param hints the query hints, may be null or empty. A defensive, unmodifiable copy is taken. + */ + public TypedQueryReferenceImpl(String name, Class resultType, Map hints) { + if (name == null) + throw new IllegalArgumentException("name is required"); + if (resultType == null) + throw new IllegalArgumentException("resultType is required"); + _name = name; + _resultType = resultType; + // deliberately not Map.copyOf(): its iteration order is randomized per JVM run, while the hints + // must be replayed in metadata declaration order (see EntityManager#createQuery(TypedQueryReference)), + // and it would reject null keys or values that this constructor has always tolerated + _hints = (hints == null || hints.isEmpty()) + ? Map.of() + : Collections.unmodifiableMap(new LinkedHashMap<>(hints)); + } + + @Override + public String getName() { + return _name; + } + + @Override + public Class getResultType() { + return _resultType; + } + + /** + * The hints declared for this named query. The returned map is unmodifiable. + */ + @Override + public Map getHints() { + return _hints; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof TypedQueryReferenceImpl)) + return false; + TypedQueryReferenceImpl o = (TypedQueryReferenceImpl) other; + return _name.equals(o._name) && _resultType.equals(o._resultType) && _hints.equals(o._hints); + } + + @Override + public int hashCode() { + return Objects.hash(_name, _resultType, _hints); + } + + @Override + public String toString() { + return "TypedQueryReference[name=" + _name + ", resultType=" + _resultType.getName() + "]"; + } +}