HHH-12713 - Make EntityGraph creation more convenient

This commit is contained in:
Aleks 2018-06-20 16:42:57 -04:00 committed by Steve Ebersole
parent 2a5f270d40
commit 526ebb4e0b
14 changed files with 2401 additions and 0 deletions

View File

@ -248,6 +248,65 @@ include::{extrasdir}/fetching-strategies-dynamic-fetching-entity-subgraph-exampl
----
====
[[fetching-strategies-dynamic-fetching-entity-graph-parsing]]
==== Creating and applying JPA graphs from text representations
JPA graphs can also be created by parsing their text representations. These are not a part of the official JPA standard (yet).
Details of syntax can be found in the `EntityGraphParser` class JavaDoc. The equivalent text representation of previous example is as follows:
.Parsing a simple graph
====
[source, JAVA, indent=0]
----
include::{sourcedir}/GraphParsingTest.java[tags=fetching-strategies-dynamic-fetching-entity-graph-parsing-example-1]
----
====
More complex graphs can easily be created. For example, we could add in the usernames, passwords and access levels for each project employee
and also request usernames of all other employees of departments of initially found employees:
.Parsing a more complex graph
====
[source, JAVA, indent=0]
----
include::{sourcedir}/GraphParsingTest.java[tags=fetching-strategies-dynamic-fetching-entity-graph-parsing-example-2]
----
====
[[fetching-strategies-dynamic-fetching-entity-graph-merging]]
==== Combining multiple JPA entity graphs into one
Multiple entity graphs can be combined into a single supergraph that acts as a union. Graph from the previous example can also be build by combining separate aspect graphs into one, such as:
.Combining multiple graphs into one
====
[source, JAVA, indent=0]
----
include::{sourcedir}/GraphParsingTest.java[tags=fetching-strategies-dynamic-fetching-entity-graph-merging-example]
----
====
[[fetching-strategies-dynamic-fetching-entity-graph-applying]]
==== Applying entity graphs to queries
Convenience/utility methods were created to make application of entity graphs to queries easier. Following examples show
.Applying an entity graph to entityManager.find()
====
[source, JAVA, indent=0]
----
include::{sourcedir}/GraphParsingTest.java[tags=fetching-strategies-dynamic-fetching-entity-graph-apply-example-find]
----
====
.Applying an entity graph to entityManager.createQuery()
====
[source, JAVA, indent=0]
----
include::{sourcedir}/GraphParsingTest.java[tags=fetching-strategies-dynamic-fetching-entity-graph-apply-example-query]
----
====
[[fetching-strategies-dynamic-fetching-profile]]
=== Dynamic fetching via Hibernate profiles

View File

@ -0,0 +1,136 @@
package org.hibernate.userguide.fetching;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertNotNull;
import java.text.ParseException;
import java.util.Collections;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import org.hibernate.graph.AbstractEntityGraphTest;
import org.hibernate.graph.EntityGraphParser;
import org.hibernate.graph.EntityGraphs;
import org.hibernate.userguide.fetching.GraphFetchingTest.Employee;
import org.hibernate.userguide.fetching.GraphFetchingTest.Department;
import org.hibernate.userguide.fetching.GraphFetchingTest.Project;
import org.junit.Assert;
import org.junit.Test;
public class GraphParsingTest extends AbstractEntityGraphTest {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[]{ Project.class, Employee.class, Department.class };
}
@Test
public void testParsingExample1() {
EntityManager entityManager = getOrCreateEntityManager();
//tag::fetching-strategies-dynamic-fetching-entity-graph-parsing-example-1[]
final EntityGraph<Project> graph = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( department )"
);
//end::fetching-strategies-dynamic-fetching-entity-graph-parsing-example-1[]
Assert.assertNotNull( graph );
}
@Test
public void testParsingExample2() {
EntityManager entityManager = getOrCreateEntityManager();
//tag::fetching-strategies-dynamic-fetching-entity-graph-parsing-example-2[]
final EntityGraph<Project> graph = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( username, password, accessLevel, department( employees( username ) ) )"
);
//end::fetching-strategies-dynamic-fetching-entity-graph-parsing-example-2[]
Assert.assertNotNull( graph );
}
@Test
public void testMergingExample() {
EntityManager entityManager = getOrCreateEntityManager();
//tag::fetching-strategies-dynamic-fetching-entity-graph-merging-example[]
final EntityGraph<Project> a = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( username )"
);
final EntityGraph<Project> b = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( password, accessLevel )"
);
final EntityGraph<Project> c = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( department( employees( username ) ) )"
);
final EntityGraph<Project> all = EntityGraphs.merge( entityManager, Project.class, a, b, c );
//end::fetching-strategies-dynamic-fetching-entity-graph-merging-example[]
final EntityGraph<Project> expected = EntityGraphParser.parse(
entityManager,
Project.class,
"employees( username, password, accessLevel, department( employees( username ) ) )"
);
Assert.assertTrue( EntityGraphs.equal( expected, all ) );
}
@Test
public void testFindExample() {
doInJPA( this::entityManagerFactory, entityManager -> {
Long userId = 1L;
//tag::fetching-strategies-dynamic-fetching-entity-graph-apply-example-find[]
Employee employee = EntityGraphs.find(
entityManager,
Employee.class,
userId,
"username, accessLevel, department"
);
//end::fetching-strategies-dynamic-fetching-entity-graph-apply-example-find[]
} );
}
@Test
public void testQueryExample() {
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::fetching-strategies-dynamic-fetching-entity-graph-apply-example-query[]
final String graphString = "username, accessLevel";
final String queryString = "select e from Employee e where e.id = 1";
final EntityGraph<Employee> graph = EntityGraphParser.parse(
entityManager,
Employee.class,
graphString // == "username, accessLevel"
);
// Given above, the following query1:
TypedQuery<Employee> query1 = entityManager.createQuery( queryString, Employee.class );
EntityGraphs.setFetchGraph( query1, graph );
// is equal to the following query2:
TypedQuery<Employee> query2 = EntityGraphs.createQuery(
entityManager,
Employee.class,
"fetch " + graphString + " " + queryString
// == "fetch username, accessLevel select e from Employee e where e.id = 1"
);
//end::fetching-strategies-dynamic-fetching-entity-graph-apply-example-query[]
} );
}
}

View File

