Skip to content

[OPENJPA-2985] Implement the mandatory JPA 3.2 methods that threw UnsupportedOperationException - #156

Open
rzo1 wants to merge 5 commits into
masterfrom
OPENJPA-2985
Open

[OPENJPA-2985] Implement the mandatory JPA 3.2 methods that threw UnsupportedOperationException#156
rzo1 wants to merge 5 commits into
masterfrom
OPENJPA-2985

Conversation

@rzo1

@rzo1 rzo1 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Implements the three mandatory JPA 3.2 methods that were still throwing UnsupportedOperationException("Not yet implemented (JPA 3.2)"), one commit each with tests: EntityManager.createQuery(TypedQueryReference), EntityManagerFactory.getNamedQueries(Class) and EntityManager.find(EntityGraph, Object, FindOption...).

createQuery(TypedQueryReference) delegates to createNamedQuery(name, resultType) and applies the reference's hints. getNamedQueries(Class) needed metadata support first, so AnnotationPersistenceMetaDataParser now honours the JPA 3.2 @NamedQuery.resultClass() the same way it already honoured @NamedNativeQuery.resultClass(); the method then returns a fresh map of TypedQueryReference for every named query whose declared result type is assignable to the requested one. find(EntityGraph, ...) resolves the root type from the graph and applies it as a load graph to the pushed fetch plan (with a cycle guard), which is the first place an EntityGraph actually influences loading in OpenJPA — until now EntityGraphImpl was a pure data holder.

Two known gaps worth a reviewer's opinion: named queries without a declared result type are excluded from getNamedQueries for every requested type including Object.class, and because the bundled orm_3_2.xsd.rsrc still carries the 3.1-shaped named-query type without result-class, XML-declared named queries can never be returned — that stale schema looks like a separate follow-up.

rzo1 added 3 commits August 20, 2026 12:29
EntityManagerImpl.createQuery(TypedQueryReference<T>) no longer throws
UnsupportedOperationException. It now:
 - calls assertNotCloseInvoked() first, so a closed EntityManager yields
   IllegalStateException;
 - rejects a null reference (and a null reference name) with
   IllegalArgumentException;
 - delegates to createNamedQuery(name, resultType) so the named query
   metadata (query string, hints, flush mode, max results, lock mode) is
   applied and the reference's result type becomes the query result class;
   an unknown query name propagates as ArgumentException, which extends
   IllegalArgumentException, matching the createNamedQuery contract;
 - applies the hints carried by the reference afterwards, so they take
   precedence over hints declared on the @NamedQuery itself; failures there
   go through translateException like the rest of the named query path.

A null result type falls back to createNamedQuery(name) defensively.

Tested by the new TestTypedQueryReference in
org.apache.openjpa.persistence.query, which covers query creation and
execution from a reference, the applied result class, reference hints
surviving in getHints(), positional parameters of the named query, null
reference, unknown query name and a closed EntityManager. All seven cases
failed against the previous UnsupportedOperationException.
EntityManagerFactoryImpl.getNamedQueries(Class) no longer throws
UnsupportedOperationException. It now forces loading of the persistent
types (mirroring the private MetaDataRepository.resolveAll() that
getQueryMetaData() uses, so a cold repository does not yield an empty
result), iterates the query metadata and returns a freshly built map of
TypedQueryReference for every named query whose declared result type is
assignable to the requested type. The declared result type is
QueryMetaData.getResultType(), falling back to getCandidateType(), which
makes the predicate exactly "createNamedQuery(name, resultType) is
legal". Named queries without any declared result type are never
returned, for any type including Object.class. The returned map is a
copy; each reference carries the query name, the result type and an
unmodifiable copy of the query hints. A null result type is rejected
with IllegalArgumentException and a closed factory raises the usual
IllegalStateException.

For this to be answerable from metadata,
AnnotationPersistenceMetaDataParser.parseNamedQueries() now honours the
JPA 3.2 @NamedQuery.resultClass() the same way parseNamedNativeQueries()
already honours @NamedNativeQuery.resultClass(): a managed result class
is stored as the candidate type, any other non-void class as the result
type. This is the identical branch that QueryImpl.setResultClass()
performs for createNamedQuery(name, resultClass), so behaviour matches
an explicit result class at the call site; note that setting the
candidate type also makes JPQLExpressionBuilder skip its own candidate
inference, which is the same situation as today's
createNamedQuery(name, X.class).

XMLPersistenceMetaDataParser is deliberately untouched: the bundled
orm_3_2.xsd.rsrc declares only a "name" attribute on <named-query>, so a
"result-class" attribute would fail schema validation before reaching
the parser. Refreshing the schema is left to a separate issue; until
then XML-declared named queries have no declared result type and are
therefore not returned by getNamedQueries().

New TypedQueryReferenceImpl is an immutable TypedQueryReference with
equals/hashCode/toString.

Tested by the new TestGetNamedQueries (with NamedQueryRefEntity, which
declares named queries with an entity resultClass plus a hint, a scalar
resultClass, no resultClass, and a native query with a resultClass):
covers the parser change, entity/scalar/supertype lookups, exclusion of
untyped queries, hints and their immutability, the returned map being a
copy, null argument handling, a cold second factory, addNamedQuery(), and
round-tripping a reference through EntityManager.createQuery().
…Option...)

