diff --git a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc index 88e6f2f95d81..18cea708582f 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc @@ -285,7 +285,8 @@ the subtype Class. Hibernate allows the creation of Jakarta Persistence fetch/load graphs by parsing a textual representation of the graph. Generally speaking, the textual representation of a graph is a comma-separated list of attribute names, optionally including any subgraph specifications. -`org.hibernate.graph.GraphParser` is the starting point for such parsing operations. +The starting point for such parsing operations is either `org.hibernate.graph.GraphParser` +or `SessionFactory#parseEntityGraph` [NOTE] ==== @@ -370,6 +371,28 @@ include::{example-dir-fetching}/GraphParsingTest.java[tags=fetching-strategies-d ==== +[[fetching-strategies-dynamic-fetching-entity-graph-parsing-annotation]] +==== @NamedEntityGraph with text representation + +Hibernate also offers a `@org.hibernate.annotations.NamedEntityGraph` annotation, as a corollary +to the `@jakarta.persistence.NamedEntityGraph`, supporting the text representation +<>. The annotation +may be placed on an entity or on a package. + + +.@NamedEntityGraph example +==== +[source, java, indent=0] +---- +@Entity +@NamedEntityGraph( graph="title,isbn,author(name,phoneNumber)" ) +class Book { + // ... +} +---- +==== + + [[fetching-strategies-dynamic-fetching-profile]] === Dynamic fetching via Hibernate profiles diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 index b3737f2086b5..b8036c9e1c81 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/graph/GraphLanguageParser.g4 @@ -25,9 +25,13 @@ package org.hibernate.grammars.graph; graph - : attributeList + : typeIndicator? attributeList ; +typeIndicator + : TYPE_NAME COLON + ; + attributeList : attributeNode (COMMA attributeNode)* ; @@ -45,9 +49,6 @@ attributeQualifier ; subGraph - : LPAREN (subType COLON)? attributeList RPAREN + : LPAREN typeIndicator? attributeList RPAREN ; -subType - : TYPE_NAME - ; diff --git a/hibernate-core/src/main/java/org/hibernate/SessionFactory.java b/hibernate-core/src/main/java/org/hibernate/SessionFactory.java index 26694b2b6fdd..827f791190da 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionFactory.java @@ -4,27 +4,31 @@ */ package org.hibernate; -import java.io.Serializable; -import java.sql.Connection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.naming.Referenceable; - +import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.FindOption; import jakarta.persistence.SynchronizationType; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.engine.spi.FilterDefinition; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.graph.GraphParser; +import org.hibernate.graph.InvalidGraphException; import org.hibernate.graph.RootGraph; +import org.hibernate.graph.internal.RootGraphImpl; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.relational.SchemaManager; import org.hibernate.stat.Statistics; -import jakarta.persistence.EntityGraph; -import jakarta.persistence.EntityManagerFactory; +import javax.naming.Referenceable; +import java.io.Serializable; +import java.sql.Connection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; import static org.hibernate.internal.TransactionManagement.manageTransaction; @@ -414,6 +418,20 @@ default R fromStatelessTransaction(Function acti */ RootGraph findEntityGraphByName(String name); + /** + * + * Create an {@link EntityGraph} for the given entity type. + * + * @param entityType The entity type for the graph + * + * @see #createGraphForDynamicEntity(String) + * + * @since 7.0 + */ + default RootGraph createEntityGraph(Class entityType) { + return new RootGraphImpl<>( null, (EntityDomainType) getMetamodel().entity( entityType ) ); + } + /** * Create an {@link EntityGraph} which may be used from loading a * {@linkplain org.hibernate.metamodel.RepresentationMode#MAP dynamic} @@ -432,9 +450,69 @@ default R fromStatelessTransaction(Function acti * @since 7.0 * * @see Session#find(EntityGraph, Object, FindOption...) + * @see #createEntityGraph(Class) */ RootGraph> createGraphForDynamicEntity(String entityName); + /** + * Creates a RootGraph for the given {@code rootEntityClass} and parses the graph text into + * it. + * + * @param rootEntityClass The entity class to use as the base of the created root-graph + * @param graphText The textual representation of the graph + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @see GraphParser#parse(Class, CharSequence, SessionFactory) + * @see #createEntityGraph(Class) + * + * @apiNote The string representation is expected to just be an attribute list. E.g. + * {@code "title, isbn, author(name, books)"} + * + * @since 7.0 + */ + default RootGraph parseEntityGraph(Class rootEntityClass, CharSequence graphText) { + return GraphParser.parse( rootEntityClass, graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); + } + + /** + * Creates a RootGraph for the given {@code rootEntityName} and parses the graph text into + * it. + * + * @param rootEntityName The name of the entity to use as the base of the created root-graph + * @param graphText The textual representation of the graph + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @see GraphParser#parse(String, CharSequence, SessionFactory) + * @see #createEntityGraph(Class) + * + * @apiNote The string representation is expected to just be an attribute list. E.g. + * {@code "title, isbn, author(name, books)"} + * + * @since 7.0 + */ + default RootGraph parseEntityGraph(String rootEntityName, CharSequence graphText) { + return GraphParser.parse( rootEntityName, graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); + } + + /** + * Creates a RootGraph based on the passed string representation. Here, the + * string representation is expected to include the root entity name. + * + * @param graphText The textual representation of the graph + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @apiNote The string representation is expected to an attribute list prefixed + * with the name of the root entity. E.g. {@code "Book: title, isbn, author(name, books)"} + * + * @since 7.0 + */ + default RootGraph parseEntityGraph(CharSequence graphText) { + return GraphParser.parse( graphText.toString(), unwrap( SessionFactoryImplementor.class ) ); + } + /** * Obtain the set of names of all {@link org.hibernate.annotations.FilterDef * defined filters}. diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java new file mode 100644 index 000000000000..bda69efd79db --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraph.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import org.hibernate.SessionFactory; +import org.hibernate.graph.GraphParser; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Defines a named {@linkplain EntityGraph entity graph} + * based on Hibernate's {@linkplain org.hibernate.graph.GraphParser entity graph language}. + *

+ * When applied to a root entity class, the root entity name is implied - e.g. {@code "title, isbn, author(name, books)"} + *

+ * When applied to a package, the root entity name must be specified - e.g. {@code "Book: title, isbn, author(name, books)"} + * + * @see EntityManager#getEntityGraph(String) + * @see org.hibernate.SessionFactory#parseEntityGraph(CharSequence) + * @see GraphParser#parse(CharSequence, SessionFactory) + * + * @see org.hibernate.graph.GraphParser + * @see jakarta.persistence.NamedEntityGraph + * + * @since 7.0 + * @author Steve Ebersole + */ +@Target({TYPE, PACKAGE}) +@Retention(RUNTIME) +@Repeatable(NamedEntityGraphs.class) +public @interface NamedEntityGraph { + /** + * The name used to identify the entity graph in calls to + * {@linkplain org.hibernate.Session#getEntityGraph(String)}. + * Entity graph names must be unique within the persistence unit. + *

+ * When applied to a root entity class, the name is optional and + * defaults to the entity-name of that entity. + */ + String name() default ""; + + /** + * The textual representation of the graph. + *

+ * When applied to a package, the syntax requires the entity name - e.g., {@code "Book: title, isbn, author(name, books)"}. + *

+ * When applied to an entity, the entity name should be omitted - e.g., {@code "title, isbn, author(name, books)"}. + *

+ * See {@linkplain org.hibernate.graph.GraphParser} for details about the syntax. + */ + String graph(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraphs.java b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraphs.java new file mode 100644 index 000000000000..d2a6724a9fd5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NamedEntityGraphs.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.annotations; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A grouping of {@link NamedEntityGraph} definitions. + * + * @since 7.0 + * @author Steve Ebersole + */ +@Target({TYPE, PACKAGE, ANNOTATION_TYPE}) +@Retention(RUNTIME) +public @interface NamedEntityGraphs { + /** + * The grouping of Hibernate named native SQL queries. + */ + NamedEntityGraph[] value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/NamedEntityGraphDefinition.java b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedEntityGraphDefinition.java index e88bdd54b6e3..b21e0bdf6e2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/NamedEntityGraphDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedEntityGraphDefinition.java @@ -5,6 +5,7 @@ package org.hibernate.boot.model; import jakarta.persistence.NamedEntityGraph; +import org.hibernate.mapping.PersistentClass; import static org.hibernate.internal.util.StringHelper.isNotEmpty; @@ -14,34 +15,64 @@ * @author Steve Ebersole */ public class NamedEntityGraphDefinition { - private final NamedEntityGraph annotation; - private final String jpaEntityName; - private final String entityName; + public enum Source { JPA, PARSED } + private final String name; - public NamedEntityGraphDefinition(NamedEntityGraph annotation, String jpaEntityName, String entityName) { - this.annotation = annotation; - this.jpaEntityName = jpaEntityName; - this.entityName = entityName; + private final String entityName; + + private final Source source; + private final NamedGraphCreator graphCreator; + + public NamedEntityGraphDefinition(jakarta.persistence.NamedEntityGraph annotation, String jpaEntityName, String entityName) { this.name = isNotEmpty( annotation.name() ) ? annotation.name() : jpaEntityName; if ( name == null ) { throw new IllegalArgumentException( "Named entity graph name cannot be null" ); } + + this.entityName = entityName; + + source = Source.JPA; + graphCreator = new NamedGraphCreatorJpa( annotation, jpaEntityName ); } - public String getRegisteredName() { - return name; + public NamedEntityGraphDefinition(org.hibernate.annotations.NamedEntityGraph annotation, PersistentClass persistentClass) { + this.name = isNotEmpty( annotation.name() ) ? annotation.name() : persistentClass.getJpaEntityName(); + if ( name == null ) { + throw new IllegalArgumentException( "Named entity graph name cannot be null" ); + } + + this.entityName = persistentClass.getEntityName(); + + source = Source.PARSED; + graphCreator = new NamedGraphCreatorParsed( persistentClass.getMappedClass(), annotation ); } - public String getJpaEntityName() { - return jpaEntityName; + public NamedEntityGraphDefinition(org.hibernate.annotations.NamedEntityGraph annotation) { + this.name = annotation.name(); + if ( name == null ) { + throw new IllegalArgumentException( "Named entity graph name cannot be null" ); + } + + this.entityName = null; + + source = Source.PARSED; + graphCreator = new NamedGraphCreatorParsed( annotation ); + } + + public String getRegisteredName() { + return name; } public String getEntityName() { return entityName; } - public NamedEntityGraph getAnnotation() { - return annotation; + public Source getSource() { + return source; + } + + public NamedGraphCreator getGraphCreator() { + return graphCreator; } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreator.java b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreator.java new file mode 100644 index 000000000000..1c990c00f0f3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreator.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.model; + +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.metamodel.model.domain.EntityDomainType; + +import java.util.function.Function; + +/** + * @author Steve Ebersole + */ +@FunctionalInterface +public interface NamedGraphCreator { + RootGraphImplementor createEntityGraph( + Function, EntityDomainType> entityDomainClassResolver, + Function> entityDomainNameResolver); +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorJpa.java b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorJpa.java new file mode 100644 index 000000000000..14e7434d200a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorJpa.java @@ -0,0 +1,155 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.model; + +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.metamodel.Attribute; +import org.hibernate.AnnotationException; +import org.hibernate.graph.internal.RootGraphImpl; +import org.hibernate.graph.spi.AttributeNodeImplementor; +import org.hibernate.graph.spi.GraphImplementor; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.graph.spi.SubGraphImplementor; +import org.hibernate.internal.util.NullnessHelper; +import org.hibernate.metamodel.model.domain.EntityDomainType; + +import java.util.function.Function; + +import static org.hibernate.internal.util.StringHelper.isNotEmpty; + +/** + * @author Steve Ebersole + */ +public class NamedGraphCreatorJpa implements NamedGraphCreator { + private final String name; + private final NamedEntityGraph annotation; + private final String jpaEntityName; + + public NamedGraphCreatorJpa(NamedEntityGraph annotation, String jpaEntityName) { + this.name = NullnessHelper.coalesceSuppliedValues( annotation::name, () -> jpaEntityName ); + this.annotation = annotation; + this.jpaEntityName = jpaEntityName; + } + + @Override + public RootGraphImplementor createEntityGraph( + Function, EntityDomainType> entityDomainClassResolver, + Function> entityDomainNameResolver) { + //noinspection unchecked + final EntityDomainType rootEntityType = (EntityDomainType) entityDomainNameResolver.apply( jpaEntityName ); + final RootGraphImplementor entityGraph = createRootGraph( name, rootEntityType, annotation.includeAllAttributes() ); + + if ( annotation.subclassSubgraphs() != null ) { + for ( NamedSubgraph subclassSubgraph : annotation.subclassSubgraphs() ) { + final Class subgraphType = subclassSubgraph.type(); + final Class graphJavaType = entityGraph.getGraphedType().getJavaType(); + if ( !graphJavaType.isAssignableFrom( subgraphType ) ) { + throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() + + "' is not a subtype of the graph type '" + graphJavaType.getName() + "'" ); + } + @SuppressWarnings("unchecked") // Safe, because we just checked + final Class subtype = (Class) subgraphType; + final GraphImplementor subgraph = entityGraph.addTreatedSubgraph( subtype ); + applyNamedAttributeNodes( subclassSubgraph.attributeNodes(), annotation, subgraph ); + } + } + + if ( annotation.attributeNodes() != null ) { + applyNamedAttributeNodes( annotation.attributeNodes(), annotation, entityGraph ); + } + + return entityGraph; + } + + private static RootGraphImplementor createRootGraph( + String name, + EntityDomainType rootEntityType, + boolean includeAllAttributes) { + final RootGraphImpl entityGraph = new RootGraphImpl<>( name, rootEntityType ); + if ( includeAllAttributes ) { + for ( Attribute attribute : rootEntityType.getAttributes() ) { + entityGraph.addAttributeNodes( attribute ); + } + } + return entityGraph; + } + private void applyNamedAttributeNodes( + NamedAttributeNode[] namedAttributeNodes, + NamedEntityGraph namedEntityGraph, + GraphImplementor graphNode) { + for ( NamedAttributeNode namedAttributeNode : namedAttributeNodes ) { + final String value = namedAttributeNode.value(); + final AttributeNodeImplementor attributeNode = + (AttributeNodeImplementor) graphNode.addAttributeNode( value ); + + if ( isNotEmpty( namedAttributeNode.subgraph() ) ) { + applyNamedSubgraphs( + namedEntityGraph, + namedAttributeNode.subgraph(), + attributeNode, + false + ); + } + if ( isNotEmpty( namedAttributeNode.keySubgraph() ) ) { + applyNamedSubgraphs( + namedEntityGraph, + namedAttributeNode.keySubgraph(), + attributeNode, + true + ); + } + } + } + + private void applyNamedSubgraphs( + NamedEntityGraph namedEntityGraph, + String subgraphName, + AttributeNodeImplementor attributeNode, + boolean isKeySubGraph) { + for ( NamedSubgraph namedSubgraph : namedEntityGraph.subgraphs() ) { + if ( subgraphName.equals( namedSubgraph.name() ) ) { + final Class subgraphType = namedSubgraph.type(); + final SubGraphImplementor subgraph; + if ( subgraphType.equals( void.class ) ) { // unspecified + subgraph = attributeNode.addValueSubgraph(); + } + else { + subgraph = isKeySubGraph + ? makeAttributeNodeKeySubgraph( attributeNode, subgraphType ) + : makeAttributeNodeValueSubgraph( attributeNode, subgraphType ); + } + applyNamedAttributeNodes( namedSubgraph.attributeNodes(), namedEntityGraph, subgraph ); + } + } + } + + private static SubGraphImplementor makeAttributeNodeValueSubgraph( + AttributeNodeImplementor attributeNode, Class subgraphType) { + final Class attributeValueType = + attributeNode.getAttributeDescriptor().getValueGraphType().getBindableJavaType(); + if ( !attributeValueType.isAssignableFrom( subgraphType ) ) { + throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() + + "' is not a subtype of the value type '" + attributeValueType.getName() + "'" ); + } + @SuppressWarnings("unchecked") // Safe, because we just checked + final Class castType = (Class) subgraphType; + return attributeNode.addValueSubgraph().addTreatedSubgraph( castType ); + } + + private static SubGraphImplementor makeAttributeNodeKeySubgraph( + AttributeNodeImplementor attributeNode, Class subgraphType) { + final Class attributeKeyType = + attributeNode.getAttributeDescriptor().getKeyGraphType().getBindableJavaType(); + if ( !attributeKeyType.isAssignableFrom( subgraphType ) ) { + throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() + + "' is not a subtype of the key type '" + attributeKeyType.getName() + "'" ); + } + @SuppressWarnings("unchecked") // Safe, because we just checked + final Class castType = (Class) subgraphType; + return attributeNode.addKeySubgraph().addTreatedSubgraph( castType ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorParsed.java b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorParsed.java new file mode 100644 index 000000000000..f801dd42c55f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/NamedGraphCreatorParsed.java @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.model; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.hibernate.UnknownEntityTypeException; +import org.hibernate.annotations.NamedEntityGraph; +import org.hibernate.grammars.graph.GraphLanguageLexer; +import org.hibernate.grammars.graph.GraphLanguageParser; +import org.hibernate.graph.InvalidGraphException; +import org.hibernate.graph.internal.parse.EntityNameResolver; +import org.hibernate.graph.internal.parse.GraphParsing; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.metamodel.model.domain.EntityDomainType; + +import java.util.function.Function; + +/** + * @author Steve Ebersole + */ +public class NamedGraphCreatorParsed implements NamedGraphCreator { + private final Class entityType; + private final NamedEntityGraph annotation; + + public NamedGraphCreatorParsed(NamedEntityGraph annotation) { + this( null, annotation ); + } + + public NamedGraphCreatorParsed(Class entityType, NamedEntityGraph annotation) { + this.entityType = entityType; + this.annotation = annotation; + } + + @Override + public RootGraphImplementor createEntityGraph( + Function, EntityDomainType> entityDomainClassResolver, + Function> entityDomainNameResolver) { + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( annotation.graph() ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + final EntityNameResolver entityNameResolver = new EntityNameResolver() { + @Override + public EntityDomainType resolveEntityName(String entityName) { + //noinspection unchecked + final EntityDomainType entityDomainType = (EntityDomainType) entityDomainNameResolver.apply( entityName ); + if ( entityDomainType != null ) { + return entityDomainType; + } + throw new UnknownEntityTypeException( entityName ); + } + }; + + if ( entityType == null ) { + if ( graphContext.typeIndicator() == null ) { + throw new InvalidGraphException( "Expecting graph text to include an entity name : " + annotation.graph() ); + } + final String jpaEntityName = graphContext.typeIndicator().TYPE_NAME().toString(); + //noinspection unchecked + final EntityDomainType entityDomainType = (EntityDomainType) entityDomainNameResolver.apply( jpaEntityName ); + return GraphParsing.parse( entityDomainType, graphContext.attributeList(), entityNameResolver ); + } + else { + if ( graphContext.typeIndicator() != null ) { + throw new InvalidGraphException( "Expecting graph text to not include an entity name : " + annotation.graph() ); + } + //noinspection unchecked + final EntityDomainType entityDomainType = (EntityDomainType) entityDomainClassResolver.apply( (Class) entityType ); + return GraphParsing.parse( entityDomainType, graphContext.attributeList(), entityNameResolver ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java index b69b29385e8b..0c0f4790d549 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java @@ -4,10 +4,12 @@ */ package org.hibernate.boot.model.internal; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Table; import org.hibernate.AnnotationException; import org.hibernate.MappingException; import org.hibernate.annotations.CollectionTypeRegistration; @@ -22,6 +24,7 @@ import org.hibernate.annotations.JdbcTypeRegistration; import org.hibernate.annotations.TypeRegistration; import org.hibernate.boot.model.IdentifierGeneratorDefinition; +import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.convert.spi.RegisteredConversion; import org.hibernate.boot.models.HibernateAnnotations; import org.hibernate.boot.models.JpaAnnotations; @@ -37,12 +40,9 @@ import org.hibernate.type.descriptor.java.BasicJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.Inheritance; -import jakarta.persistence.InheritanceType; -import jakarta.persistence.MappedSuperclass; -import jakarta.persistence.Table; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.hibernate.boot.model.internal.AnnotatedClassType.EMBEDDABLE; import static org.hibernate.boot.model.internal.AnnotatedClassType.ENTITY; @@ -73,7 +73,9 @@ private AnnotationBinder() {} public static void bindDefaults(MetadataBuildingContext context) { final GlobalRegistrations globalRegistrations = context.getMetadataCollector().getGlobalRegistrations(); - // id generators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // id generators globalRegistrations.getSequenceGeneratorRegistrations().forEach( (name, generatorRegistration) -> { final IdentifierGeneratorDefinition.Builder definitionBuilder = new IdentifierGeneratorDefinition.Builder(); @@ -95,14 +97,17 @@ public static void bindDefaults(MetadataBuildingContext context) { context.getMetadataCollector().addDefaultIdentifierGenerator( idGenDef ); } ); - // result-set-mappings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // result-set-mappings globalRegistrations.getSqlResultSetMappingRegistrations().forEach( (name, mappingRegistration) -> { QueryBinder.bindSqlResultSetMapping( mappingRegistration.configuration(), context, true ); } ); - // queries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // queries globalRegistrations.getNamedQueryRegistrations().forEach( (name, queryRegistration) -> { QueryBinder.bindQuery( queryRegistration.configuration(), context, true, null ); @@ -115,6 +120,7 @@ public static void bindDefaults(MetadataBuildingContext context) { globalRegistrations.getNamedStoredProcedureQueryRegistrations().forEach( (name, queryRegistration) -> { QueryBinder.bindNamedStoredProcedureQuery( queryRegistration.configuration(), context, true ); } ); + } private static SourceModelBuildingContext sourceContext(MetadataBuildingContext context) { @@ -140,6 +146,19 @@ public static void bindPackage(ClassLoaderService cls, String packageName, Metad bindQueries( packageInfoClassDetails, context ); bindFilterDefs( packageInfoClassDetails, context ); + + bindNamedEntityGraphs( packageInfoClassDetails, context ); + } + + private static void bindNamedEntityGraphs(ClassDetails packageInfoClassDetails, MetadataBuildingContext context) { + packageInfoClassDetails.forEachRepeatedAnnotationUsages( + HibernateAnnotations.NAMED_ENTITY_GRAPH, + context.getMetadataCollector().getSourceModelBuildingContext(), + (annotation) -> { + final NamedEntityGraphDefinition graphDefinition = new NamedEntityGraphDefinition( annotation ); + context.getMetadataCollector().addNamedEntityGraph( graphDefinition ); + } + ); } public static void bindQueries(AnnotationTarget annotationTarget, MetadataBuildingContext context) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index f2eed5fc0b4e..d7577568fcb1 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -4,52 +4,30 @@ */ package org.hibernate.boot.model.internal; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - +import jakarta.persistence.Access; +import jakarta.persistence.AssociationOverride; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Cacheable; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.IdClass; +import jakarta.persistence.Inheritance; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.PrimaryKeyJoinColumns; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.SecondaryTables; +import jakarta.persistence.UniqueConstraint; import org.hibernate.AnnotationException; import org.hibernate.AssertionFailure; import org.hibernate.MappingException; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import org.hibernate.annotations.CacheLayout; -import org.hibernate.annotations.Check; -import org.hibernate.annotations.Checks; -import org.hibernate.annotations.ConcreteProxy; -import org.hibernate.annotations.DiscriminatorFormula; -import org.hibernate.annotations.DynamicInsert; -import org.hibernate.annotations.DynamicUpdate; -import org.hibernate.annotations.Filter; -import org.hibernate.annotations.Filters; -import org.hibernate.annotations.HQLSelect; -import org.hibernate.annotations.Immutable; -import org.hibernate.annotations.Mutability; -import org.hibernate.annotations.NaturalIdCache; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OptimisticLockType; -import org.hibernate.annotations.OptimisticLocking; -import org.hibernate.annotations.QueryCacheLayout; -import org.hibernate.annotations.RowId; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.SQLDeleteAll; -import org.hibernate.annotations.SQLInsert; -import org.hibernate.annotations.SQLRestriction; -import org.hibernate.annotations.SQLSelect; -import org.hibernate.annotations.SQLUpdate; -import org.hibernate.annotations.SecondaryRow; -import org.hibernate.annotations.SecondaryRows; -import org.hibernate.annotations.SoftDelete; -import org.hibernate.annotations.Subselect; -import org.hibernate.annotations.Synchronize; -import org.hibernate.annotations.TypeBinderType; -import org.hibernate.annotations.View; +import org.hibernate.annotations.*; import org.hibernate.binder.TypeBinder; import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.internal.InheritanceState.ElementsToProcess; @@ -106,26 +84,15 @@ import org.hibernate.models.spi.TypeDetails; import org.hibernate.spi.NavigablePath; -import jakarta.persistence.Access; -import jakarta.persistence.AssociationOverride; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.Cacheable; -import jakarta.persistence.ConstraintMode; -import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import jakarta.persistence.ForeignKey; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.IdClass; -import jakarta.persistence.Inheritance; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.NamedEntityGraph; -import jakarta.persistence.PrimaryKeyJoinColumn; -import jakarta.persistence.PrimaryKeyJoinColumns; -import jakarta.persistence.SecondaryTable; -import jakarta.persistence.SecondaryTables; -import jakarta.persistence.UniqueConstraint; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; import static jakarta.persistence.InheritanceType.SINGLE_TABLE; import static org.hibernate.boot.model.internal.AnnotatedClassType.MAPPED_SUPERCLASS; @@ -1590,13 +1557,29 @@ public PersistentClass getPersistentClass() { private void processNamedEntityGraphs() { annotatedClass.forEachAnnotationUsage( NamedEntityGraph.class, getSourceModelContext(), this::processNamedEntityGraph ); + + processParsedNamedGraphs(); + } + + private void processParsedNamedGraphs() { + annotatedClass.forEachRepeatedAnnotationUsages( + HibernateAnnotations.NAMED_ENTITY_GRAPH, + getSourceModelContext(), + this::processParsedNamedEntityGraph + ); } private void processNamedEntityGraph(NamedEntityGraph annotation) { if ( annotation != null ) { getMetadataCollector() - .addNamedEntityGraph( new NamedEntityGraphDefinition( annotation, name, - persistentClass.getEntityName() ) ); + .addNamedEntityGraph( new NamedEntityGraphDefinition( annotation, name, persistentClass.getEntityName() ) ); + } + } + + private void processParsedNamedEntityGraph(org.hibernate.annotations.NamedEntityGraph annotation) { + if ( annotation != null ) { + getMetadataCollector() + .addNamedEntityGraph( new NamedEntityGraphDefinition( annotation, persistentClass ) ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java index 8d0f917cee13..d6904f3a1bf2 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/HibernateAnnotations.java @@ -420,6 +420,15 @@ public interface HibernateAnnotations { Mutability.class, MutabilityAnnotation.class ); + OrmAnnotationDescriptor NAMED_ENTITY_GRAPHS = new OrmAnnotationDescriptor<>( + NamedEntityGraphs.class, + NamedEntityGraphsAnnotation.class + ); + OrmAnnotationDescriptor NAMED_ENTITY_GRAPH = new OrmAnnotationDescriptor<>( + NamedEntityGraph.class, + NamedEntityGraphAnnotation.class, + NAMED_ENTITY_GRAPHS + ); OrmAnnotationDescriptor NAMED_NATIVE_QUERIES = new OrmAnnotationDescriptor<>( NamedNativeQueries.class, NamedNativeQueriesAnnotation.class diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphAnnotation.java b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphAnnotation.java new file mode 100644 index 000000000000..02d5d7aaafeb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphAnnotation.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.models.annotations.internal; + +import org.hibernate.annotations.NamedEntityGraph; +import org.hibernate.models.spi.SourceModelBuildingContext; + +import java.lang.annotation.Annotation; +import java.util.Map; + + +/** + * @author Steve Ebersole + */ +public class NamedEntityGraphAnnotation implements NamedEntityGraph { + private String name; + private String graph; + + /** + * Used in creating dynamic annotation instances (e.g. from XML) + */ + public NamedEntityGraphAnnotation(SourceModelBuildingContext modelContext) { + name = ""; + } + + /** + * Used in creating annotation instances from JDK variant + */ + public NamedEntityGraphAnnotation(NamedEntityGraph annotation, SourceModelBuildingContext modelContext) { + this.name = annotation.name(); + this.graph = annotation.graph(); + } + + /** + * Used in creating annotation instances from Jandex variant + */ + public NamedEntityGraphAnnotation(Map attributeValues, SourceModelBuildingContext modelContext) { + this.name = (String) attributeValues.get( "name" ); + this.graph = (String) attributeValues.get( "graph" ); + } + + @Override + public Class annotationType() { + return NamedEntityGraph.class; + } + + @Override + public String name() { + return name; + } + + public void name(String name) { + this.name = name; + } + + @Override + public String graph() { + return graph; + } + + public void graph(String graph) { + this.graph = graph; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphsAnnotation.java b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphsAnnotation.java new file mode 100644 index 000000000000..3a95716328df --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/models/annotations/internal/NamedEntityGraphsAnnotation.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.boot.models.annotations.internal; + +import org.hibernate.annotations.NamedEntityGraph; +import org.hibernate.annotations.NamedEntityGraphs; +import org.hibernate.boot.models.annotations.spi.RepeatableContainer; +import org.hibernate.models.spi.SourceModelBuildingContext; + +import java.lang.annotation.Annotation; +import java.util.Map; + +import static org.hibernate.boot.models.HibernateAnnotations.NAMED_ENTITY_GRAPHS; +import static org.hibernate.boot.models.internal.OrmAnnotationHelper.extractJdkValue; + +/** + * @author Steve Ebersole + */ +public class NamedEntityGraphsAnnotation implements NamedEntityGraphs, RepeatableContainer { + private NamedEntityGraph[] value; + + /** + * Used in creating dynamic annotation instances (e.g. from XML) + */ + public NamedEntityGraphsAnnotation(SourceModelBuildingContext modelContext) { + } + + /** + * Used in creating annotation instances from JDK variant + */ + public NamedEntityGraphsAnnotation(NamedEntityGraphs annotation, SourceModelBuildingContext modelContext) { + this.value = extractJdkValue( annotation, NAMED_ENTITY_GRAPHS, "value", modelContext ); + } + + /** + * Used in creating annotation instances from Jandex variant + */ + public NamedEntityGraphsAnnotation(Map attributeValues, SourceModelBuildingContext modelContext) { + this.value = (NamedEntityGraph[]) attributeValues.get( "value" ); + } + + @Override + public Class annotationType() { + return NamedEntityGraphs.class; + } + + @Override + public NamedEntityGraph[] value() { + return value; + } + + public void value(NamedEntityGraph[] value) { + this.value = value; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java index 397c67102cad..1a199f3bd5f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionFactoryImplementor.java @@ -5,6 +5,7 @@ package org.hibernate.engine.spi; import java.util.Collection; +import java.util.Map; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.Incubating; @@ -21,6 +22,7 @@ import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EntityCopyObserverFactory; import org.hibernate.event.spi.EventEngine; +import org.hibernate.graph.RootGraph; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.event.service.spi.EventListenerGroups; import org.hibernate.metamodel.model.domain.JpaMetamodel; @@ -291,6 +293,14 @@ default JpaMetamodel getJpaMetamodel() { @Override RootGraphImplementor findEntityGraphByName(String name); + @Override + default RootGraphImplementor createEntityGraph(Class entityType) { + return (RootGraphImplementor) SessionFactory.super.createEntityGraph( entityType ); + } + + @Override + RootGraph> createGraphForDynamicEntity(String entityName); + /** * The best guess entity name for an entity not in an association */ diff --git a/hibernate-core/src/main/java/org/hibernate/graph/GraphParser.java b/hibernate-core/src/main/java/org/hibernate/graph/GraphParser.java index 77055d904730..0a1e7892c654 100644 --- a/hibernate-core/src/main/java/org/hibernate/graph/GraphParser.java +++ b/hibernate-core/src/main/java/org/hibernate/graph/GraphParser.java @@ -9,17 +9,21 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Subgraph; +import org.hibernate.SessionFactory; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.graph.internal.parse.GraphParsing; import org.hibernate.graph.spi.GraphImplementor; -import org.hibernate.graph.spi.RootGraphImplementor; /** - * Parser for string representations of JPA {@link jakarta.persistence.EntityGraph} - * ({@link RootGraph}) and {@link jakarta.persistence.Subgraph} ({@link SubGraph}), - * using a simple syntax defined by the {@code graph.g} ANTLR grammar. For example: - *

employees(username, password, accessLevel, department(employees(username)))
- *

+ * Parser for string representations of {@linkplain RootGraph entity graphs}. + * The syntax is

+ *     graph:: (rootEntityName COLON)? attributeList
+ *     attributeList:: attributeNode (COMMA attributeNode)*
+ *     attributeNode:: attributePath subGraph?
+ *     subGraph:: LPAREN (subTypeEntityName COLON)? attributeList RPAREN
+ * 
+ *

* The {@link #parse} methods all create a root {@link jakarta.persistence.EntityGraph} * based on the passed entity class and parse the graph string into that root graph. *

@@ -37,12 +41,101 @@ public final class GraphParser { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Parse (creation) + /** + * Creates a root graph based on the passed {@code rootEntityClass} and parses + * {@code graphText} into the generated root graph. + * + * @param rootEntityClass The entity class to use as the graph root + * @param graphText The textual representation of the graph + * @param sessionFactory The SessionFactory + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @apiNote The string representation is expected to just be an attribute list, without + * the entity-type prefix. E.g. {@code "title, isbn, author(name, books)"} + * + * @see org.hibernate.SessionFactory#parseEntityGraph(Class, CharSequence) + * + * @since 7.0 + */ + public static RootGraph parse( + final Class rootEntityClass, + final CharSequence graphText, + final SessionFactory sessionFactory) { + if ( graphText == null ) { + return null; + } + return GraphParsing.parse( + rootEntityClass, + graphText.toString(), + sessionFactory.unwrap( SessionFactoryImplementor.class ) + ); + } + + /** + * Creates a root graph based on the passed {@code rootEntityName} and parses + * {@code graphText} into the generated root graph. + * + * @param rootEntityName The name of the entity to use as the graph root + * @param graphText The textual representation of the graph + * @param sessionFactory The SessionFactory + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @apiNote The string representation is expected to just be an attribute list, without + * the entity-type prefix. E.g. {@code "title, isbn, author(name, books)"} + * + * @see org.hibernate.SessionFactory#parseEntityGraph(Class, CharSequence) + * + * @since 7.0 + */ + public static RootGraph parse( + final String rootEntityName, + final CharSequence graphText, + final SessionFactory sessionFactory) { + if ( graphText == null ) { + return null; + } + return GraphParsing.parse( + rootEntityName, + graphText.toString(), + sessionFactory.unwrap( SessionFactoryImplementor.class ) + ); + } + + /** + * Creates a root graph based on the passed {@code graphText}. The format of this + * text is the root name with a colon, followed by an attribute list. + * E.g. {@code "Book: title, isbn, author(name, books)"}. + * + * @param graphText The textual representation of the graph + * @param sessionFactory The SessionFactory + * + * @throws InvalidGraphException if the textual representation is invalid. + * + * @see org.hibernate.SessionFactory#parseEntityGraph(Class, CharSequence) + * + * @since 7.0 + */ + public static RootGraph parse( + final CharSequence graphText, + final SessionFactory sessionFactory) { + if ( graphText == null ) { + return null; + } + return GraphParsing.parse( + graphText.toString(), + sessionFactory.unwrap( SessionFactoryImplementor.class ) + ); + } + /** * Creates a root graph based on the passed `rootType` and parses `graphText` into * the generated root graph * * @apiNote The passed EntityManager is expected to be a Hibernate implementation. - * Attempting to pass another provider's EntityManager implementation will fail + * Attempting to pass another provider's EntityManager implementation will fail. + * @implNote Simply delegates to {@linkplain #parse(Class, CharSequence, SessionFactory)} * * @param rootType The root entity type * @param graphText The textual representation of the graph @@ -54,20 +147,14 @@ public static RootGraph parse( final Class rootType, final CharSequence graphText, final EntityManager entityManager) { - return parse( rootType, graphText, (SessionImplementor) entityManager ); - } - - private static RootGraphImplementor parse( - final Class rootType, - final CharSequence graphText, - final SessionImplementor session) { if ( graphText == null ) { return null; } - - final RootGraphImplementor graph = session.createEntityGraph( rootType ); - parseInto( (GraphImplementor) graph, graphText, session.getSessionFactory() ); - return graph; + return GraphParsing.parse( + rootType, + graphText.toString(), + entityManager.getEntityManagerFactory().unwrap( SessionFactoryImplementor.class ) + ); } @@ -216,7 +303,7 @@ private static void parseInto( final CharSequence graphText, SessionFactoryImplementor sessionFactory) { if ( graphText != null ) { - org.hibernate.graph.internal.parse.GraphParser.parseInto( graph, graphText, sessionFactory ); + GraphParsing.parseInto( graph, graphText, sessionFactory ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolver.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolver.java new file mode 100644 index 000000000000..82173956a20a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolver.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.graph.internal.parse; + +import org.hibernate.metamodel.model.domain.EntityDomainType; + +/** + * @author Steve Ebersole + */ +@FunctionalInterface +public interface EntityNameResolver { + EntityDomainType resolveEntityName(String entityName); +} diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolverSessionFactory.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolverSessionFactory.java new file mode 100644 index 000000000000..49e8249fd45e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/EntityNameResolverSessionFactory.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.graph.internal.parse; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.model.domain.EntityDomainType; + +/** + * @author Steve Ebersole + */ +public class EntityNameResolverSessionFactory implements EntityNameResolver { + private final SessionFactoryImplementor sessionFactory; + + public EntityNameResolverSessionFactory(SessionFactoryImplementor sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public EntityDomainType resolveEntityName(String entityName) { + //noinspection unchecked + return (EntityDomainType) sessionFactory.getJpaMetamodel().findEntityType( entityName ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParser.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParser.java index 6852ca2975f8..603eb1172e89 100644 --- a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParser.java +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParser.java @@ -5,7 +5,6 @@ package org.hibernate.graph.internal.parse; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.grammars.graph.GraphLanguageLexer; import org.hibernate.grammars.graph.GraphLanguageParser; import org.hibernate.grammars.graph.GraphLanguageParserBaseVisitor; import org.hibernate.graph.GraphNode; @@ -17,69 +16,45 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; - import static org.hibernate.graph.internal.GraphParserLogging.PARSING_LOGGER; /** + * Unified access to the Antlr parser for Hibernate's "graph language" + * * @author Steve Ebersole */ public class GraphParser extends GraphLanguageParserBaseVisitor> { + private final EntityNameResolver entityNameResolver; - /** - * Parse the passed graph textual representation into the passed Graph. - */ - public static void parseInto( - GraphImplementor targetGraph, - String graphString, - SessionFactoryImplementor sessionFactory) { - // Build the lexer - final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphString ) ); - - // Build the parser... - final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); - - // Build an instance of this class as a visitor - final GraphParser visitor = new GraphParser( sessionFactory ); - visitor.graphStack.push( targetGraph ); - try { - visitor.visitGraph( parser.graph() ); - } - finally { - visitor.graphStack.pop(); + private final Stack> graphStack = new StandardStack<>(); + private final Stack> attributeNodeStack = new StandardStack<>(); + private final Stack graphSourceStack = new StandardStack<>(); - assert visitor.graphStack.isEmpty(); - } + public GraphParser(EntityNameResolver entityNameResolver) { + this.entityNameResolver = entityNameResolver; } /** - * Parse the passed graph textual representation into the passed Graph. + * @apiNote It is important that this form only be used after the session-factory is fully + * initialized, especially the {@linkplain SessionFactoryImplementor#getJpaMetamodel()} JPA metamodel}. + * + * @see GraphParser#GraphParser(EntityNameResolver) */ - public static void parseInto( - GraphImplementor targetGraph, - CharSequence graphString, - SessionFactoryImplementor sessionFactory) { - parseInto( targetGraph, graphString.toString(), sessionFactory ); + public GraphParser(SessionFactoryImplementor sessionFactory) { + this( new EntityNameResolverSessionFactory( sessionFactory ) ); } - private final SessionFactoryImplementor sessionFactory; - - private final Stack> graphStack = new StandardStack<>(); - private final Stack> attributeNodeStack = new StandardStack<>(); - private final Stack graphSourceStack = new StandardStack<>(); - - public GraphParser(SessionFactoryImplementor sessionFactory) { - this.sessionFactory = sessionFactory; + public Stack> getGraphStack() { + return graphStack; } @Override - public AttributeNodeImplementor visitAttributeNode(GraphLanguageParser.AttributeNodeContext ctx) { - final String attributeName = ctx.attributePath().ATTR_NAME().getText(); + public AttributeNodeImplementor visitAttributeNode(GraphLanguageParser.AttributeNodeContext attributeNodeContext) { + final String attributeName = attributeNodeContext.attributePath().ATTR_NAME().getText(); final SubGraphGenerator subGraphCreator; - if ( ctx.attributePath().attributeQualifier() == null ) { + if ( attributeNodeContext.attributePath().attributeQualifier() == null ) { if ( PARSING_LOGGER.isDebugEnabled() ) { PARSING_LOGGER.debugf( "%s Start attribute : %s", @@ -91,7 +66,7 @@ public AttributeNodeImplementor visitAttributeNode(GraphLanguageParser.At subGraphCreator = PathQualifierType.VALUE.getSubGraphCreator(); } else { - final String qualifierName = ctx.attributePath().attributeQualifier().ATTR_NAME().getText(); + final String qualifierName = attributeNodeContext.attributePath().attributeQualifier().ATTR_NAME().getText(); if ( PARSING_LOGGER.isDebugEnabled() ) { PARSING_LOGGER.debugf( @@ -108,12 +83,12 @@ public AttributeNodeImplementor visitAttributeNode(GraphLanguageParser.At final AttributeNodeImplementor attributeNode = resolveAttributeNode( attributeName ); - if ( ctx.subGraph() != null ) { + if ( attributeNodeContext.subGraph() != null ) { attributeNodeStack.push( attributeNode ); graphSourceStack.push( subGraphCreator ); try { - visitSubGraph( ctx.subGraph() ); + visitSubGraph( attributeNodeContext.subGraph() ); } finally { @@ -156,8 +131,8 @@ private PathQualifierType resolvePathQualifier(String qualifier) { } @Override - public SubGraphImplementor visitSubGraph(GraphLanguageParser.SubGraphContext ctx) { - final String subTypeName = ctx.subType() == null ? null : ctx.subType().getText(); + public SubGraphImplementor visitSubGraph(GraphLanguageParser.SubGraphContext subGraphContext) { + final String subTypeName = subGraphContext.typeIndicator() == null ? null : subGraphContext.typeIndicator().TYPE_NAME().getText(); if ( PARSING_LOGGER.isDebugEnabled() ) { PARSING_LOGGER.debugf( @@ -173,13 +148,13 @@ public SubGraphImplementor visitSubGraph(GraphLanguageParser.SubGraphContext final SubGraphImplementor subGraph = subGraphCreator.createSubGraph( attributeNode, subTypeName, - sessionFactory + entityNameResolver ); graphStack.push( subGraph ); try { - ctx.attributeList().accept( this ); + subGraphContext.attributeList().accept( this ); } finally { graphStack.pop(); diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParsing.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParsing.java new file mode 100644 index 000000000000..dded046e71e6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/GraphParsing.java @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.graph.internal.parse; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.grammars.graph.GraphLanguageLexer; +import org.hibernate.grammars.graph.GraphLanguageParser; +import org.hibernate.graph.InvalidGraphException; +import org.hibernate.graph.internal.RootGraphImpl; +import org.hibernate.graph.spi.GraphImplementor; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.metamodel.model.domain.EntityDomainType; + +/** + * Helper for dealing with graph text parsing. + * + * @author Steve Ebersole + */ +public class GraphParsing { + public static RootGraphImplementor parse( + Class entityClass, + String graphText, + SessionFactoryImplementor sessionFactory) { + if ( graphText == null ) { + return null; + } + + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphText ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + if ( graphContext.typeIndicator() != null ) { + // todo : an alternative here would be to simply validate that the entity type + // from the text matches the passed one... + throw new InvalidGraphException( "Expecting graph text to not include an entity name : " + graphText ); + } + + final EntityDomainType entityType = sessionFactory.getJpaMetamodel().entity( entityClass ); + return parse( entityType, graphContext.attributeList(), sessionFactory ); + } + + public static RootGraphImplementor parse( + EntityDomainType entityDomainType, + String graphText, + SessionFactoryImplementor sessionFactory) { + if ( graphText == null ) { + return null; + } + + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphText ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + if ( graphContext.typeIndicator() != null ) { + // todo : an alternative here would be to simply validate that the entity type + // from the text matches the passed one... + throw new InvalidGraphException( "Expecting graph text to not include an entity name : " + graphText ); + } + + return parse( entityDomainType, graphContext.attributeList(), sessionFactory ); + } + + public static RootGraphImplementor parse( + String entityName, + String graphText, + SessionFactoryImplementor sessionFactory) { + if ( graphText == null ) { + return null; + } + + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphText ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + if ( graphContext.typeIndicator() != null ) { + // todo : an alternative here would be to simply validate that the entity type + // from the text matches the passed one... + throw new InvalidGraphException( "Expecting graph text to not include an entity name : " + graphText ); + } + + //noinspection unchecked + final EntityDomainType entityType = (EntityDomainType) sessionFactory.getJpaMetamodel().entity( entityName ); + return parse( entityType, graphContext.attributeList(), sessionFactory ); + } + + public static RootGraphImplementor parse( + String graphText, + SessionFactoryImplementor sessionFactory) { + if ( graphText == null ) { + return null; + } + + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphText ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + if ( graphContext.typeIndicator() == null ) { + throw new InvalidGraphException( "Expecting graph text to include an entity name : " + graphText ); + } + + final String entityName = graphContext.typeIndicator().TYPE_NAME().getText(); + + //noinspection unchecked + final EntityDomainType entityType = (EntityDomainType) sessionFactory.getJpaMetamodel().entity( entityName ); + return parse( entityType, graphContext.attributeList(), sessionFactory ); + } + + public static RootGraphImplementor parse( + EntityDomainType rootType, + GraphLanguageParser.AttributeListContext attributeListContext, + SessionFactoryImplementor sessionFactory) { + return parse( rootType, attributeListContext, new EntityNameResolverSessionFactory( sessionFactory ) ); + } + + public static RootGraphImplementor parse( + EntityDomainType rootType, + GraphLanguageParser.AttributeListContext attributeListContext, + EntityNameResolver entityNameResolver) { + final RootGraphImpl targetGraph = new RootGraphImpl<>( null, rootType ); + + final GraphParser visitor = new GraphParser( entityNameResolver ); + visitor.getGraphStack().push( targetGraph ); + try { + visitor.visitAttributeList( attributeListContext ); + } + finally { + visitor.getGraphStack().pop(); + + assert visitor.getGraphStack().isEmpty(); + } + + return targetGraph; + } + + /** + * Parse the passed graph textual representation into the passed Graph. + * Essentially overlays the text representation on top of the graph. + */ + public static void parseInto( + GraphImplementor targetGraph, + CharSequence graphString, + SessionFactoryImplementor sessionFactory) { + final GraphLanguageLexer lexer = new GraphLanguageLexer( CharStreams.fromString( graphString.toString() ) ); + final GraphLanguageParser parser = new GraphLanguageParser( new CommonTokenStream( lexer ) ); + final GraphLanguageParser.GraphContext graphContext = parser.graph(); + + if ( graphContext.typeIndicator() != null ) { + // todo : throw an exception? Log warning? Ignore? + // for now, ignore + } + + // Build an instance of this class as a visitor + final GraphParser visitor = new GraphParser( sessionFactory ); + + visitor.getGraphStack().push( targetGraph ); + try { + visitor.visitAttributeList( graphContext.attributeList() ); + } + finally { + visitor.getGraphStack().pop(); + + assert visitor.getGraphStack().isEmpty(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/PathQualifierType.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/PathQualifierType.java index b1b6a1dfbd96..bc44fc8af311 100644 --- a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/PathQualifierType.java +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/PathQualifierType.java @@ -5,8 +5,7 @@ package org.hibernate.graph.internal.parse; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.metamodel.model.domain.JpaMetamodel; +import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; /** @@ -14,26 +13,22 @@ */ public enum PathQualifierType { - KEY( (attributeNode, subtypeName, sessionFactory) -> subtypeName == null + KEY( (attributeNode, subtypeName, entityNameResolver) -> subtypeName == null ? attributeNode.addKeySubgraph() - : attributeNode.addKeySubgraph().addTreatedSubgraph( managedType( subtypeName, sessionFactory ) ) + : attributeNode.addKeySubgraph().addTreatedSubgraph( managedType( subtypeName, entityNameResolver ) ) ), - VALUE( (attributeNode, subtypeName, sessionFactory) -> subtypeName == null + VALUE( (attributeNode, subtypeName, entityNameResolver) -> subtypeName == null ? attributeNode.addValueSubgraph() - : attributeNode.addValueSubgraph().addTreatedSubgraph( managedType( subtypeName, sessionFactory ) ) + : attributeNode.addValueSubgraph().addTreatedSubgraph( managedType( subtypeName, entityNameResolver ) ) ); - private static ManagedDomainType managedType(String subtypeName, SessionFactoryImplementor sessionFactory) { - final JpaMetamodel metamodel = sessionFactory.getJpaMetamodel(); - ManagedDomainType managedType = metamodel.findManagedType( subtypeName ); - if ( managedType == null ) { - managedType = metamodel.getHqlEntityReference( subtypeName ); - } - if ( managedType == null ) { + private static ManagedDomainType managedType(String subtypeName, EntityNameResolver entityNameResolver) { + final EntityDomainType entityDomainType = entityNameResolver.resolveEntityName( subtypeName ); + if ( entityDomainType == null ) { throw new IllegalArgumentException( "Unknown managed type: " + subtypeName ); } - return managedType; + return entityDomainType; } private final SubGraphGenerator subGraphCreator; diff --git a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/SubGraphGenerator.java b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/SubGraphGenerator.java index f19aa465e64d..3e75d5aaf927 100644 --- a/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/SubGraphGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/graph/internal/parse/SubGraphGenerator.java @@ -4,7 +4,6 @@ */ package org.hibernate.graph.internal.parse; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.graph.spi.AttributeNodeImplementor; import org.hibernate.graph.spi.SubGraphImplementor; @@ -16,5 +15,5 @@ public interface SubGraphGenerator { SubGraphImplementor createSubGraph( AttributeNodeImplementor attributeNode, String subTypeName, - SessionFactoryImplementor sessionFactory); + EntityNameResolver entityNameResolver); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java index a7511dd7837c..ccafbc920992 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java @@ -21,18 +21,13 @@ import org.checkerframework.checker.nullness.qual.Nullable; -import org.hibernate.AnnotationException; import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.query.NamedQueryDefinition; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.graph.internal.RootGraphImpl; -import org.hibernate.graph.spi.AttributeNodeImplementor; -import org.hibernate.graph.spi.GraphImplementor; import org.hibernate.graph.spi.RootGraphImplementor; -import org.hibernate.graph.spi.SubGraphImplementor; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.jpa.spi.JpaCompliance; @@ -60,17 +55,12 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.EntityGraph; -import jakarta.persistence.NamedAttributeNode; -import jakarta.persistence.NamedEntityGraph; -import jakarta.persistence.NamedSubgraph; -import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.EmbeddableType; import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.ManagedType; import jakarta.persistence.metamodel.Type; import static java.util.Collections.emptySet; -import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.metamodel.internal.InjectionHelper.injectEntityGraph; import static org.hibernate.metamodel.internal.InjectionHelper.injectTypedQueryReference; @@ -157,18 +147,38 @@ public EntityDomainType findEntityType(@Nullable String entityName) { if ( entityName == null ) { return null; } + final ManagedDomainType managedType = managedTypeByName.get( entityName ); - return managedType instanceof EntityDomainType entityDomainType ? entityDomainType : null; + if ( managedType instanceof EntityDomainType entityDomainType ){ + return entityDomainType; + } + + // NOTE: `managedTypeByName` is keyed by Hibernate entity name. + // If there is a direct match based on key, we want that one - see above. + // However, the JPA contract for `#entity` is to match `@Entity(name)`; if there + // was no direct match, we need to iterate over all of the and look based on + // JPA entity-name. + + for ( Map.Entry> entry : managedTypeByName.entrySet() ) { + if ( entry.getValue() instanceof EntityDomainType possibility ) { + if ( entityName.equals( possibility.getName() ) ) { + return possibility; + } + } + } + + return null; } @Override public EntityDomainType entity(String entityName) { final EntityDomainType entityType = findEntityType( entityName ); if ( entityType == null ) { - // per JPA + // per JPA, this is an exception throw new IllegalArgumentException( "Not an entity: " + entityName ); } return entityType; + } @Override @@ -484,149 +494,34 @@ private ImportInfo resolveImport(final String name) { private void applyNamedEntityGraphs(Collection namedEntityGraphs) { for ( NamedEntityGraphDefinition definition : namedEntityGraphs ) { log.debugf( - "Applying named entity graph [name=%s, entity-name=%s, jpa-entity-name=%s]", + "Applying named entity graph [name=%s, source=%s]", definition.getRegisteredName(), - definition.getEntityName(), - definition.getJpaEntityName() + definition.getSource() ); - final EntityDomainType entityType = findEntityType( definition.getEntityName() ); - if ( entityType == null ) { - throw new IllegalArgumentException( - "Attempted to register named entity graph [" + definition.getRegisteredName() - + "] for unknown entity [" + definition.getEntityName() + "]" - - ); - } - - final NamedEntityGraph namedEntityGraph = definition.getAnnotation(); - final RootGraphImplementor entityGraph = - createEntityGraph( - namedEntityGraph, - definition.getRegisteredName(), - entityType, - namedEntityGraph.includeAllAttributes() - ); - - entityGraphMap.put( definition.getRegisteredName(), entityGraph ); - } - } - - private RootGraphImplementor createEntityGraph( - NamedEntityGraph namedEntityGraph, - String registeredName, - EntityDomainType entityType, - boolean includeAllAttributes) { - final RootGraphImplementor entityGraph = - createRootGraph( registeredName, entityType, includeAllAttributes ); - - if ( namedEntityGraph.subclassSubgraphs() != null ) { - for ( NamedSubgraph subclassSubgraph : namedEntityGraph.subclassSubgraphs() ) { - final Class subgraphType = subclassSubgraph.type(); - final Class graphJavaType = entityGraph.getGraphedType().getJavaType(); - if ( !graphJavaType.isAssignableFrom( subgraphType ) ) { - throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() - + "' is not a subtype of the graph type '" + graphJavaType.getName() + "'" ); - } - @SuppressWarnings("unchecked") // Safe, because we just checked - final Class subtype = (Class) subgraphType; - final GraphImplementor subgraph = entityGraph.addTreatedSubgraph( subtype ); - applyNamedAttributeNodes( subclassSubgraph.attributeNodes(), namedEntityGraph, subgraph ); - } - } - - if ( namedEntityGraph.attributeNodes() != null ) { - applyNamedAttributeNodes( namedEntityGraph.attributeNodes(), namedEntityGraph, entityGraph ); - } - return entityGraph; - } - - private static RootGraphImplementor createRootGraph( - String name, EntityDomainType entityType, boolean includeAllAttributes) { - final RootGraphImpl entityGraph = new RootGraphImpl<>( name, entityType ); - if ( includeAllAttributes ) { - for ( Attribute attribute : entityType.getAttributes() ) { - entityGraph.addAttributeNodes( attribute ); - } - } - return entityGraph; - } - - private void applyNamedAttributeNodes( - NamedAttributeNode[] namedAttributeNodes, - NamedEntityGraph namedEntityGraph, - GraphImplementor graphNode) { - for ( NamedAttributeNode namedAttributeNode : namedAttributeNodes ) { - final String value = namedAttributeNode.value(); - final AttributeNodeImplementor attributeNode = - (AttributeNodeImplementor) graphNode.addAttributeNode( value ); - - if ( isNotEmpty( namedAttributeNode.subgraph() ) ) { - applyNamedSubgraphs( - namedEntityGraph, - namedAttributeNode.subgraph(), - attributeNode, - false - ); - } - if ( isNotEmpty( namedAttributeNode.keySubgraph() ) ) { - applyNamedSubgraphs( - namedEntityGraph, - namedAttributeNode.keySubgraph(), - attributeNode, - true - ); - } - } - } - - private void applyNamedSubgraphs( - NamedEntityGraph namedEntityGraph, - String subgraphName, - AttributeNodeImplementor attributeNode, - boolean isKeySubGraph) { - for ( NamedSubgraph namedSubgraph : namedEntityGraph.subgraphs() ) { - if ( subgraphName.equals( namedSubgraph.name() ) ) { - final Class subgraphType = namedSubgraph.type(); - final SubGraphImplementor subgraph; - if ( subgraphType.equals( void.class ) ) { // unspecified - subgraph = attributeNode.addValueSubgraph(); - } - else { - subgraph = isKeySubGraph - ? makeAttributeNodeKeySubgraph( attributeNode, subgraphType ) - : makeAttributeNodeValueSubgraph( attributeNode, subgraphType ); - } - applyNamedAttributeNodes( namedSubgraph.attributeNodes(), namedEntityGraph, subgraph ); - } - } - } - - private static SubGraphImplementor makeAttributeNodeValueSubgraph( - AttributeNodeImplementor attributeNode, Class subgraphType) { - final Class attributeValueType = - attributeNode.getAttributeDescriptor().getValueGraphType().getBindableJavaType(); - if ( !attributeValueType.isAssignableFrom( subgraphType ) ) { - throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() - + "' is not a subtype of the value type '" + attributeValueType.getName() + "'" ); + final RootGraphImplementor graph = definition.getGraphCreator().createEntityGraph( + (entityClass) -> { + final ManagedDomainType managedDomainType = managedTypeByClass.get( entityClass ); + if ( managedDomainType instanceof EntityDomainType match ) { + return match; + } + throw new IllegalArgumentException( "Cannot resolve entity class : " + entityClass.getName() ); + }, + (jpaEntityName) -> { + for ( Map.Entry> entry : managedTypeByName.entrySet() ) { + if ( entry.getValue() instanceof EntityDomainType possibility ) { + if ( jpaEntityName.equals( possibility.getName() ) ) { + return possibility; + } + } + } + throw new IllegalArgumentException( "Cannot resolve entity name : " + jpaEntityName ); + } + ); + entityGraphMap.put( definition.getRegisteredName(), graph ); } - @SuppressWarnings("unchecked") // Safe, because we just checked - final Class castType = (Class) subgraphType; - return attributeNode.addValueSubgraph().addTreatedSubgraph( castType ); } - private static SubGraphImplementor makeAttributeNodeKeySubgraph( - AttributeNodeImplementor attributeNode, Class subgraphType) { - final Class attributeKeyType = - attributeNode.getAttributeDescriptor().getKeyGraphType().getBindableJavaType(); - if ( !attributeKeyType.isAssignableFrom( subgraphType ) ) { - throw new AnnotationException( "Named subgraph type '" + subgraphType.getName() - + "' is not a subtype of the key type '" + attributeKeyType.getName() + "'" ); - } - @SuppressWarnings("unchecked") // Safe, because we just checked - final Class castType = (Class) subgraphType; - return attributeNode.addKeySubgraph().addTreatedSubgraph( castType ); - } private Class resolveRequestedClass(String entityName) { try { @@ -795,7 +690,7 @@ private void populateStaticMetamodel(MetadataImplementor bootMetamodel, Metadata -> injectTypedQueryReference( definition, namedQueryMetamodelClass( definition, context ) ) ); bootMetamodel.visitNamedNativeQueryDefinitions( definition -> injectTypedQueryReference( definition, namedQueryMetamodelClass( definition, context ) ) ); - bootMetamodel.getNamedEntityGraphs().values().forEach( definition + bootMetamodel.getNamedEntityGraphs().values().stream().filter( (definition) -> definition.getEntityName() != null ).forEach( definition -> injectEntityGraph( definition, graphMetamodelClass( definition, context ), this ) ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/ClassLevelTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/ClassLevelTests.java new file mode 100644 index 000000000000..f0d6ce45a95d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/ClassLevelTests.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed; + +import org.hibernate.DuplicateMappingException; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.graph.InvalidGraphException; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.Book; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.DomesticPublishingHouse; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.Duplicator; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.ForeignPublishingHouse; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.InvalidParsedGraphEntity; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.Isbn; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.Person; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.Publisher; +import org.hibernate.orm.test.entitygraph.named.parsed.entity.PublishingHouse; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.hibernate.orm.test.entitygraph.parser.AssertionHelper.assertBasicAttributes; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for Hibernate's {@link org.hibernate.annotations.NamedEntityGraph @NamedEntityGraph} + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class ClassLevelTests { + + @Test + @DomainModel(annotatedClasses = { + Book.class, + Person.class, + Publisher.class, + PublishingHouse.class, + DomesticPublishingHouse.class, + ForeignPublishingHouse.class, + Isbn.class + + }) + @SessionFactory(exportSchema = false) + void testRegistrations(SessionFactoryScope factoryScope) { + final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory(); + + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn" ), "title", "isbn" ); + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn-author" ), "title", "isbn", "author" ); + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn-editor" ), "title", "isbn", "editor" ); + + assertBasicAttributes( sessionFactory.findEntityGraphByName( "publishing-house-bio" ), "name", "ceo", "boardMembers" ); + } + + @Test + @DomainModel(annotatedClasses = InvalidParsedGraphEntity.class) + void testInvalidParsedGraph(DomainModelScope modelScope) { + final MetadataImplementor domainModel = modelScope.getDomainModel(); + try { + try (org.hibernate.SessionFactory sessionFactory = domainModel.buildSessionFactory()) { + fail( "Expecting an exception" ); + } + catch (InvalidGraphException expected) { + } + } + catch (InvalidGraphException expected) { + } + } + + @Test + @ServiceRegistry + void testDuplicateNames(ServiceRegistryScope registryScope) { + final MetadataSources metadataSources = new MetadataSources( registryScope.getRegistry() ) + .addAnnotatedClasses( Duplicator.class ); + try { + metadataSources.buildMetadata(); + fail( "Expecting a failure" ); + } + catch (DuplicateMappingException expected) { + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/PackageLevelTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/PackageLevelTests.java new file mode 100644 index 000000000000..ac17e369fb64 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/PackageLevelTests.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed; + +import org.hibernate.DuplicateMappingException; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.graph.InvalidGraphException; +import org.hibernate.orm.test.entitygraph.named.parsed.pkg.Book; +import org.hibernate.orm.test.entitygraph.named.parsed.pkg.Duplicator; +import org.hibernate.orm.test.entitygraph.named.parsed.pkg.Isbn; +import org.hibernate.orm.test.entitygraph.named.parsed.pkg.Person; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.hibernate.orm.test.entitygraph.parser.AssertionHelper.assertBasicAttributes; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class PackageLevelTests { + @Test + @DomainModel( + annotatedClasses = { Book.class, Isbn.class, Person.class }, + annotatedPackageNames = "org.hibernate.orm.test.entitygraph.named.parsed.pkg" + ) + @SessionFactory(exportSchema = false) + void testDiscovery(SessionFactoryScope factoryScope) { + final SessionFactoryImplementor sessionFactory = factoryScope.getSessionFactory(); + + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn" ), "title", "isbn" ); + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn-author" ), "title", "isbn", "author" ); + assertBasicAttributes( sessionFactory.findEntityGraphByName( "book-title-isbn-editor" ), "title", "isbn", "editor" ); + } + + @Test + @ServiceRegistry + void testDuplication(ServiceRegistryScope registryScope) { + final StandardServiceRegistry serviceRegistry = registryScope.getRegistry(); + try { + new MetadataSources( serviceRegistry ) + .addAnnotatedClass( Duplicator.class ) + .addPackage( "org.hibernate.orm.test.entitygraph.named.parsed.pkg" ) + .buildMetadata(); + fail( "Expected an exception" ); + } + catch (DuplicateMappingException expected) { + } + } + + @Test + @DomainModel( + annotatedClasses = Person.class, + annotatedPackageNames = "org.hibernate.orm.test.entitygraph.named.parsed.pkg2" + ) + void testInvalid(DomainModelScope modelScope) { + try (org.hibernate.SessionFactory sessionFactory = modelScope.getDomainModel().buildSessionFactory()) { + fail( "Expected an exception" ); + } + catch (InvalidGraphException expected) { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Book.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Book.java new file mode 100644 index 000000000000..d721dfe4ca2e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Book.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.annotations.NamedEntityGraph; + +/** + * @author Steve Ebersole + */ +@Entity(name = "Book") +@Table(name = "Book") +@NamedEntityGraph(name = "book-title-isbn", graph = "title, isbn") +@NamedEntityGraph(name = "book-title-isbn-author", graph = "title, isbn, author") +@NamedEntityGraph(name = "book-title-isbn-editor", graph = "title, isbn, editor") +public class Book { + @Id + private Integer id; + private String title; + @Embedded + private Isbn isbn; + + @ManyToOne + @JoinColumn(name = "author_fk") + Person author; + + @ManyToOne + @JoinColumn(name = "editor_fk") + Person editor; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/DomesticPublishingHouse.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/DomesticPublishingHouse.java new file mode 100644 index 000000000000..4ba60af5fecf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/DomesticPublishingHouse.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "DomesticPublishHouse") +@Table(name = "DomesticPublishHouse") +public class DomesticPublishingHouse extends PublishingHouse { + private String taxIdentifier; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Duplicator.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Duplicator.java new file mode 100644 index 000000000000..a55951d7838e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Duplicator.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.NamedAttributeNode; +import org.hibernate.annotations.NamedEntityGraph; + +/** + * @author Steve Ebersole + */ +@NamedEntityGraph(name = "test-id-name", graph = "(id, name)") +@jakarta.persistence.NamedEntityGraph( + name = "test-id-name", + attributeNodes = { + @NamedAttributeNode("id"), + @NamedAttributeNode("name") + } +) +@Entity +public class Duplicator { + @Id + private Integer id; + private String name; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/ForeignPublishingHouse.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/ForeignPublishingHouse.java new file mode 100644 index 000000000000..1422114a9866 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/ForeignPublishingHouse.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "ForeignPublishHouse") +@Table(name = "ForeignPublishHouse") +public class ForeignPublishingHouse extends PublishingHouse { + private String euIdentifier; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/InvalidParsedGraphEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/InvalidParsedGraphEntity.java new file mode 100644 index 000000000000..d345db4cd9a9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/InvalidParsedGraphEntity.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.NamedEntityGraph; + +/** + * @author Steve Ebersole + */ +@Entity(name = "InvalidParsedGraphEntity") +@Table(name = "InvalidParsedGraphEntity") +@NamedEntityGraph(name = "invalid", graph = "InvalidParsedGraphEntity: name") +public class InvalidParsedGraphEntity { + @Id + private Integer id; + private String name; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Isbn.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Isbn.java new file mode 100644 index 000000000000..c42c70f22ac4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Isbn.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Embeddable; + +/** + * @author Steve Ebersole + */ +@Embeddable +public class Isbn { + private String prefix; + private String element; + private String registrationGroup; + private String registrant; + private String publication; + private String checkDigit; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Person.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Person.java new file mode 100644 index 000000000000..d545b2bb263d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Person.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "Person") +@Table(name = "Person") +public class Person { + @Id + private Integer id; + private String name; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Publisher.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Publisher.java new file mode 100644 index 000000000000..4ed32ff172ca --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/Publisher.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity(name = "Publisher") +@Table(name = "Publisher") +public class Publisher { + @Id + private Integer id; + private String registrationId; + + @ManyToOne + @JoinColumn(name = "person_data_fk") + private Person personDetails; + + @ManyToOne + @JoinColumn(name = "pub_house_fk") + private PublishingHouse publishingHouse; + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/PublishingHouse.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/PublishingHouse.java new file mode 100644 index 000000000000..3995f6eb4360 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/entity/PublishingHouse.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.annotations.Bag; +import org.hibernate.annotations.NamedEntityGraph; + +import java.util.Collection; + +/** + * @author Steve Ebersole + */ +@Entity(name = "PublishingHouse") +@Table(name = "PublishingHouse") +@Inheritance(strategy = InheritanceType.JOINED) +@NamedEntityGraph(name = "publishing-house-bio", graph = "name, ceo, boardMembers") +public class PublishingHouse { + @Id + private Integer id; + private String name; + + @ManyToOne + @JoinColumn(name = "ceo_fk") + private Person ceo; + + @OneToMany + @Bag + @JoinColumn(name = "board_mem_fk") + private Collection boardMembers; + + @OneToMany + @Bag + @JoinColumn(name = "ceo_fk") + private Collection publishers; + + @OneToMany + @Bag + @JoinColumn(name = "editor_fk") + private Collection editors; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/package-info.java new file mode 100644 index 000000000000..69e50c2adc2b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/package-info.java @@ -0,0 +1,10 @@ +/** + * Testing of Hibernate's {@linkplain org.hibernate.annotations.NamedEntityGraph}. + *

+ * We really only test the discovery and basic interpretation of the + * annotations here, since robust testing of the application of graphs + * is handled elsewhere. + * + * @author Steve Ebersole + */ +package org.hibernate.orm.test.entitygraph.named.parsed; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Book.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Book.java new file mode 100644 index 000000000000..03fe7cc2e7f9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Book.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.pkg; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Entity +public class Book { + @Id + private Integer id; + private String title; + @Embedded + private Isbn isbn; + + @ManyToOne + @JoinColumn(name = "author_fk") + Person author; + + @ManyToOne + @JoinColumn(name = "editor_fk") + Person editor; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Duplicator.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Duplicator.java new file mode 100644 index 000000000000..7582cf6a1fce --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Duplicator.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.pkg; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import org.hibernate.annotations.NamedEntityGraph; + +/** + * @author Steve Ebersole + */ +@Entity +@NamedEntityGraph( name = "duplicated-name", graph = "name") +public class Duplicator { + @Id + private Integer id; + @Basic + private String name; + + protected Duplicator() { + // for Hibernate use + } + + public Duplicator(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Isbn.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Isbn.java new file mode 100644 index 000000000000..705c8bdbc738 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Isbn.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.pkg; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class Isbn { + private String prefix; + private String element; + private String registrationGroup; + private String registrant; + private String publication; + private String checkDigit; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Person.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Person.java new file mode 100644 index 000000000000..12a038ac0175 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/Person.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.named.parsed.pkg; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Person { + @Id + private Integer id; + private String name; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/package-info.java new file mode 100644 index 000000000000..c0dfa7ae3ed0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg/package-info.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ + +/** + * @author Steve Ebersole + */ + +@NamedEntityGraph( name = "book-title-isbn", graph = "Book: title, isbn") +@NamedEntityGraph( name = "book-title-isbn-author", graph = "Book: title, isbn, author") +@NamedEntityGraph( name = "book-title-isbn-editor", graph = "Book: title, isbn, editor") +@NamedEntityGraph( name = "duplicated-name", graph = "Book: title") +package org.hibernate.orm.test.entitygraph.named.parsed.pkg; + +import org.hibernate.annotations.NamedEntityGraph; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg2/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg2/package-info.java new file mode 100644 index 000000000000..44d9ec5606b3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/named/parsed/pkg2/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ + +/** + * @author Steve Ebersole + */ +@NamedEntityGraph( name = "person-name", graph = "name") +package org.hibernate.orm.test.entitygraph.named.parsed.pkg2; + +import org.hibernate.annotations.NamedEntityGraph; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AbstractEntityGraphTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AbstractEntityGraphTest.java index f1a2d0838d3d..10209c4b2fee 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AbstractEntityGraphTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AbstractEntityGraphTest.java @@ -4,20 +4,12 @@ */ package org.hibernate.orm.test.entitygraph.parser; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import jakarta.persistence.AttributeNode; -import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; -import jakarta.persistence.Subgraph; import org.hibernate.graph.GraphParser; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; -import org.junit.Assert; - public abstract class AbstractEntityGraphTest extends BaseEntityManagerFunctionalTestCase { public AbstractEntityGraphTest() { @@ -38,67 +30,4 @@ protected RootGraphImplementor parseGraph(String gra return parseGraph( GraphParsingTestEntity.class, graphString ); } - private > void assertNullOrEmpty(C collection) { - if ( collection != null ) { - Assert.assertEquals( 0, collection.size() ); - } - } - - protected > void assertNullOrEmpty(M map) { - if ( map != null ) { - Assert.assertEquals( 0, map.size() ); - } - } - - protected void assertBasicAttributes(EntityGraph graph, String... names) { - Assert.assertNotNull( graph ); - assertBasicAttributes( graph.getAttributeNodes(), names ); - } - - protected void assertBasicAttributes(Subgraph graph, String... names) { - Assert.assertNotNull( graph ); - assertBasicAttributes( graph.getAttributeNodes(), names ); - } - - private void assertBasicAttributes(List> attrs, String... names) { - if ( ( names == null ) || ( names.length == 0 ) ) { - assertNullOrEmpty( attrs ); - } - else { - Assert.assertNotNull( attrs ); - Assert.assertTrue( names.length <= attrs.size() ); - - for ( String name : names ) { - AttributeNode node = null; - for ( AttributeNode candidate : attrs ) { - if ( candidate.getAttributeName().equals( name ) ) { - node = candidate; - break; - } - } - Assert.assertNotNull( node ); - assertNullOrEmpty( node.getKeySubgraphs() ); - assertNullOrEmpty( node.getSubgraphs() ); - } - } - } - - protected AttributeNode getAttributeNodeByName(EntityGraph graph, String name, boolean required) { - return getAttributeNodeByName( graph.getAttributeNodes(), name, required ); - } - - protected AttributeNode getAttributeNodeByName(Subgraph graph, String name, boolean required) { - return getAttributeNodeByName( graph.getAttributeNodes(), name, required ); - } - - private AttributeNode getAttributeNodeByName(List> attrs, String name, boolean required) { - for ( AttributeNode attr : attrs ) { - if ( name.equals( attr.getAttributeName() ) ) - return attr; - } - if ( required ) - Assert.fail( "Required attribute not found." ); - return null; - } - } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AssertionHelper.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AssertionHelper.java new file mode 100644 index 000000000000..ce387bf8aa53 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/AssertionHelper.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.parser; + +import jakarta.persistence.AttributeNode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.Subgraph; +import org.junit.Assert; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author Steve Ebersole + */ +public class AssertionHelper { + static > void assertNullOrEmpty(C collection) { + if ( collection != null ) { + Assert.assertEquals( 0, collection.size() ); + } + } + + public static > void assertNullOrEmpty(M map) { + if ( map != null ) { + Assert.assertEquals( 0, map.size() ); + } + } + + public static void assertBasicAttributes(EntityGraph graph, String... names) { + Assert.assertNotNull( graph ); + assertBasicAttributes( graph.getAttributeNodes(), names ); + } + + public static void assertBasicAttributes(Subgraph graph, String... names) { + Assert.assertNotNull( graph ); + assertBasicAttributes( graph.getAttributeNodes(), names ); + } + + public static void assertBasicAttributes(List> attrs, String... names) { + if ( ( names == null ) || ( names.length == 0 ) ) { + assertNullOrEmpty( attrs ); + } + else { + Assert.assertNotNull( attrs ); + Assert.assertTrue( names.length <= attrs.size() ); + + for ( String name : names ) { + AttributeNode node = null; + for ( AttributeNode candidate : attrs ) { + if ( candidate.getAttributeName().equals( name ) ) { + node = candidate; + break; + } + } + Assert.assertNotNull( node ); + assertNullOrEmpty( node.getKeySubgraphs() ); + assertNullOrEmpty( node.getSubgraphs() ); + } + } + } + + public static AttributeNode getAttributeNodeByName(EntityGraph graph, String name, boolean required) { + return getAttributeNodeByName( graph.getAttributeNodes(), name, required ); + } + + public static AttributeNode getAttributeNodeByName(Subgraph graph, String name, boolean required) { + return getAttributeNodeByName( graph.getAttributeNodes(), name, required ); + } + + public static AttributeNode getAttributeNodeByName(List> attrs, String name, boolean required) { + for ( AttributeNode attr : attrs ) { + if ( name.equals( attr.getAttributeName() ) ) + return attr; + } + if ( required ) + Assert.fail( "Required attribute not found." ); + return null; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTest.java index 0477bf5d076a..50bc2e0294ab 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTest.java @@ -40,13 +40,13 @@ public void testNullParsing() { @Test public void testOneBasicAttributeParsing() { EntityGraph graph = parseGraph( "name" ); - assertBasicAttributes( graph, "name" ); + AssertionHelper.assertBasicAttributes( graph, "name" ); } @Test public void testTwoBasicAttributesParsing() { EntityGraph graph = parseGraph( "name, description" ); - assertBasicAttributes( graph, "name", "description" ); + AssertionHelper.assertBasicAttributes( graph, "name", "description" ); } @Test @@ -59,10 +59,10 @@ public void testLinkParsing() { AttributeNode node = attrs.get( 0 ); assertNotNull( node ); assertEquals( "linkToOne", node.getAttributeName() ); - assertNullOrEmpty( node.getKeySubgraphs() ); + AssertionHelper.assertNullOrEmpty( node.getKeySubgraphs() ); @SuppressWarnings("rawtypes") Map sub = node.getSubgraphs(); - assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + AssertionHelper.assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); } @Test @@ -75,10 +75,10 @@ public void testMapKeyParsing() { AttributeNode node = attrs.get( 0 ); assertNotNull( node ); assertEquals( "map", node.getAttributeName() ); - assertNullOrEmpty( node.getSubgraphs() ); + AssertionHelper.assertNullOrEmpty( node.getSubgraphs() ); @SuppressWarnings("rawtypes") Map sub = node.getKeySubgraphs(); - assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + AssertionHelper.assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); } @Test @@ -91,10 +91,10 @@ public void testMapValueParsing() { AttributeNode node = attrs.get( 0 ); assertNotNull( node ); assertEquals( "map", node.getAttributeName() ); - assertNullOrEmpty( node.getKeySubgraphs() ); + AssertionHelper.assertNullOrEmpty( node.getKeySubgraphs() ); @SuppressWarnings("rawtypes") Map sub = node.getSubgraphs(); - assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + AssertionHelper.assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); } @Test @@ -103,39 +103,39 @@ public void testMixParsingWithMaps() { g = g.replace( " ", " " ); for ( int i = 1; i <= 2; i++, g = g.replace( " ", "" ) ) { EntityGraph graph = parseGraph( g ); - assertBasicAttributes( graph, "name", "description" ); + AssertionHelper.assertBasicAttributes( graph, "name", "description" ); - AttributeNode linkToOne = getAttributeNodeByName( graph, "linkToOne", true ); - assertNullOrEmpty( linkToOne.getKeySubgraphs() ); + AttributeNode linkToOne = AssertionHelper.getAttributeNodeByName( graph, "linkToOne", true ); + AssertionHelper.assertNullOrEmpty( linkToOne.getKeySubgraphs() ); @SuppressWarnings("rawtypes") Map linkToOneSubgraphs = linkToOne.getSubgraphs(); @SuppressWarnings("rawtypes") Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( linkToOneRoot, "name", "description" ); + AssertionHelper.assertBasicAttributes( linkToOneRoot, "name", "description" ); - AttributeNode linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true ); + AttributeNode linkToOneMap = AssertionHelper.getAttributeNodeByName( linkToOneRoot, "map", true ); @SuppressWarnings("rawtypes") Map linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs(); @SuppressWarnings("rawtypes") Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( linkToOneMapKeyRoot, "name" ); + AssertionHelper.assertBasicAttributes( linkToOneMapKeyRoot, "name" ); @SuppressWarnings("rawtypes") Map linkToOneMapSubgraphs = linkToOneMap.getSubgraphs(); @SuppressWarnings("rawtypes") Subgraph linkToOneMapRoot = linkToOneMapSubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( linkToOneMapRoot, "description" ); + AssertionHelper.assertBasicAttributes( linkToOneMapRoot, "description" ); - AttributeNode map = getAttributeNodeByName( graph, "map", true ); + AttributeNode map = AssertionHelper.getAttributeNodeByName( graph, "map", true ); @SuppressWarnings("rawtypes") Map mapKeySubgraphs = map.getKeySubgraphs(); @SuppressWarnings("rawtypes") Subgraph mapKeyRoot = mapKeySubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( mapKeyRoot, "name", "description" ); + AssertionHelper.assertBasicAttributes( mapKeyRoot, "name", "description" ); @SuppressWarnings("rawtypes") Map mapSubgraphs = map.getSubgraphs(); @SuppressWarnings("rawtypes") Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( mapRoot, "description" ); + AssertionHelper.assertBasicAttributes( mapRoot, "description" ); } } @@ -145,29 +145,29 @@ public void testMixParsingWithSimplifiedMaps() { g = g.replace( " ", " " ); for ( int i = 1; i <= 2; i++, g = g.replace( " ", "" ) ) { EntityGraph graph = parseGraph( g ); - assertBasicAttributes( graph, "name", "description" ); + AssertionHelper.assertBasicAttributes( graph, "name", "description" ); - AttributeNode linkToOne = getAttributeNodeByName( graph, "linkToOne", true ); - assertNullOrEmpty( linkToOne.getKeySubgraphs() ); + AttributeNode linkToOne = AssertionHelper.getAttributeNodeByName( graph, "linkToOne", true ); + AssertionHelper.assertNullOrEmpty( linkToOne.getKeySubgraphs() ); @SuppressWarnings("rawtypes") Map linkToOneSubgraphs = linkToOne.getSubgraphs(); @SuppressWarnings("rawtypes") Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( linkToOneRoot, "name", "description" ); + AssertionHelper.assertBasicAttributes( linkToOneRoot, "name", "description" ); - AttributeNode linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true ); + AttributeNode linkToOneMap = AssertionHelper.getAttributeNodeByName( linkToOneRoot, "map", true ); @SuppressWarnings("rawtypes") Map linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs(); @SuppressWarnings("rawtypes") Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( linkToOneMapKeyRoot, "name" ); + AssertionHelper.assertBasicAttributes( linkToOneMapKeyRoot, "name" ); - AttributeNode map = getAttributeNodeByName( graph, "map", true ); + AttributeNode map = AssertionHelper.getAttributeNodeByName( graph, "map", true ); @SuppressWarnings("rawtypes") Map mapSubgraphs = map.getSubgraphs(); @SuppressWarnings("rawtypes") Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class ); - assertBasicAttributes( mapRoot, "description", "name" ); + AssertionHelper.assertBasicAttributes( mapRoot, "description", "name" ); } } @@ -184,12 +184,12 @@ public void testLinkSubtypeParsing() { assertNotNull( linkToOneNode ); assertEquals( "linkToOne", linkToOneNode.getAttributeName() ); - assertNullOrEmpty( linkToOneNode.getKeySubgraphs() ); + AssertionHelper.assertNullOrEmpty( linkToOneNode.getKeySubgraphs() ); final SubGraphImplementor subgraph = linkToOneNode.getSubGraphs().get( GraphParsingTestSubEntity.class ); assertNotNull( subgraph ); - assertBasicAttributes( subgraph, "sub" ); + AssertionHelper.assertBasicAttributes( subgraph, "sub" ); } @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTypedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTypedTest.java new file mode 100644 index 000000000000..fbadea3d4bb0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/parser/EntityGraphParserTypedTest.java @@ -0,0 +1,214 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph.parser; + +import jakarta.persistence.AttributeNode; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.Subgraph; +import org.hibernate.graph.GraphParser; +import org.hibernate.graph.spi.AttributeNodeImplementor; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.graph.spi.SubGraphImplementor; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.hibernate.orm.test.entitygraph.parser.AssertionHelper.assertBasicAttributes; +import static org.hibernate.orm.test.entitygraph.parser.AssertionHelper.assertNullOrEmpty; +import static org.hibernate.orm.test.entitygraph.parser.AssertionHelper.getAttributeNodeByName; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * A unit test of {@link GraphParser} using root entity name in the graph text. + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = {GraphParsingTestEntity.class, GraphParsingTestSubEntity.class }) +@SessionFactory(exportSchema = false) +public class EntityGraphParserTypedTest { + + @Test + public void testOneBasicAttributeParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: name", + factoryScope.getSessionFactory() + ); + assertBasicAttributes( graph, "name" ); + } + + @Test + public void testTwoBasicAttributesParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: name, description", + factoryScope.getSessionFactory() + ); + assertBasicAttributes( graph, "name", "description" ); + } + + @Test + public void testLinkParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: linkToOne(name, description)", + factoryScope.getSessionFactory() + ); + assertNotNull( graph ); + List> attrs = graph.getAttributeNodes(); + assertNotNull( attrs ); + assertEquals( 1, attrs.size() ); + AttributeNode node = attrs.get( 0 ); + assertNotNull( node ); + assertEquals( "linkToOne", node.getAttributeName() ); + assertNullOrEmpty( node.getKeySubgraphs() ); + @SuppressWarnings("rawtypes") + Map sub = node.getSubgraphs(); + assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + } + + @Test + public void testMapKeyParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: map.key(name, description)", + factoryScope.getSessionFactory() + ); + assertNotNull( graph ); + List> attrs = graph.getAttributeNodes(); + assertNotNull( attrs ); + assertEquals( 1, attrs.size() ); + AttributeNode node = attrs.get( 0 ); + assertNotNull( node ); + assertEquals( "map", node.getAttributeName() ); + assertNullOrEmpty( node.getSubgraphs() ); + @SuppressWarnings("rawtypes") + Map sub = node.getKeySubgraphs(); + assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + } + + @Test + public void testMapValueParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: map.value(name, description)", + factoryScope.getSessionFactory() + ); + assertNotNull( graph ); + List> attrs = graph.getAttributeNodes(); + assertNotNull( attrs ); + assertEquals( 1, attrs.size() ); + AttributeNode node = attrs.get( 0 ); + assertNotNull( node ); + assertEquals( "map", node.getAttributeName() ); + assertNullOrEmpty( node.getKeySubgraphs() ); + @SuppressWarnings("rawtypes") + Map sub = node.getSubgraphs(); + assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" ); + } + + @Test + public void testMixParsingWithMaps(SessionFactoryScope factoryScope) { + String g = " name , linkToOne ( description, map . key ( name ) , map . value ( description ) , name ) , description , map . key ( name , description ) , map . value ( description ) "; + g = g.replace( " ", " " ); + for ( int i = 1; i <= 2; i++, g = g.replace( " ", "" ) ) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: " + g, + factoryScope.getSessionFactory() + ); + assertBasicAttributes( graph, "name", "description" ); + + AttributeNode linkToOne = getAttributeNodeByName( graph, "linkToOne", true ); + assertNullOrEmpty( linkToOne.getKeySubgraphs() ); + @SuppressWarnings("rawtypes") + Map linkToOneSubgraphs = linkToOne.getSubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( linkToOneRoot, "name", "description" ); + + AttributeNode linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true ); + @SuppressWarnings("rawtypes") + Map linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( linkToOneMapKeyRoot, "name" ); + @SuppressWarnings("rawtypes") + Map linkToOneMapSubgraphs = linkToOneMap.getSubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph linkToOneMapRoot = linkToOneMapSubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( linkToOneMapRoot, "description" ); + + AttributeNode map = getAttributeNodeByName( graph, "map", true ); + @SuppressWarnings("rawtypes") + Map mapKeySubgraphs = map.getKeySubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph mapKeyRoot = mapKeySubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( mapKeyRoot, "name", "description" ); + @SuppressWarnings("rawtypes") + Map mapSubgraphs = map.getSubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( mapRoot, "description" ); + } + } + + @Test + public void testMixParsingWithSimplifiedMaps(SessionFactoryScope factoryScope) { + String g = " name , linkToOne ( description, map . key ( name ) , name ) , description , map . value ( description, name ) "; + g = g.replace( " ", " " ); + for ( int i = 1; i <= 2; i++, g = g.replace( " ", "" ) ) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: " + g, + factoryScope.getSessionFactory() + ); + assertBasicAttributes( graph, "name", "description" ); + + AttributeNode linkToOne = getAttributeNodeByName( graph, "linkToOne", true ); + assertNullOrEmpty( linkToOne.getKeySubgraphs() ); + @SuppressWarnings("rawtypes") + Map linkToOneSubgraphs = linkToOne.getSubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( linkToOneRoot, "name", "description" ); + + AttributeNode linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true ); + @SuppressWarnings("rawtypes") + Map linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( linkToOneMapKeyRoot, "name" ); + + AttributeNode map = getAttributeNodeByName( graph, "map", true ); + @SuppressWarnings("rawtypes") + Map mapSubgraphs = map.getSubgraphs(); + @SuppressWarnings("rawtypes") + Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class ); + assertBasicAttributes( mapRoot, "description", "name" ); + } + } + + @Test + public void testLinkSubtypeParsing(SessionFactoryScope factoryScope) { + final EntityGraph graph = GraphParser.parse( + "GraphParsingTestEntity: linkToOne(name, description), linkToOne(GraphParsingTestSubEntity: sub)", + factoryScope.getSessionFactory() + ); + assertNotNull( graph ); + + List> attrs = ( (RootGraphImplementor) graph ).getAttributeNodeList(); + assertNotNull( attrs ); + assertEquals( 1, attrs.size() ); + + AttributeNodeImplementor linkToOneNode = attrs.get( 0 ); + assertNotNull( linkToOneNode ); + assertEquals( "linkToOne", linkToOneNode.getAttributeName() ); + + assertNullOrEmpty( linkToOneNode.getKeySubgraphs() ); + + final SubGraphImplementor subgraph = linkToOneNode.getSubGraphs().get( GraphParsingTestSubEntity.class ); + assertNotNull( subgraph ); + + assertBasicAttributes( subgraph, "sub" ); + } +} diff --git a/migration-guide.adoc b/migration-guide.adoc index 7e6667e26b46..dc90908147c2 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -34,6 +34,28 @@ earlier versions, see any other pertinent migration guides as well. See this https://in.relation.to/2024/04/01/jakarta-persistence-3/[blog post] for a good discussion of the changes in Jakarta Persistence 3.2. +[[NamedEntityGraph]] +== @NamedEntityGraph + +A new annotation (`@org.hibernate.annotations.NamedEntityGraph`) has been added to allow +specifying a named entity-graph using Hibernate's ability to parse a string representation of the graph. + + +[source,java] +---- +@Entity +@NamedEntityGraph( graph="title, isbn, author( name, phoneNumber )" ) +class Book { + // ... +} +---- + + +See `org.hibernate.graph.GraphParser` for details on the syntax and the +link:{user-guide-url}#fetching-strategies-dynamic-fetching-entity-graph-parsing-annotation[user guide] for additional details. + + + [[hibernate-models]] == Hibernate Models