@ -0,0 +1,228 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.AttributeNode;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
/**
* A temporary internal representation of an entity graph attribute used in the process of
* {@linkplain EntityGraphs#merge(EntityManager, Class, EntityGraph...) merging multiple entity graphs}
* together.
*
* @author asusnjar
*/
class EntityGraphAttribute {
/**
* Name of the attribute.
*/
final String name;
/**
* An attributeSubclass-&gt;attributeName-&gt;{@code EntityGraphAttribute} map
* for map attribute keys (only applies to map attributes).
*/
Map<Class<?>, Map<String, EntityGraphAttribute>> keySubgraphs = new HashMap<>();
/**
* An attributeSubclass-&gt;attributeName-&gt;{@code EntityGraphAttribute} map
* for collection and/or map attribute values.
*/
Map<Class<?>, Map<String, EntityGraphAttribute>> valueSubgraphs = new HashMap<>();
/**
* Basic attribute constructor.
*
* @param name Name of the attribute.
*/
EntityGraphAttribute(String name) {
this.name = name;
}
/**
* Constructor from a JPA {@link AttributeNode}.
*
* @param attrNode JPA attribute node to replicate.
*/
EntityGraphAttribute(AttributeNode<?> attrNode) {
this( attrNode.getAttributeName() );
copySubgraphs( keySubgraphs, attrNode.getKeySubgraphs() );
copySubgraphs( valueSubgraphs, attrNode.getSubgraphs() );
}
/**
* Copies a JPA class-&gt;subgraph map to an internal
* class-&gt;attributeName-&gt;{@code EntityGraphAttribute} map structure.
*
* @param dest Internal map to replicate the source subgraphs into.
* @param source JPA class-subgraph map to copy subgraphs from.
*/
@SuppressWarnings("rawtypes")
private static void copySubgraphs(Map<Class<?>, Map<String, EntityGraphAttribute>> dest, Map<Class, Subgraph> source) {
for ( Map.Entry<Class, Subgraph> entry : source.entrySet() ) {
dest.put( entry.getKey(), mapOf( entry.getValue() ) );
}
}
/**
* Converts a JPA Subgraph to an internally used attributeName-&gt;{@code EntityGraphAttribute} map.
*
* @param jpaSubgraph JPA subgraph to convert.
*/
static Map<String, EntityGraphAttribute> mapOf(Subgraph<?> jpaSubgraph) {
Map<String, EntityGraphAttribute> result = null;
if ( jpaSubgraph != null ) {
result = new HashMap<>();
for ( AttributeNode<?> node : jpaSubgraph.getAttributeNodes() ) {
result.put( node.getAttributeName(), new EntityGraphAttribute( node ) );
}
}
return result;
}
/**
* Merges the {@code source} attributeName-&gt;{@link EntityGraphAttribute} into {@code dest}.
*
* @param dest Map of attributes to merge {@code source} into.
*
* @param source Map of attributes to merge into {@code dest}.
*
* @see #merge(Map, Subgraph)
*/
static void merge(Map<String, EntityGraphAttribute> dest, Map<String, EntityGraphAttribute> source) {
for ( Map.Entry<String, EntityGraphAttribute> entry : source.entrySet() ) {
String name = entry.getKey();
EntityGraphAttribute sourceAttr = entry.getValue();
EntityGraphAttribute destAttr = dest.get( name );
if ( destAttr == null ) {
dest.put( name, sourceAttr );
}
else {
mergeSubgraphMaps( destAttr.keySubgraphs, sourceAttr.keySubgraphs );
mergeSubgraphMaps( destAttr.valueSubgraphs, sourceAttr.valueSubgraphs );
}
}
}
/**
* Merges the {@code source} subgraph attributes into the {@code dest} attributeName-&gt;{@link EntityGraphAttribute} map.
*
* @param dest Map of attributes to merge {@code source} into.
*
* @param source Subgraph to merge into {@code dest}.
*
* @see #merge(Map, Map)
*/
static void merge(Map<String, EntityGraphAttribute> dest, Subgraph<?> source) {
merge( dest, mapOf( source ) );
}
/**
* Merges the {@code source} class-&gt;name-&gt;{@link EntityGraphAttribute} map into {@code dest}.
*
* @param dest Map of attributes to merge {@code source} into.
*
* @param source Map of attributes to merge into {@code dest}.
*/
private static void mergeSubgraphMaps(Map<Class<?>, Map<String, EntityGraphAttribute>> dest, Map<Class<?>, Map<String, EntityGraphAttribute>> source) {
for ( Map.Entry<Class<?>, Map<String, EntityGraphAttribute>> entry : source.entrySet() ) {
Class<?> subclass = entry.getKey();
Map<String, EntityGraphAttribute> sourceMap = entry.getValue();
Map<String, EntityGraphAttribute> destMap = dest.get( subclass );
if ( destMap == null ) {
dest.put( subclass, sourceMap );
}
else {
merge( destMap, sourceMap );
}
}
}
/**
* Configures the specified entity graph given a root type and attributeName-&gt;{@code EntityGraphAttribute} map.
*
* @param <T> Root entity type of the graph.
* @param graph Graph to configure.
* @param rootType Root entity type of the graph.
* @param map An attributeName-&gt;{@code EntityGraphAttribute} map to apply to the graph.
*
* @see #configure(Subgraph, Map)
*/
static <T> void configure(EntityGraph<T> graph, Class<T> rootType, Map<String, EntityGraphAttribute> map) {
configure( new GraphAsSubgraph<T>( graph, rootType ), map );
}
/**
* Configures the specified entity subgraph given an attributeName-&gt;{@code EntityGraphAttribute} map.
*
* @param subgraph Subraph to configure.
* @param map An attributeName-&gt;{@code EntityGraphAttribute} map to apply to the subgraph.
*
* @see #configure(EntityGraph, Class, Map)
*/
static void configure(Subgraph<?> subgraph, Map<String, EntityGraphAttribute> map) {
for ( EntityGraphAttribute attr : map.values() ) {
attr.configure( subgraph );
}
}
/**
* Applies the information in 'this' {@code EntityGraphAttribute} to configure the specified subgraph.
*
* @param subgraph JPA subgraph to configure.
*/
private void configure(Subgraph<?> subgraph) {
if ( !keySubgraphs.isEmpty() ) {
for ( Map.Entry<Class<?>, Map<String, EntityGraphAttribute>> entry : keySubgraphs.entrySet() ) {
Class<?> subclass = entry.getKey();
Subgraph<?> innerSubgraph;
if ( subclass == null ) {
innerSubgraph = subgraph.addKeySubgraph( name );
}
else {
innerSubgraph = subgraph.addKeySubgraph( name, subclass );
}
configure( innerSubgraph, entry.getValue() );
}
}
if ( !valueSubgraphs.isEmpty() ) {
for ( Map.Entry<Class<?>, Map<String, EntityGraphAttribute>> entry : valueSubgraphs.entrySet() ) {
Class<?> subclass = entry.getKey();
Subgraph<?> innerSubgraph;
if ( subclass == null ) {
innerSubgraph = subgraph.addSubgraph( name );
}
else {
innerSubgraph = subgraph.addSubgraph( name, subclass );
}
configure( innerSubgraph, entry.getValue() );
}
}
if ( keySubgraphs.isEmpty() && valueSubgraphs.isEmpty() ) {
subgraph.addAttributeNodes( name );
}
}
}

View File