find(EntityGraph, Object, FindOption...) threw UnsupportedOperationException.
It now looks up an instance of the root entity type of the given graph
(EntityGraphImpl.getEntityType()) by primary key and applies the graph as a
JPA 3.2 load graph: every attribute named by the graph and, recursively, by
its subgraphs and key subgraphs is added to the fetch plan, while attributes
outside the graph keep their declared fetch behaviour - the default fetch
group is deliberately left in place, which is what distinguishes a load graph
from a fetch graph.

Details:
- The graph is applied to a fetch plan pushed for the duration of the call
  only, so neither the entity manager's fetch plan nor its maximum fetch
  depth is affected afterwards. The maximum fetch depth is raised only when
  the graph is nested deeper than a currently configured finite depth; an
  infinite depth (-1) is never lowered.
- Attributes are resolved through ClassMetaData/FieldMetaData and added by
  their full name (declaring type + field), so attributes inherited from a
  mapped superclass or a superclass entity are matched correctly.
- A subgraph's declared class type is preferred, falling back to the declared
  type of the owning value's element/key, because for a plural attribute
  EntityGraphImpl derives the subgraph type from Attribute.getJavaType(),
  which is the collection type.
- Recursion is bounded by a path scoped identity guard, so a graph that
  references itself terminates.
- A null graph, a null primary key, a non-entity root type and a foreign
  EntityGraph implementation all raise IllegalArgumentException; the message
  for a foreign implementation mirrors
  EntityManagerFactoryImpl.addNamedEntityGraph.
- The FindOption parsing of find(Class, Object, FindOption...) was extracted
  into a shared private helper so both overloads behave identically. The only
  observable delta on the existing overload is that an explicit null option
  array is now a no-op instead of an NPE, matching lock(Object, LockModeType,
  LockOption...).

Tested by the new TestEntityGraphFind (10 tests) in the existing entitygraph
test package; EGDepartment gained a lazy inverse collection so a graph has
something to change. The tests assert the load state via the state manager's
loaded bit set and pair every positive assertion with a fresh entity manager
control that asserts the attribute is NOT loaded without the graph, covering
a flat graph, a subgraph, a cyclic graph, find options, a missing row and the
three IllegalArgumentException cases.
@rzo1 rzo1 self-assigned this Aug 20, 2026
@rzo1
rzo1 requested a review from solomax August 20, 2026 10:46
@solomax

solomax commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Build need to be fixed :)

The getNamedQueries(Class) implementation guarded its lazy metadata load
with a dedicated "private final Object _namedQueriesLock = new Object()".
EntityManagerFactoryImpl is serialized field by field (it implements
OpenJPAEntityManagerFactory, which extends Serializable, and the class
declares no writeObject/readObject), so that non-transient bare Object
field made the whole factory unserializable:
java.io.NotSerializableException: java.lang.Object.

The targeted tests for the new API never serialize a factory, so they
stayed green; CI runs the full suite and
org.apache.openjpa.persistence.simple.TestSerializedFactory, which writes
the EntityManagerFactory to an ObjectOutputStream, failed on all four
jobs. The neighbouring _entityGraphs guard did not have this problem
because it locks on a ConcurrentHashMap, which is itself serializable.

Fix: use a java.util.concurrent.locks.ReentrantLock as the monitor, the
same idiom AbstractBrokerFactory already uses for its internal lock in
the kernel. ReentrantLock is Serializable and deserializes unlocked, so
the field can stay final and non-transient, no serialization hooks are
needed, and the double-checked initialization (volatile flag read
outside, re-checked inside the lock, written last) is unchanged.

No new test: TestSerializedFactory already reproduces the failure exactly
and passes with the fix.
props.put(JPAProperties.LOCK_SCOPE, pls);
} else if (opt instanceof Timeout timeout) {
props.put(JPAProperties.LOCK_TIMEOUT, timeout.milliseconds());
if (options != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this one can be flattened a bit with:

if (options == null) {
    return mode;
}

? :)

throw new IllegalArgumentException("resultType is required");
_name = name;
_resultType = resultType;
_hints = (hints == null || hints.isEmpty())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_hints = (hints == null || hints.isEmpty())
_hints = (hints == null || hints.isEmpty()) ? Map.of() : Map.copyOf(hints)

@solomax solomax left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added couple suggestions :)

hopefully others can review as well :)

- EntityManagerImpl.parseFindOptions(): flatten the null check into an
  early return instead of wrapping the whole loop in "if (options != null)".
- TypedQueryReferenceImpl: use Map.of() for the empty-hints branch.

Deviation: the non-empty branch keeps unmodifiableMap(new LinkedHashMap<>(hints))
instead of Map.copyOf(hints), because Map.copyOf randomizes iteration order per
JVM run while createQuery(TypedQueryReference) replays the hints in that order
and OpenJPA has aliased hint keys (openjpa.FetchPlan.LockTimeout /
jakarta.persistence.lock.timeout, likewise for QueryTimeout) where the last write
wins, and because Map.copyOf rejects null keys and values that this public
constructor has always tolerated. TestGetNamedQueries now pins the order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants