From 3086df6a0f5355bc38c17fb8651de4a6245275f2 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sun, 13 Jul 2025 16:55:29 +0200 Subject: [PATCH 1/2] HHH-19612 allow EntityGraph names in the JPA standard hints and fix a mistake in the Javadoc fot SharedSessionContract --- .../org/hibernate/SharedSessionContract.java | 9 +++-- .../engine/spi/SessionImplementor.java | 10 ------ .../spi/SharedSessionContractImplementor.java | 10 ++++++ .../spi/SharedSessionDelegatorBaseImpl.java | 7 ++-- .../spi/AbstractCommonQueryContract.java | 36 +++++++++++++++---- .../query/sql/internal/NativeQueryImpl.java | 4 +-- 6 files changed, 51 insertions(+), 25 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/SharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/SharedSessionContract.java index 9c1b0ef123c7..bf1ced5dff72 100644 --- a/hibernate-core/src/main/java/org/hibernate/SharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/SharedSessionContract.java @@ -375,23 +375,26 @@ public interface SharedSessionContract extends QueryProducer, AutoCloseable, Ser * @throws IllegalArgumentException if the graph with the given * name does not have the given entity type as its root * + * @see jakarta.persistence.EntityManager#createEntityGraph(String) + * * @since 6.3 */ RootGraph createEntityGraph(Class rootType, String graphName); /** * Obtain an immutable reference to a predefined - * {@linkplain jakarta.persistence.NamedEntityGraph named entity graph} - * or return {@code null} if there is no predefined graph with the given - * name. + * {@linkplain jakarta.persistence.NamedEntityGraph named entity graph}. * * @param graphName the name of the predefined named entity graph + * @throws IllegalArgumentException if there is no predefined graph + * with the given name * * @apiNote This method returns {@code RootGraph}, requiring an * unchecked typecast before use. It's cleaner to obtain a graph using * the static metamodel for the class which defines the graph, or by * calling {@link SessionFactory#getNamedEntityGraphs(Class)} instead. * + * @see jakarta.persistence.EntityManager#getEntityGraph(String) * @see SessionFactory#getNamedEntityGraphs(Class) * @see jakarta.persistence.EntityManagerFactory#addNamedEntityGraph(String, EntityGraph) * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionImplementor.java index 6b5f669f0972..bf62391fefe9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionImplementor.java @@ -10,7 +10,6 @@ import org.hibernate.Session; import org.hibernate.engine.jdbc.LobCreationContext; import org.hibernate.engine.jdbc.spi.JdbcCoordinator; -import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.resource.jdbc.spi.JdbcSessionOwner; @@ -66,15 +65,6 @@ default SessionImplementor getSession() { @Override SessionFactoryImplementor getSessionFactory(); - @Override - RootGraphImplementor createEntityGraph(Class rootType); - - @Override - RootGraphImplementor createEntityGraph(String graphName); - - @Override - RootGraphImplementor getEntityGraph(String graphName); - @Override QueryImplementor createQuery(CriteriaSelect selectQuery); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java index c304cb9f899c..2cc3517198a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionContractImplementor.java @@ -20,6 +20,7 @@ import org.hibernate.bytecode.enhance.spi.interceptor.SessionAssociationMarkers; import org.hibernate.dialect.Dialect; import org.hibernate.event.spi.EventSource; +import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.query.Query; import org.hibernate.SharedSessionContract; import org.hibernate.Transaction; @@ -584,4 +585,13 @@ default boolean isStatelessSession() { */ @Incubating SessionAssociationMarkers getSessionAssociationMarkers(); + + @Override + RootGraphImplementor createEntityGraph(Class rootType); + + @Override + RootGraphImplementor createEntityGraph(String graphName); + + @Override + RootGraphImplementor getEntityGraph(String graphName); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 2031afbee78e..5175ea06e686 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -29,6 +29,7 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.graph.RootGraph; +import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.Work; import org.hibernate.persister.entity.EntityPersister; @@ -632,12 +633,12 @@ public TimeZone getJdbcTimeZone() { } @Override - public RootGraph createEntityGraph(Class rootType) { + public RootGraphImplementor createEntityGraph(Class rootType) { return delegate.createEntityGraph( rootType ); } @Override - public RootGraph createEntityGraph(String graphName) { + public RootGraphImplementor createEntityGraph(String graphName) { return delegate.createEntityGraph( graphName ); } @@ -647,7 +648,7 @@ public RootGraph createEntityGraph(Class rootType, String graphName) { } @Override - public RootGraph getEntityGraph(String graphName) { + public RootGraphImplementor getEntityGraph(String graphName) { return delegate.getEntityGraph( graphName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java index 8aab94753782..0845c85a2275 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java @@ -354,13 +354,13 @@ protected final boolean applySelectionHint(String hintName, Object value) { DEPRECATION_LOGGER.deprecatedSetting( HINT_JAVAEE_FETCH_GRAPH, HINT_SPEC_FETCH_GRAPH ); //fall through to: case HINT_SPEC_FETCH_GRAPH: - applyEntityGraphHint( hintName, value ); + applyEntityGraphHint( GraphSemantic.FETCH, value, hintName ); return true; case HINT_JAVAEE_LOAD_GRAPH: DEPRECATION_LOGGER.deprecatedSetting( HINT_JAVAEE_LOAD_GRAPH, HINT_SPEC_LOAD_GRAPH ); //fall through to: case HINT_SPEC_LOAD_GRAPH: - applyEntityGraphHint( hintName, value ); + applyEntityGraphHint( GraphSemantic.LOAD, value, hintName ); return true; case HINT_FETCH_PROFILE: queryOptions.enableFetchProfile( (String) value ); @@ -371,17 +371,39 @@ protected final boolean applySelectionHint(String hintName, Object value) { } } - protected void applyEntityGraphHint(String hintName, Object value) { - final GraphSemantic graphSemantic = GraphSemantic.fromHintName( hintName ); + protected void applyEntityGraphHint(GraphSemantic graphSemantic, Object value, String hintName) { if ( value instanceof RootGraphImplementor rootGraphImplementor ) { applyGraph( rootGraphImplementor, graphSemantic ); } else if ( value instanceof String string ) { - applyGraph( string, graphSemantic ); + // try and interpret it as the name of a @NamedEntityGraph + final var entityGraph = getEntityGraph( string ); + if ( entityGraph == null ) { + try { + // try and parse it in the entity graph language + applyGraph( string, graphSemantic ); + } + catch ( IllegalArgumentException e ) { + throw new IllegalArgumentException( "The string value of the hint '" + hintName + + "' must be the name of a named EntityGraph, or a representation understood by GraphParser" ); + } + } + else { + applyGraph( entityGraph, graphSemantic ); + } } else { throw new IllegalArgumentException( "The value of the hint '" + hintName - + "' must be an instance of EntityGraph or the string name of a named EntityGraph" ); + + "' must be an instance of EntityGraph, the string name of a named EntityGraph, or a string representation understood by GraphParser" ); + } + } + + private RootGraphImplementor getEntityGraph(String string) { + try { + return getSession().getEntityGraph( string ); + } + catch ( IllegalArgumentException e ) { + return null; } } @@ -392,7 +414,7 @@ protected void applyGraph(String graphString, GraphSemantic graphSemantic) { throw new IllegalArgumentException( String.format( ROOT, - "Invalid entity-graph definition `%s`; expected form `${EntityName}( ${property1} ... )", + "Invalid entity-graph definition '%s'; expected form '${EntityName}( ${property1} ... )'", graphString ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 00767e558a4f..d7ba2a44f9ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -673,8 +673,8 @@ public Query applyGraph(@SuppressWarnings("rawtypes") RootGraph graph, GraphS } @Override - protected void applyEntityGraphHint(String hintName, Object value) { - super.applyEntityGraphHint( hintName, value ); + protected void applyEntityGraphHint(GraphSemantic graphSemantic, Object value, String hintName) { + super.applyEntityGraphHint( graphSemantic, value, hintName ); } @Override From 72af4a4a5b1f7de7c08345477d2324baf6e28350 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sun, 13 Jul 2025 17:40:27 +0200 Subject: [PATCH 2/2] HHH-19612 test EntityGraph name in the JPA standard hint --- ...ithInParamListAndNamedEntityGraphTest.java | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryWithInParamListAndNamedEntityGraphTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryWithInParamListAndNamedEntityGraphTest.java index 2d1521c4317a..2d310a38215a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryWithInParamListAndNamedEntityGraphTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/QueryWithInParamListAndNamedEntityGraphTest.java @@ -7,21 +7,25 @@ import java.util.HashSet; import java.util.Set; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.NamedAttributeNode; import jakarta.persistence.NamedEntityGraph; import jakarta.persistence.Table; -import jakarta.persistence.TypedQuery; +import org.hibernate.jpa.SpecHints; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hibernate.Hibernate.isInitialized; +import static org.junit.jupiter.api.Assertions.assertTrue; + /** * Based on the test developed by Hans Desmet to reproduce the bug reported in HHH-9230 * @@ -33,6 +37,23 @@ }) public class QueryWithInParamListAndNamedEntityGraphTest { + @BeforeAll + void prepareTest(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Person boss = new Person(); + boss.id = 2L; + boss.name = "B"; + entityManager.persist( boss ); + Person person = new Person(); + person.id = 1L; + person.name = "X"; + person.boss = boss; + entityManager.persist( person ); + } + ); + } + @Test public void testInClause(EntityManagerFactoryScope scope) { // this test works @@ -41,9 +62,9 @@ public void testInClause(EntityManagerFactoryScope scope) { Set ids = new HashSet<>(); ids.add( 1L ); ids.add( 2L ); - TypedQuery query = entityManager.createQuery( "select p from Person p where p.id in :ids", Person.class ); - query.setParameter( "ids", ids ); - query.getResultList(); + entityManager.createQuery( "select p from Person p where p.id in :ids", Person.class ) + .setParameter( "ids", ids ) + .getResultList(); } ); } @@ -53,9 +74,10 @@ public void testEntityGraph(EntityManagerFactoryScope scope) { // this test works scope.inTransaction( entityManager -> { - TypedQuery query = entityManager.createQuery( "select p from Person p", Person.class ); - query.setHint( "javax.persistence.loadgraph", entityManager.createEntityGraph( "withBoss" ) ); - query.getResultList(); + entityManager.createQuery( "select p from Person p", Person.class ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, + entityManager.createEntityGraph( QueryWithInParamListAndNamedEntityGraphTest_.Person_.GRAPH_WITH_BOSS ) ) + .getResultList(); } ); } @@ -68,23 +90,38 @@ public void testEntityGraphAndInClause(EntityManagerFactoryScope scope) { Set ids = new HashSet<>(); ids.add( 1L ); ids.add( 2L ); - TypedQuery query = entityManager.createQuery( "select p from Person p where p.id in :ids", Person.class ); - query.setHint( "javax.persistence.loadgraph", entityManager.createEntityGraph( "withBoss" ) ); - query.setParameter( "ids", ids ); - query.getResultList(); + entityManager.createQuery( "select p from Person p where p.id in :ids", Person.class ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, + entityManager.createEntityGraph( QueryWithInParamListAndNamedEntityGraphTest_.Person_.GRAPH_WITH_BOSS ) ) + .setParameter( "ids", ids ) + .getResultList(); + } + ); + } + + @Test + public void testNamedEntityGraph(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Person person = + entityManager.createQuery( "select p from Person p where p.boss is not null", Person.class ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, + QueryWithInParamListAndNamedEntityGraphTest_.Person_.GRAPH_WITH_BOSS ) + .getSingleResult(); + assertTrue( isInitialized( person.boss ) ); } ); } @Entity(name = "Person") @Table(name = "Person") - @NamedEntityGraph(name = "withBoss", attributeNodes = @NamedAttributeNode("boss")) + @NamedEntityGraph(name = "withBoss", + attributeNodes = @NamedAttributeNode("boss")) public static class Person { @Id - @GeneratedValue private long id; private String name; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private Person boss; }