@ -0,0 +1,396 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
/**
* A parser of {@link EntityGraph} string representations using a simple syntax.
* <p>
* A single graph is represented as a comma-separated list of attribute specifications, such as:
* </p>
*
* <pre>
* firstName, lastName, birthDate
* </pre>
* <p>
* Simple attributes that are to be fetched or loaded eagerly simply need their name listed. Relational/link attributes,
* such as *ToOne, collections and maps have an additional syntax to allow their attributes to be fetched - these are
* specified in parentheses following the attribute name, such as:
* </p>
*
* <pre>
* firstName, lastName, parents(firstName, lastName, children(firstName, lastName))
* </pre>
* <p>
* Maps require separate specification for their keys and values, as follows:
* </p>
*
* <pre>
* mapAttributeName.key(keyAttr1, keyAttr2, ....), mapAttributeName.value(valueAttr1, valueAttr2, ...)
* </pre>
* <p>
* To specify additional attributes to be fetched/loaded for subclasses add the class name after the attribute name as
* follows:
* </p>
*
* <pre>
* vehicles(brand, model), vehicles:Car(topSpeed), vehicles:Truck(maxCargoWeight),
* productOwners.key(name), productOwners.key:Swag(year),
* productOwners.value(firstName, lastName), productOwners.value:HistoricalUser(employmentEndDate)
* </pre>
* <p>
* Above, the "Car", "Truck", "Swag" and "HistoricalUser" are class names and indicate for which subclass of the (root)
* property/attribute class the subgraph inside the following parentheses apply to. The class names can take three
* different forms:
* </p>
* <ul>
* <li>e.g. {@code Truck} - a simple name of the class inside the same package as the property class in which scope this
* declaration is made (the superclass). For example, if the superclass is
* {@code com.bluecatnetworks.proteus.data.Vehicle}, then the resulting fully qualified name is
* {@code com.bluecatnetworks.proteus.data.Truck}.</li>
* <li>e.g. {@code .cargo.Truck} - (starting with a dot) a relative name to the package of the property class in which
* scope this declaration is made. For example, if the superclass is {@code com.bluecatnetworks.proteus.data.Vehicle},
* then the resulting fully qualified name is {@code com.bluecatnetworks.proteus.data.cargo.Truck}.</li>
* <li>e.g. {@code org.acme.Truck} - (containing dots but not starting with a dot) a fully qualified class name.</li>
* </ul>
* <p>
* To get an EntityGraph from a text form as per above, please use the
* {@link #parse(EntityManager, Class, CharSequence)} method. For example:
* </p>
* <code>
* {@link EntityGraph EntityGraph}&lt;Person&gt; personGraph = EntityGraphParser.{@link #parse(EntityManager, Class, CharSequence) parse}(entityManager, Person.class, "firstName, lastName");
* </code>
* <p>
* Multiple graphs made for the same entity type can be merged if it is necessary to satisfy multiple clients with the
* same query. Gather all the individual entity graphs, then merge them using
* {@link EntityGraphs#merge(EntityManager, Class, EntityGraph...)}.
* </p>
* <p>
* For additional convenience please use methods of {@link EntityGraphs}. They allow easier configuration of
* JPA/Hibernate queries and even embedding the above fetch/load graph text representation inside JPQL/HQL queries. See:
* </p>
* <ul>
* <li>{@link EntityGraphs#createQuery(EntityManager, Class, CharSequence)}</li>
* <li>{@link EntityGraphs#createNamedQuery(EntityManager, Class, String, CharSequence)}</li>
* <li>{@link EntityGraphs#find(EntityManager, Class, Object, CharSequence)}</li>
* <li>{@link FetchAndLoadEntityGraphs#applyTo(javax.persistence.Query)}</li>
* <li>{@link FetchAndLoadEntityGraphs#list(org.hibernate.Session, org.hibernate.Query)}</li>
* <li>{@link FetchAndLoadEntityGraphs#scroll(org.hibernate.Session, org.hibernate.Query)}</li>
* <li>{@link FetchAndLoadEntityGraphs#iterate(org.hibernate.Session, org.hibernate.Query)}</li>
* <li>{@link FetchAndLoadEntityGraphs#run(org.hibernate.Session, java.util.function.Supplier)}</li>
* </ul>
*
* @author asusnjar
*/
public final class EntityGraphParser {
/**
* Parses and returns the textual graph representation as {@linkplain EntityGraphParser described above}.
*
* @param <T> The root entity type of the graph.
*
* @param em JPA EntityManager to use to create the graph.
* @param rootType Root entity type of the graph.
* @param text Textual representation to parse.
*
* @return A parsed EntityGraph.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
public static <T> EntityGraph<T> parse(final EntityManager em, final Class<T> rootType, final CharSequence text) {
EntityGraph<T> graph = null;
if ( text != null ) {
graph = em.createEntityGraph( rootType );
parseInto( graph, rootType, text );
}
return graph;
}
/**
* Parses and returns the textual graph representation as {@linkplain EntityGraphParser described above}.
*
* @param <T> The root entity type of the graph.
*
* @param em JPA EntityManager to use to create the graph.
* @param rootType Root entity type of the graph.
* @param text Textual representation to parse.
*
* @return A parsed EntityGraph.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
private static <T> EntityGraph<T> parse(final EntityManager em, final Class<T> rootType, final ParseBuffer buffer) {
EntityGraph<T> graph = null;
if ( buffer != null ) {
graph = em.createEntityGraph( rootType );
parseInto( buffer, subgraphFromGraph( rootType, graph ) );
}
return graph;
}
/**
* Parses the textual graph representation as {@linkplain EntityGraphParser described above}
* into the specified graph.
*
* @param <T> The root entity type of the graph.
*
* @param graph JPA EntityGraph to parse into.
* @param rootType Root entity type of the graph.
* @param text Textual representation to parse.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
public static <T> void parseInto(EntityGraph<T> graph, final Class<T> rootType, final CharSequence text) {
if ( text != null ) {
parseInto( subgraphFromGraph( rootType, graph ), text );
}
}
/**
* Parses the textual graph representation as {@linkplain EntityGraphParser described above}
* into the specified graph.
*
* @param <T> The root entity type of the subgraph.
*
* @param graph JPA EntityGraph to parse into.
* @param rootType Root entity type of the graph.
* @param text Textual representation to parse.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
public static <T> void parseInto(Subgraph<T> subgraph, final CharSequence text) {
if ( text != null ) {
parseInto( new ParseBuffer( text ), subgraph );
}
}
/**
* Parses the parse buffer into the specified subgraph.
*
* @param <T> The root entity type of the subgraph.
*
* @param buffer Buffer to parse.
* @param targetSubgraph Subgraph to parse into.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
private static <T> void parseInto(ParseBuffer buffer, Subgraph<T> targetSubgraph) {
boolean expectSeparator = false;
while ( !buffer.isAtEnd() ) {
if ( expectSeparator ) {
buffer.skipWhitespace();
if ( !buffer.match( ',' ) ) {
break;
}
}
else {
expectSeparator = true;
}
buffer.skipWhitespace();
final String propertyIdentifier = buffer.consumeIdentifier( false );
if ( propertyIdentifier == null ) {
break;
}
buffer.skipWhitespace();
boolean mapKey, mapValue;
if ( buffer.match( "." ) ) {
buffer.skipWhitespace();
mapKey = buffer.match( "keys" ) || buffer.match( "key" );
mapValue = mapKey ? false : ( buffer.match( "values" ) || buffer.match( "value" ) );
}
else {
mapKey = mapValue = false;
}
// Check if there is a subgraph for this
buffer.skipWhitespace();
Class<? extends T> subclass = null;
if ( buffer.match( ':' ) ) {
buffer.skipWhitespace();
String subclassName = buffer.consumeIdentifier( true );
int firstDotPosition = subclassName.indexOf( '.' );
if ( firstDotPosition < 0 ) {
// No package specified at all, assume the same as of the owner entity
subclassName = targetSubgraph.getClassType().getPackage().getName() + "." + subclassName;
}
else if ( firstDotPosition == 0 ) {
// A subpackage of the owner entity's package specified
subclassName = targetSubgraph.getClassType().getPackage().getName() + subclassName;
}
subclass = getSubclassByName( targetSubgraph.getClassType(), subclassName );
buffer.skipWhitespace();
}
if ( buffer.match( '(' ) ) {
// Yes, a collection subgraph
final Subgraph<? extends T> subgraph;
if ( mapKey ) {
if ( subclass == null ) {
subgraph = targetSubgraph.addKeySubgraph( propertyIdentifier );
}
else {
subgraph = targetSubgraph.addKeySubgraph( propertyIdentifier, subclass );
}
}
else {
if ( subclass == null ) {
subgraph = targetSubgraph.addSubgraph( propertyIdentifier );
}
else {
subgraph = targetSubgraph.addSubgraph( propertyIdentifier, subclass );
}
}
parseInto( buffer, subgraph );
buffer.skipWhitespace();
if ( !buffer.match( ')' ) ) {
throw new InvalidGraphException( "Unclosed subgraph found near character " + buffer.getPosition() );
}
}
else {
// A simple attribute?
if ( subclass != null ) {
throw new InvalidGraphException( "Subclass specified for an attribute without a subgraph near character " + buffer.getPosition() );
}
if ( mapKey ) {
throw new InvalidGraphException( "A map key without a subgraph near character " + buffer.getPosition() );
}
if ( mapValue ) {
throw new InvalidGraphException( "A map value without a subgraph near character " + buffer.getPosition() );
}
targetSubgraph.addAttributeNodes( propertyIdentifier );
}
}
}
/**
* Returns the named subclass of the specified superclass if it is available and
* is indeed a subclass, throws an {@link InvalidGraphException} otherwise.
*
* @param <T> The superclass.
*
* @param superclass (Super)class that the named subclass must be a subclass of.
* @param subclassName Name of the subclass to find.
*
* @return A valid subclass of {@code superclass} with the specified name.
*
* @throws InvalidGraphException if the class is not found or is not a subclass of
* {@code superclass}.
*/
@SuppressWarnings("unchecked")
private static <T> Class<T> getSubclassByName(Class<T> superclass, String subclassName) {
Class<T> subclass = null;
try {
subclass = (Class<T>) Class.forName( subclassName );
}
catch (ClassNotFoundException e) {
throw new InvalidGraphException( "Unavailable subclass specified: " + subclassName, e );
}
if ( !superclass.isAssignableFrom( subclass ) ) {
throw new InvalidGraphException( "Specified class (" + subclassName + ") must be but is not a subclass of " + superclass.getName() );
}
return subclass;
}
/**
* Creates a {@link Subgraph}-like proxy of the specified {@link EntityGraph}.
* This helps reuse the code as {@link Subgraph} and {@link EntityGraph}, unfortunately,
* do not share declarations yet are almost identical.
*
* @param <T> The root entity type.
*
* @param rootType Root entity type of the graph.
* @param graph Graph to wrap into the new proxy.
*
* @return A {@link Subgraph}-like proxy of the specified {@link EntityGraph}.
*/
private static <T> Subgraph<T> subgraphFromGraph(final Class<T> rootType, final EntityGraph<T> graph) {
return new GraphAsSubgraph<T>( graph, rootType );
}
/**
* Parse the fetch and load textual graphs representations that are at the beginning of the
* JPQL or HQL query and returns the pair.
*
* <p>
* The textual representations of the graphs are the same as {@linkplain EntityGraphParser described above}
* but add extra structure. Specifically, the graphs must be preceeded with the keyword "fetch" and/or "load".
* This allows the parser to disambiguate the graphs but also to detect cases when no graphs are specified
* at all and we have a plain JPQL/HQL query.
* </p>
* <p>
* For example:
* </p>
* <ul>
* <li>{@code fetch name, description [select ...] from ...} contains a <i>fetch</i> graph specifying
* "name" and "description" attributes.</li>
* <li>{@code load name, description [select ...] from ...} contains a <i>load</i> graph specifying
* "name" and "description" attributes.</li>
* <li>{@code fetch name load description [select ...] from ...} contains a <i>fetch</i> graph specifying
* only the "name" and a <i>load</i> graph specifying the "description" attribute.</li>
* <li>{@code load description fetch name [select ...] from ...} is equivalent to previous.</li>
* <li>{@code fetch name fetch description [select ...] from ...} is invalid because it specifies two
* fetch graphs.</li>
* <li>{@code load name load description [select ...] from ...} is invalid because it specifies two
* load graphs.</li>
* </ul>
* <p>
* The parser stops short (before) the actual JPQL/HQL portion and leaves it in the parse buffer so
* that it can be passed to the corresponding query parsing/creation methods. It makes no attempt to
* understand, check or reconcile the fetch/load graph specifications with the query in part due to
* the fact that JPA standard is ambiguous as to how the FetchGraph and LoadGraph hints are applied
* to multiple items in the {@code select} clause.
* </p>
*
* @param <T> The root entity type.
*
* @param em Entity manager to use to create the graphs.
* @param rootType Root entity type for the graphs.
* @param buffer Parse buffer to use parse from.
*
* @return A {@link FetchAndLoadEntityGraphs} containing a pair of parsed graphs (fetch, load).
* If any of the graphs is not declared/included it will be {@code null}.
*
* @throw InvalidGraphException if the textual representation is invalid.
*/
static <T> FetchAndLoadEntityGraphs<T> parsePreQueryGraphDescriptors(final EntityManager em, Class<T> rootType, ParseBuffer buffer) {
EntityGraph<T> fetchGraph = null;
EntityGraph<T> loadGraph = null;
buffer.skipWhitespace();
boolean foundSomething;
do {
String word = buffer.consumeIdentifier( false );
if ( "fetch".equalsIgnoreCase( word ) ) {
buffer.skipWhitespace();
foundSomething = true;
fetchGraph = parse( em, rootType, buffer );
buffer.skipWhitespace();
}
else if ( "load".equalsIgnoreCase( word ) ) {
buffer.skipWhitespace();
foundSomething = true;
loadGraph = parse( em, rootType, buffer );
buffer.skipWhitespace();
}
else {
if ( word != null) {
buffer.back( word );
}
foundSomething = false;
}
} while ( foundSomething && ( ( fetchGraph == null ) || ( loadGraph == null ) ) );
return new FetchAndLoadEntityGraphs<T>( fetchGraph, loadGraph );
}
}

View File

@ -0,0 +1,352 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.AttributeNode;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.Subgraph;
import javax.persistence.TypedQuery;
/**
* A collection of {@link EntityGraph} utilities.
* These methods really belong inside other classes that we cannot modify, hence here.
*
* @author asusnjar
*/
public final class EntityGraphs {
public static String HINT_FETCHGRAPH = "javax.persistence.fetchgraph";
public static String HINT_LOADGRAPH = "javax.persistence.loadgraph";
/**
* Configures the query hints so that ONLY the attributes specified by the graph are eagerly fetched, regardless of
* what the mapping defaults indicate (even if they specify more properties to be loaded eagerly).
*
* @param <T> Root entity type of the query and graph.
*
* @param query Query to configure.
* @param graph Graph of properties/attributes to eagerly load.
*
* @return The same (input) query.
*
* @see #setFetchGraph(Query, EntityManager, Class, CharSequence)
* @see #setLoadGraph(Query, EntityGraph)
*/
public static <T> Query setFetchGraph(final Query query, EntityGraph<T> graph) {
return query.setHint( HINT_FETCHGRAPH, graph );
}
/**
* Configures the query hints so that ONLY the attributes specified by the graph are eagerly fetched, regardless of
* what the mapping defaults indicate (even if they specify more properties to be loaded eagerly).
*
* @param <T> Root entity type of the query and graph.
*
* @param query Query to configure.
* @param graph Graph of properties/attributes to eagerly load.
*
* @return The same (input) query.
*
* @see #setFetchGraph(Query, EntityGraph)
* @see #setLoadGraph(Query, EntityGraph)
*/
public static <T> Query setFetchGraph(final Query query, EntityManager em, Class<T> rootType, CharSequence graph) {
return setFetchGraph( query, EntityGraphParser.parse( em, rootType, graph ) );
}
/**
* Configures the query hints so that the attributes specified by the graph are eagerly fetched IN ADDITION to those
* specified to be loaded eagerly in the mapping.
*
* @param <T> Root entity type of the query and graph.
*
* @param query Query to configure.
* @param graph Graph of properties/attributes to eagerly load.
*
* @return The same (input) query.
*
* @see #setFetchGraph(Query, EntityGraph)
* @see #setLoadGraph(Query, EntityManager, Class, CharSequence)
*/
public static <T> Query setLoadGraph(final Query query, EntityGraph<T> graph) {
return query.setHint( HINT_LOADGRAPH, graph );
}
/**
* Configures the query hints so that the attributes specified by the graph are eagerly fetched IN ADDITION to those
* specified to be loaded eagerly in the mapping.
*
* @param <T> Root entity type of the query and graph.
*
* @param query Query to configure.
* @param graph Graph of properties/attributes to eagerly load.
*
* @return The same (input) query.
*
* @see #setFetchGraph(Query, EntityGraph)
* @see #setLoadGraph(Query, EntityGraph)
*/
public static <T> Query setLoadGraph(final Query query, EntityManager em, Class<T> rootType, CharSequence graph) {
return setLoadGraph( query, EntityGraphParser.parse( em, rootType, graph ) );
}
/**
* Creates a query just like {@link EntityManager#createQuery(String, Class)} does but allows the query string to
* begin with the "fetch" and/or "load" entity graph specifications (see {@link EntityGraphParser}) that will
* internally converted to correct hints (see {@link #setFetchGraph(Query, EntityManager, Class, CharSequence)} and
* {@link #setLoadGraph(Query, EntityManager, Class, CharSequence)}.
*
* @param <T> Root entity type of the query and graph.
*
* @param em EntityManager to use to create the query and entity graphs (if found).
* @param rootType The root type of results.
* @param qlString The query language string that can begin with fetch/load specification (see
* {@link EntityGraphParser}).
*
* @return The typed query preconfigured as per any fetch/load specifications in the {@code qlString}.
*
* @throws InvalidGraphException if a specified fetch or load graph is invalid.
*/
public static <T> TypedQuery<T> createQuery(final EntityManager em, Class<T> rootType, CharSequence qlString) {
ParseBuffer buffer = new ParseBuffer( qlString );
FetchAndLoadEntityGraphs<T> graphs = EntityGraphParser.parsePreQueryGraphDescriptors( em, rootType, buffer );
TypedQuery<T> query = em.createQuery( buffer.toString(), rootType );
if ( graphs.fetchGraph != null ) {
setFetchGraph( query, graphs.fetchGraph );
}
if ( graphs.loadGraph != null ) {
setLoadGraph( query, graphs.loadGraph );
}
return query;
}
/**
* Creates a query just like {@link EntityManager#createNamedQuery(String, Class)} does but allows the query string
* to begin with the "fetch" and/or "load" entity graph specifications (see {@link EntityGraphParser}) that will
* internally converted to correct hints (see {@link #setFetchGraph(Query, EntityManager, Class, CharSequence)} and
* {@link #setLoadGraph(Query, EntityManager, Class, CharSequence)}.
*
* @param <T> Root entity type of the query and graph.
*
* @param em EntityManager to use to create the query and entity graphs (if found).
* @param rootType The root type of results.
* @param qlString The query language string that can begin with fetch/load specification (see
* {@link EntityGraphParser}).
*
* @return The typed query preconfigured as per any fetch/load specifications in the {@code qlString}.
*
* @throws InvalidGraphException if a specified fetch or load graph is invalid.
*/
public static <T> TypedQuery<T> createNamedQuery(final EntityManager em, Class<T> rootType, String queryName, CharSequence graphString) {
final EntityGraph<T> graph = EntityGraphParser.parse( em, rootType, graphString );
final TypedQuery<T> query = em.createNamedQuery( queryName, rootType );
setFetchGraph( query, graph );
return query;
}
/**
* Finds an entity by its class and primary key just like
* {@link EntityManager#find(Class, Object, javax.persistence.LockModeType, Map)} does but includes the fetch entity
* graph hint as specified in the {@code graphString}.
*
* @param <T> Root entity type of the query and graph.
*
* @param em EntityManager to use.
* @param entityClass The type of the entity to find.
* @param primaryKey The primary key of the entity to find.
* @param graphString The specification of attributes to fetch (see {@link EntityGraphParser}).
*
* @return The entity if found, same as
* {@link EntityManager#find(Class, Object, javax.persistence.LockModeType, Map)}.
*
* @throws InvalidGraphException if a specified fetch or load graph is invalid.
*/
public static <T> T find(final EntityManager em, Class<T> entityClass, Object primaryKey, CharSequence graphString) {
em.find( entityClass, primaryKey );
EntityGraph<T> graph = EntityGraphParser.parse( em, entityClass, graphString );
Map<String, Object> props = new HashMap<String, Object>();
props.put( HINT_FETCHGRAPH, graph );
return em.find( entityClass, primaryKey, props );
}
/**
* Merges multiple entity graphs into a single graph that specifies the fetching/loading of all attributes the input
* graphs specify.
*
* @param <T> Root entity type of the query and graph.
*
* @param em EntityManager to use to create the new merged graph.
* @param rootType Root type of the entity for which the graph is being merged.
* @param graphs Graphs to merge.
*
* @return The merged graph.
*/
@SafeVarargs
public static <T> EntityGraph<T> merge(EntityManager em, Class<T> rootType, EntityGraph<T>... graphs) {
Map<String, EntityGraphAttribute> resultMap = null;
for ( EntityGraph<T> graph : graphs ) {
if ( graph != null ) {
Subgraph<T> pretendSubgraph = new GraphAsSubgraph<T>( graph, rootType );
if ( resultMap == null ) {
resultMap = EntityGraphAttribute.mapOf( pretendSubgraph );
}
else {
EntityGraphAttribute.merge( resultMap, pretendSubgraph );
}
}
}
EntityGraph<T> merged = em.createEntityGraph( rootType );
EntityGraphAttribute.configure( merged, rootType, resultMap );
return merged;
}
/**
* Compares two entity graphs and returns {@code true} if they are equal, ignoring attribute order.
*
* @param <T> Root entity type of BOTH graphs.
* @param a Graph to compare.
* @param b Graph to compare.
*
*/
public static <T> boolean equal(EntityGraph<T> a, EntityGraph<T> b) {
if ( a == b ) {
return true;
}
if ( ( a == null ) || ( b == null ) ) {
return false;
}
List<AttributeNode<?>> aNodes = a.getAttributeNodes();
List<AttributeNode<?>> bNodes = b.getAttributeNodes();
if ( aNodes.size() != bNodes.size() ) {
return false;
}
for ( AttributeNode<?> aNode : aNodes ) {
String attributeName = aNode.getAttributeName();
AttributeNode<?> bNode = null;
for ( AttributeNode<?> bCandidate : bNodes ) {
if ( attributeName.equals( bCandidate.getAttributeName() ) ) {
bNode = bCandidate;
break;
}
}
if ( !equal( aNode, bNode ) ) {
return false;
}
}
return true;
}
/**
* Compares two entity graph attribute node and returns {@code true} if they are equal, ignoring subgraph attribute
* order.
*/
public static boolean equal(AttributeNode<?> a, AttributeNode<?> b) {
if ( a == b ) {
return true;
}
if ( ( a == null ) || ( b == null ) ) {
return false;
}
if ( a.getAttributeName().equals( b.getAttributeName() ) ) {
return equal( a.getSubgraphs(), b.getSubgraphs() ) && equal( a.getKeySubgraphs(), b.getKeySubgraphs() );
}
else {
return false;
}
}
/**
* Compares two entity subgraph maps and returns {@code true} if they are equal, ignoring order.
*/
public static boolean equal(@SuppressWarnings("rawtypes") Map<Class, Subgraph> a, @SuppressWarnings("rawtypes") Map<Class, Subgraph> b) {
if ( a == b ) {
return true;
}
if ( ( a == null ) || ( b == null ) ) {
return false;
}
@SuppressWarnings("rawtypes")
Set<Class> aKeys = a.keySet();
@SuppressWarnings("rawtypes")
Set<Class> bKeys = b.keySet();
if ( aKeys.equals( bKeys ) ) {
for ( Class<?> clazz : aKeys ) {
if ( !bKeys.contains( clazz ) ) {
return false;
}
if ( !equal( a.get( clazz ), b.get( clazz ) ) ) {
return false;
}
}
return true;
}
else {
return false;
}
}
/**
* Compares two entity subgraphs and returns {@code true} if they are equal, ignoring attribute order.
*/
public static boolean equal(@SuppressWarnings("rawtypes") Subgraph a, @SuppressWarnings("rawtypes") Subgraph b) {
if ( a == b ) {
return true;
}
if ( ( a == null ) || ( b == null ) ) {
return false;
}
if ( a.getClassType() != b.getClassType() ) {
return false;
}
@SuppressWarnings("unchecked")
List<AttributeNode<?>> aNodes = a.getAttributeNodes();
@SuppressWarnings("unchecked")
List<AttributeNode<?>> bNodes = b.getAttributeNodes();
if ( aNodes.size() != bNodes.size() ) {
return false;
}
for ( AttributeNode<?> aNode : aNodes ) {
String attributeName = aNode.getAttributeName();
AttributeNode<?> bNode = null;
for ( AttributeNode<?> bCandidate : bNodes ) {
if ( attributeName.equals( bCandidate.getAttributeName() ) ) {
bNode = bCandidate;
break;
}
}
if ( !equal( aNode, bNode ) ) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,115 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionImplementor;
/**
* Internally used class to carry a pair of graphs (fetch and load),
* a result of {@link EntityGraphParser#parsePreQueryGraphDescriptors(EntityManager, Class, ParseBuffer)}.
*
* @author asusnjar
*
* @param <T> Root entity type of both graphs.
*/
class FetchAndLoadEntityGraphs<T> {
final EntityGraph<T> fetchGraph;
final EntityGraph<T> loadGraph;
public FetchAndLoadEntityGraphs(EntityGraph<T> fetchGraph, EntityGraph<T> loadGraph) {
this.fetchGraph = fetchGraph;
this.loadGraph = loadGraph;
}
public FetchAndLoadEntityGraphs(EntityManager em, Class<T> rootType, CharSequence fetchGraph, CharSequence loadGraph) {
this( EntityGraphParser.parse( em, rootType, fetchGraph ), EntityGraphParser.parse( em, rootType, loadGraph ) );
}
public FetchAndLoadEntityGraphs(EntityManager em, Class<T> rootType, CharSequence fetchGraph) {
this( em, rootType, fetchGraph, null );
}
public EntityGraph<T> getFetchGraph() {
return fetchGraph;
}
public EntityGraph<T> getLoadGraph() {
return loadGraph;
}
public void applyTo(Query query) {
if ( fetchGraph != null ) {
query.setHint( EntityGraphs.HINT_FETCHGRAPH, fetchGraph );
}
if ( loadGraph != null ) {
query.setHint( EntityGraphs.HINT_LOADGRAPH, loadGraph );
}
}
public <R> R run(Session session, Supplier<R> supplier) {
LoadQueryInfluencers influencers = ( (SessionImplementor) session ).getLoadQueryInfluencers();
EntityGraph<?> preexistingFetchGraph = influencers.getFetchGraph();
EntityGraph<?> preexistingLoadGraph = influencers.getLoadGraph();
try {
influencers.setFetchGraph( fetchGraph );
influencers.setLoadGraph( loadGraph );
return supplier.get();
}
finally {
influencers.setFetchGraph( preexistingFetchGraph );
influencers.setLoadGraph( preexistingLoadGraph );
}
}
/**
* @deprecated as {@link org.hibernate.Query} is deprecated.
*/
@SuppressWarnings({ "unchecked", "deprecation" })
@Deprecated
public List<T> list(Session session, @SuppressWarnings("rawtypes") org.hibernate.Query query) {
return (List<T>) run( session, () -> query.list() );
}
/**
* @deprecated as {@link org.hibernate.Query} is deprecated.
*/
@SuppressWarnings({ "unchecked", "deprecation" })
@Deprecated
public Iterator<T> iterate(Session session, @SuppressWarnings("rawtypes") org.hibernate.Query query) {
return (Iterator<T>) run( session, () -> query.iterate() );
}
/**
* @deprecated as {@link org.hibernate.Query} is deprecated.
*/
@SuppressWarnings("deprecation")
@Deprecated
public ScrollableResults scroll(Session session, @SuppressWarnings("rawtypes") org.hibernate.Query query) {
return run( session, () -> query.scroll() );
}
public boolean hasFetchGraph() {
return fetchGraph != null;
}
public boolean hasLoadGraph() {
return loadGraph != null;
}
}

View File

@ -0,0 +1,144 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.util.List;
import javax.persistence.AttributeNode;
import javax.persistence.EntityGraph;
import javax.persistence.Subgraph;
import javax.persistence.metamodel.Attribute;
/**
* A proxy class that wraps an {@link EntityGraph} instance and mimics the {@link Subgraph} interface
* by delegating method calls.
*
* @author asusnjar
*
* @param <T> Root entity type.
*/
final class GraphAsSubgraph<T> implements Subgraph<T> {
/**
* A wrapped (proxied) graph.
*/
private final EntityGraph<T> graph;
/**
* A root entity type.
*/
private final Class<T> rootType;
/**
* A sole constructor.
*
* @param graph Graph to wrap.
*
* @param rootType Root entity type of the graph
* (which, unfortunately, cannot be obtained from the graph itself)
*/
GraphAsSubgraph(EntityGraph<T> graph, Class<T> rootType) {
this.graph = graph;
this.rootType = rootType;
}
/**
* @see Subgraph#addAttributeNodes(String...)
*/
@Override
public void addAttributeNodes(String... attributeNames) {
graph.addAttributeNodes( attributeNames );
}
/**
* @see Subgraph#addAttributeNodes(Attribute...)
*/
@Override
public void addAttributeNodes(@SuppressWarnings("unchecked") Attribute<T, ?>... attributes) {
graph.addAttributeNodes( attributes );
}
/**
* @see Subgraph#addSubgraph(Attribute)
*/
@Override
public <X> Subgraph<X> addSubgraph(Attribute<T, X> attribute) {
return graph.addSubgraph( attribute );
}
/**
* @see Subgraph#addSubgraph(Attribute, Class)
*/
@Override
public <X> Subgraph<? extends X> addSubgraph(Attribute<T, X> attribute, Class<? extends X> type) {
return graph.addSubgraph( attribute, type );
}
/**
* @see Subgraph#addSubgraph(String)
*/
@Override
public <X> Subgraph<X> addSubgraph(String attributeName) {
return graph.addSubgraph( attributeName );
}
/**
* @see Subgraph#addSubgraph(String, Class)
*/
@Override
public <X> Subgraph<X> addSubgraph(String attributeName, Class<X> type) {
return graph.addSubgraph( attributeName, type );
}
/**
* @see Subgraph#addKeySubgraph(Attribute)
*/
@Override
public <X> Subgraph<X> addKeySubgraph(Attribute<T, X> attribute) {
return graph.addKeySubgraph( attribute );
}
/**
* @see Subgraph#addKeySubgraph(Attribute, Class)
*/
@Override
public <X> Subgraph<? extends X> addKeySubgraph(Attribute<T, X> attribute, Class<? extends X> type) {
return graph.addKeySubgraph( attribute, type );
}
/**
* @see Subgraph#addKeySubgraph(String)
*/
@Override
public <X> Subgraph<X> addKeySubgraph(String attributeName) {
return graph.addKeySubgraph( attributeName );
}
/**
* @see Subgraph#addKeySubgraph(String, Class)
*/
@Override
public <X> Subgraph<X> addKeySubgraph(String attributeName, Class<X> type) {
return graph.addKeySubgraph( attributeName, type );
}
/**
* @see Subgraph#getAttributeNodes()
*/
@Override
public List<AttributeNode<?>> getAttributeNodes() {
return graph.getAttributeNodes();
}
/**
* @see Subgraph#getClassType()
*/
@Override
public Class<T> getClassType() {
return rootType;
}
}

View File

@ -0,0 +1,37 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
/**
* Throw by {@link EntityGraphParser} to indicate textual entity graph representation parsing errors.
*
* @author asusnjar
*
*/
public class InvalidGraphException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvalidGraphException() {
}
public InvalidGraphException(String message) {
super( message );
}
public InvalidGraphException(Throwable cause) {
super( cause );
}
public InvalidGraphException(String message, Throwable cause) {
super( message, cause );
}
public InvalidGraphException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super( message, cause, enableSuppression, writableStackTrace );
}
}

View File

@ -0,0 +1,279 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
/**
* A parsing helper class used by the {@link EntityGraphParser} to traverse
* {@linkplain CharSequence character sequences) while tracking the parsing
* position.
*
* @author asusnjar
*
*/
class ParseBuffer implements CharSequence {
/**
* Character sequence being parsed.
*/
private final CharSequence text;
/**
* Length of the character sequence.
*/
private final int length;
/**
* Current parsing position.
*/
private int position = 0;
/**
* Main constructor.
*
* @param text Character sequence to parse.
*/
public ParseBuffer(CharSequence text) {
this.text = text;
this.length = text.length();
}
/**
* Returns the number of characters remaining in the buffer (starting from the current parsing position).
*
* @see CharSequence#length()
*/
@Override
public int length() {
return length - position;
}
/**
* Returns the characters remaining in the buffer (starting from the current parsing position).
*
* @see CharSequence#toString()
*/
@Override
public String toString() {
return text.subSequence( position, length ).toString();
}
/**
* Returns the character that is at the specified index <u>after</u> the current parsing position.
*
* @see CharSequence#charAt(int)
*/
@Override
public char charAt(int index) {
return text.charAt( position + index );
}
/**
* Returns a subsequence with {@code start} and {@code end} indexes being relative to the
* current parsing position.
*
* @see CharSequence#subSequence(int, int)
*/
@Override
public CharSequence subSequence(int start, int end) {
return text.subSequence( position + start, position + end );
}
/**
* Returns {@code true} if and only if we are at the end of the buffer.
*/
public boolean isAtEnd() {
return position >= length;
}
/**
* Skips all {@linkplain Character#isWhitespace(char) regular whitespace characters} in the buffer.
*/
public void skipWhitespace() {
while ( ( position < length ) && Character.isWhitespace( text.charAt( position ) ) ) {
position++;
}
}
/**
* Returns the character at the current parsing position, without advancing the position.
*
* @see ParseBuffer#consumeChar()
*/
public char peekChar() {
return text.charAt( position );
}
/**
* Returns the character at the current parsing position and advances to the next character.
*
* @see #peekChar()
*/
public char consumeChar() {
return text.charAt( position++ );
}
/**
* Returns the character sequence that follows the Java identifier name restrictions
* (see {@link Character#isJavaIdentifierStart(char)} and {@link Character#isJavaIdentifierPart(char)})
* and advances the parsing position accordingly.
*
* @param withDots If {@code true}, also allows/includes dots ({@code '.'}) in identifiers.
*
* @return A consumed identifier or {@code null} if at end of the buffer or a non-identifier
* character is at the current parsing position. Characters that match Java identifier name
* restrictions (and dots if {@code withDots==true}) are eagerly consumed.
*/
public CharSequence consumeIdentifierSeq(boolean withDots) {
if ( isAtEnd() ) {
return null;
}
char ch = peekChar();
if ( ( ( ch != '.' ) || !withDots ) && !Character.isJavaIdentifierStart( text.charAt( position ) ) ) {
return null;
}
int start = position;
int terminator = start + 1;
while ( terminator < length ) {
ch = text.charAt( terminator );
if ( ( !Character.isJavaIdentifierPart( ch ) ) && ( ( ch != '.' ) || !withDots ) ) {
break;
}
terminator++;
}
position = terminator;
return text.subSequence( start, terminator );
}
/**
* A convenience version of {@link #consumeIdentifierSeq(boolean)}.
*/
public String consumeIdentifier(boolean withDots) {
CharSequence id = consumeIdentifierSeq( withDots );
return ( id == null ) ? (String) null : id.toString();
}
/**
* Rewinds the parsing position by one character back.
*
* @see #back(int)
* @see #back(CharSequence)
*/
public void back() {
position--;
}
/**
* Rewinds the parsing position by the specified number of character.
*
* @param charCount Number of characters to move.
*/
public void back(int charCount) {
position -= charCount;
}
/**
* "Returns" the specified character sequence to the buffer.
*
* Present implementation does not validate that these characters match the preceding
* buffer content, it only rewinds the buffer based on the sequence length.
*
* However, this should not be abused - validation may be introduced at any time.
*/
public void back(CharSequence seq) {
back( seq.length() );
}
/**
* Returns {@code true} if and only if the next character in the buffer is equal to the specified
* character (case sensitive).
*
* @see #matchIgnoreCase(char)
* @see #match(CharSequence)
*/
public boolean match(char ch) {
if ( isAtEnd() || ( peekChar() != ch ) ) {
return false;
}
position++;
return true;
}
/**
* Returns {@code true} if and only if the next character in the buffer is equal to the specified
* character (case insensitive).
*
* @see #match(char)
* @see #matchIgnoreCase(CharSequence)
*/
public boolean matchIgnoreCase(char ch) {
if ( isAtEnd() || ( Character.toLowerCase( text.charAt( position ) ) != Character.toLowerCase( ch ) ) ) {
return false;
}
position++;
return true;
}
/**
* Returns {@code true} if and only if the next sequence of characters in the buffer
* are equal to the specified sequence (case sensitive).
*
* @see #matchIgnoreCase(CharSequence)
* @see #match(char)
*/
public boolean match(CharSequence seq) {
final int len = seq.length();
if ( len >= this.length() ) {
return false;
}
for ( int i = 0; i < len; i++ ) {
if ( seq.charAt( i ) != text.charAt( position + i ) ) {
return false;
}
}
position += len;
return true;
}
/**
* Returns {@code true} if and only if the next sequence of characters in the buffer
* are equal to the specified sequence (case insensitive).
*
* @see #match(CharSequence)
* @see #matchIgnoreCase(char)
*/
public boolean matchIgnoreCase(CharSequence seq) {
final int len = seq.length();
if ( len >= this.length() ) {
return false;
}
for ( int i = 0; i < len; i++ ) {
if ( Character.toLowerCase( seq.charAt( i ) ) != Character.toLowerCase( text.charAt( position + i ) ) ) {
return false;
}
}
position += len;
return true;
}
/**
* Returns the current parsing position.
*/
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,105 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.persistence.AttributeNode;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.junit.Assert;
public abstract class AbstractEntityGraphTest extends BaseEntityManagerFunctionalTestCase {
public AbstractEntityGraphTest() {
super();
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[]{ GraphParsingTestEntity.class, GraphParsingTestSubentity.class };
}
protected <T> EntityGraph<T> parseGraph(Class<T> entityType, String graphString) {
EntityManager entityManager = getOrCreateEntityManager();
return EntityGraphParser.parse( entityManager, entityType, graphString );
}
protected <T> EntityGraph<GraphParsingTestEntity> parseGraph(String graphString) {
return parseGraph( GraphParsingTestEntity.class, graphString );
}
private <C extends Collection<?>> void assertNullOrEmpty(C collection) {
if ( collection != null ) {
Assert.assertEquals( 0, collection.size() );
}
}
protected <M extends Map<?, ?>> 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<AttributeNode<?>> 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<AttributeNode<?>> 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;
}
}

View File

@ -0,0 +1,266 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import java.util.List;
import java.util.Map;
import javax.persistence.AttributeNode;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import javax.persistence.Subgraph;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
/**
* A unit test of {@link EntityGraphParser}.
*
* @author asusnjar
*/
public class EntityGraphParserTest extends AbstractEntityGraphTest {
@Test
public void testNullParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( (String) null );
Assert.assertNull( graph );
}
@Test
public void testOneBasicAttributeParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "name" );
assertBasicAttributes( graph, "name" );
}
@Test
public void testTwoBasicAttributesParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "name, description" );
assertBasicAttributes( graph, "name", "description" );
}
@Test
public void testLinkParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "linkToOne(name, description)" );
Assert.assertNotNull( graph );
List<AttributeNode<?>> attrs = graph.getAttributeNodes();
Assert.assertNotNull( attrs );
Assert.assertEquals( 1, attrs.size() );
AttributeNode<?> node = attrs.get( 0 );
Assert.assertNotNull( node );
Assert.assertEquals( "linkToOne", node.getAttributeName() );
assertNullOrEmpty( node.getKeySubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> sub = node.getSubgraphs();
assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" );
}
@Test
public void testMapKeyParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "map.key(name, description)" );
Assert.assertNotNull( graph );
List<AttributeNode<?>> attrs = graph.getAttributeNodes();
Assert.assertNotNull( attrs );
Assert.assertEquals( 1, attrs.size() );
AttributeNode<?> node = attrs.get( 0 );
Assert.assertNotNull( node );
Assert.assertEquals( "map", node.getAttributeName() );
assertNullOrEmpty( node.getSubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> sub = node.getKeySubgraphs();
assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" );
}
@Test
public void testMapValueParsing() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "map.value(name, description)" );
Assert.assertNotNull( graph );
List<AttributeNode<?>> attrs = graph.getAttributeNodes();
Assert.assertNotNull( attrs );
Assert.assertEquals( 1, attrs.size() );
AttributeNode<?> node = attrs.get( 0 );
Assert.assertNotNull( node );
Assert.assertEquals( "map", node.getAttributeName() );
assertNullOrEmpty( node.getKeySubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> sub = node.getSubgraphs();
assertBasicAttributes( sub.get( GraphParsingTestEntity.class ), "name", "description" );
}
@Test
@Ignore
public void testMixParsingWithMaps() {
// Fails due to: https://hibernate.atlassian.net/browse/HHH-12696
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( " ", "" ) ) {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( g );
assertBasicAttributes( graph, "name", "description" );
AttributeNode<?> linkToOne = getAttributeNodeByName( graph, "linkToOne", true );
assertNullOrEmpty( linkToOne.getKeySubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> linkToOneSubgraphs = linkToOne.getSubgraphs();
@SuppressWarnings("rawtypes")
Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( linkToOneRoot, "name", "description" );
AttributeNode<?> linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs();
@SuppressWarnings("rawtypes")
Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( linkToOneMapKeyRoot, "name" );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> linkToOneMapSubgraphs = linkToOneMap.getSubgraphs();
@SuppressWarnings("rawtypes")
Subgraph linkToOneMapRoot = linkToOneMapSubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( linkToOneMapRoot, "description" );
AttributeNode<?> map = getAttributeNodeByName( graph, "map", true );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> mapKeySubgraphs = map.getKeySubgraphs();
@SuppressWarnings("rawtypes")
Subgraph mapKeyRoot = mapKeySubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( mapKeyRoot, "name", "description" );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> mapSubgraphs = map.getSubgraphs();
@SuppressWarnings("rawtypes")
Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( mapRoot, "description" );
}
}
@Test
public void testMixParsingWithSimplifiedMaps() {
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( " ", "" ) ) {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( g );
assertBasicAttributes( graph, "name", "description" );
AttributeNode<?> linkToOne = getAttributeNodeByName( graph, "linkToOne", true );
assertNullOrEmpty( linkToOne.getKeySubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> linkToOneSubgraphs = linkToOne.getSubgraphs();
@SuppressWarnings("rawtypes")
Subgraph linkToOneRoot = linkToOneSubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( linkToOneRoot, "name", "description" );
AttributeNode<?> linkToOneMap = getAttributeNodeByName( linkToOneRoot, "map", true );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> linkToOneMapKeySubgraphs = linkToOneMap.getKeySubgraphs();
@SuppressWarnings("rawtypes")
Subgraph linkToOneMapKeyRoot = linkToOneMapKeySubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( linkToOneMapKeyRoot, "name" );
AttributeNode<?> map = getAttributeNodeByName( graph, "map", true );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> mapSubgraphs = map.getSubgraphs();
@SuppressWarnings("rawtypes")
Subgraph mapRoot = mapSubgraphs.get( GraphParsingTestEntity.class );
assertBasicAttributes( mapRoot, "description", "name" );
}
}
@Test
@Ignore("Cannot run due to Hibernate bug: https://hibernate.atlassian.net/browse/HHH-10378")
// TODO Re-enable when Hibernate bug HHH-10378 is fixed
public void testLinkSubtypeParsing() {
// https://hibernate.atlassian.net/browse/HHH-10378
//
// Specifically the isTreatable(...) method in org.hibernate.jpa.graph.internal.AttributeNodeImpl
//
// It states:
//
// Check to make sure that the java type of the given entity persister is treatable as the given type. In other
// words, is the given type a subclass of the class represented by the persister.
//
// But that does not clearly match the implementation (seems opposite):
//
// return type.isAssignableFrom( entityPersister.getMappedClass() );
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "linkToOne(name, description), linkToOne:MockSubentity(sub)" );
Assert.assertNotNull( graph );
List<AttributeNode<?>> attrs = graph.getAttributeNodes();
Assert.assertNotNull( attrs );
Assert.assertEquals( 1, attrs.size() );
AttributeNode<?> node = attrs.get( 0 );
Assert.assertNotNull( node );
Assert.assertEquals( "linkToOne", node.getAttributeName() );
assertNullOrEmpty( node.getKeySubgraphs() );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> sub = node.getSubgraphs();
assertBasicAttributes( sub.get( GraphParsingTestSubentity.class ), "sub" );
}
@Test
public void testHHH10378IsNotFixedYet() {
EntityManager entityManager = getOrCreateEntityManager();
try {
EntityGraph<GraphParsingTestEntity> graph = entityManager.createEntityGraph( GraphParsingTestEntity.class );
graph.addSubgraph( "linkToOne", GraphParsingTestSubentity.class );
Assert.fail( "https://hibernate.atlassian.net/browse/HHH-10378 appears to have been fixed. Please check and update the tests here." );
}
catch (IllegalArgumentException iax) {
if ( iax.getMessage().startsWith( "Attribute [linkToOne] cannot be treated as requested type" ) ) {
// This is, unfortunately, expected.
System.err.println( "*Sigh*, https://hibernate.atlassian.net/browse/HHH-10378 appears to not have been fixed yet." );
}
else {
Assert.fail( "https://hibernate.atlassian.net/browse/HHH-10378 may have been fixed. Please check and update the tests here." );
}
}
}
@Test
@Ignore("Cannot run due to Hibernate bug: https://hibernate.atlassian.net/browse/HHH-12696")
public void testHHH12696MapSubgraphsKeyFirst() {
EntityManager entityManager = getOrCreateEntityManager();
EntityGraph<GraphParsingTestEntity> graph = entityManager.createEntityGraph( GraphParsingTestEntity.class );
final String mapAttributeName = "map";
Subgraph<GraphParsingTestEntity> keySubgraph = graph.addKeySubgraph( mapAttributeName );
Subgraph<GraphParsingTestEntity> valueSubgraph = graph.addSubgraph( mapAttributeName );
checkMapKeyAndValueSubgraphs( graph, mapAttributeName, keySubgraph, valueSubgraph );
}
private void checkMapKeyAndValueSubgraphs(EntityGraph<GraphParsingTestEntity> graph, final String mapAttributeName, Subgraph<GraphParsingTestEntity> keySubgraph,
Subgraph<GraphParsingTestEntity> valueSubgraph) {
int count = 0;
for ( AttributeNode<?> node : graph.getAttributeNodes() ) {
if ( mapAttributeName.equals( node.getAttributeName() ) ) {
count++;
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> keySubgraphs = node.getKeySubgraphs();
Assert.assertTrue( "Missing the key subgraph", !keySubgraphs.isEmpty() );
Assert.assertSame( keySubgraph, keySubgraphs.get( GraphParsingTestEntity.class ) );
@SuppressWarnings("rawtypes")
Map<Class, Subgraph> valueSubgraphs = node.getSubgraphs();
Assert.assertTrue( "Missing the value subgraph", !valueSubgraphs.isEmpty() );
Assert.assertSame( valueSubgraph, valueSubgraphs.get( GraphParsingTestEntity.class ) );
}
}
Assert.assertEquals( 1, count );
}
@Test
@Ignore("Cannot run due to Hibernate bug: https://hibernate.atlassian.net/browse/HHH-12696")
public void testHHH12696MapSubgraphsValueFirst() {
EntityManager entityManager = getOrCreateEntityManager();
EntityGraph<GraphParsingTestEntity> graph = entityManager.createEntityGraph( GraphParsingTestEntity.class );
final String mapAttributeName = "map";
Subgraph<GraphParsingTestEntity> valueSubgraph = graph.addSubgraph( mapAttributeName );
Subgraph<GraphParsingTestEntity> keySubgraph = graph.addKeySubgraph( mapAttributeName );
checkMapKeyAndValueSubgraphs( graph, mapAttributeName, keySubgraph, valueSubgraph );
}
}

View File

@ -0,0 +1,192 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.graph;
import javax.persistence.EntityGraph;
import javax.persistence.EntityManager;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
public class EntityGraphsTest extends AbstractEntityGraphTest {
private final <T> void checkMerge(Class<T> rootType, EntityGraph<T> expected, @SuppressWarnings("unchecked") EntityGraph<T>... graphs) {
EntityManager entityManager = getOrCreateEntityManager();
EntityGraph<T> actual = EntityGraphs.merge( entityManager, rootType, graphs );
Assert.assertTrue( EntityGraphs.equal( expected, actual ) );
}
@SafeVarargs
private final void checkMerge(EntityGraph<GraphParsingTestEntity> expected, EntityGraph<GraphParsingTestEntity>... graphs) {
checkMerge( GraphParsingTestEntity.class, expected, graphs );
}
@Test
public void testSameBasicsEqual() {
EntityGraph<GraphParsingTestEntity> g = parseGraph( "name, description " );
Assert.assertTrue( EntityGraphs.equal( g, g ) );
}
@Test
public void testEqualBasicsEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "name, description " );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "description, name " );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentBasicsEqual1() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "name, description " );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "description " );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentBasicsEqual2() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "name " );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "description " );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testEqualLinksEqual1() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "linkToOne(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "linkToOne(description, name)" );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
@Ignore("Cannot run due to Hibernate bug: https://hibernate.atlassian.net/browse/HHH-10378")
public void testEqualLinksWithSubclassesEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "linkToOne(name), linkToOne:MockSubentity(description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "linkToOne:MockSubentity(description), linkToOne(name)" );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentLinksEqual1() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "linkToOne(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "linkToOne(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentLinksEqual2() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "linkToOne(name)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "linkToOne(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
@Ignore("Cannot run due to Hibernate bug: https://hibernate.atlassian.net/browse/HHH-10378")
public void testDifferentLinksEqual3() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "linkToOne(name), linkToOne:MockSubentity(description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "linkToOne(name, description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testEqualMapKeysEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.key(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.key(description, name)" );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentMapKeysEqual1() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.key(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.key(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentMapKeysEqual2() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.key(name)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.key(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testEqualMapValuesEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.value(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.value(description, name)" );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentMapValuesEqual1() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.value(name, description)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.value(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentMapValuesEqual2() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.value(name)" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "map.value(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testEqualComplexGraphsEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.key(name, description), name, linkToOne(description), description" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "description, map.key(description, name), name, linkToOne(description)" );
Assert.assertTrue( EntityGraphs.equal( a, b ) );
}
@Test
public void testDifferentComplexGraphsEqual() {
EntityGraph<GraphParsingTestEntity> a = parseGraph( "map.key(name, description), name, linkToOne(description), description" );
EntityGraph<GraphParsingTestEntity> b = parseGraph( "description, map.value(description, name), name, linkToOne(description)" );
Assert.assertFalse( EntityGraphs.equal( a, b ) );
}
@Test
public void testNullsEqual() {
Assert.assertTrue( EntityGraphs.equal( (EntityGraph<GraphParsingTestEntity>) null, (EntityGraph<GraphParsingTestEntity>) null ) );
}
@Test
public void testNullAndNonNullEqual() {
EntityGraph<GraphParsingTestEntity> graph = parseGraph( "name " );
Assert.assertFalse( EntityGraphs.equal( graph, (EntityGraph<GraphParsingTestEntity>) null ) );
Assert.assertFalse( EntityGraphs.equal( (EntityGraph<GraphParsingTestEntity>) null, graph ) );
}
@Test
public void testBasicMerge() {
EntityGraph<GraphParsingTestEntity> g1 = parseGraph( "name" );
EntityGraph<GraphParsingTestEntity> g2 = parseGraph( "description" );
EntityGraph<GraphParsingTestEntity> expected = parseGraph( "name, description " );
checkMerge( expected, g1, g2 );
}
@Test
public void testLinkMerge() {
EntityGraph<GraphParsingTestEntity> g1 = parseGraph( "linkToOne(name)" );
EntityGraph<GraphParsingTestEntity> g2 = parseGraph( "linkToOne(description)" );
EntityGraph<GraphParsingTestEntity> expected = parseGraph( "linkToOne(name, description) " );
checkMerge( expected, g1, g2 );
}
@Test
public void testMapKeyMerge() {
EntityGraph<GraphParsingTestEntity> g1 = parseGraph( "map.key(name)" );
EntityGraph<GraphParsingTestEntity> g2 = parseGraph( "map.key(description)" );
EntityGraph<GraphParsingTestEntity> expected = parseGraph( "map.key(name, description) " );
checkMerge( expected, g1, g2 );
}
@Test
public void testMapValueMerge() {
EntityGraph<GraphParsingTestEntity> g1 = parseGraph( "map.value(name)" );
EntityGraph<GraphParsingTestEntity> g2 = parseGraph( "map.value(description)" );
EntityGraph<GraphParsingTestEntity> expected = parseGraph( "map.value(name, description) " );
checkMerge( expected, g1, g2 );
}
}

View File

@ -0,0 +1,72 @@
package org.hibernate.graph;
import java.util.Map;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
@Entity
public class GraphParsingTestEntity {
private String id;
private String name;
private String description;
private GraphParsingTestEntity linkToOne;
private Map<GraphParsingTestEntity, GraphParsingTestEntity> map;
@Id
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Basic
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToOne
public GraphParsingTestEntity getLinkToOne() {
return linkToOne;
}
public void setLinkToOne(GraphParsingTestEntity linkToOne) {
this.linkToOne = linkToOne;
}
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@ElementCollection(targetClass = GraphParsingTestEntity.class)
public Map<GraphParsingTestEntity, GraphParsingTestEntity> getMap() {
return map;
}
public void setMap(Map<GraphParsingTestEntity, GraphParsingTestEntity> map) {
this.map = map;
}
@Basic
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,20 @@
package org.hibernate.graph;
import javax.persistence.Basic;
import javax.persistence.Entity;
@Entity
public class GraphParsingTestSubentity extends GraphParsingTestEntity {
private String sub;
@Basic
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
}